diff --git a/aw-datastore/src/datastore.rs b/aw-datastore/src/datastore.rs index 26bf4b6c..ae1d196b 100644 --- a/aw-datastore/src/datastore.rs +++ b/aw-datastore/src/datastore.rs @@ -914,10 +914,6 @@ impl DatastoreInstance { Ok(KeyValue { key: row.get(0)?, value: row.get(1)?, - timestamp: Some(DateTime::from_utc( - NaiveDateTime::from_timestamp_opt(row.get(2)?, 0).unwrap(), - Utc, - )), }) }) { Ok(result) => Ok(result), @@ -932,12 +928,12 @@ impl DatastoreInstance { } } - pub fn get_keys_starting( + pub fn get_key_values( &self, conn: &Connection, pattern: &str, - ) -> Result, DatastoreError> { - let mut stmt = match conn.prepare("SELECT key FROM key_value WHERE key LIKE ?") { + ) -> Result, DatastoreError> { + let mut stmt = match conn.prepare("SELECT key, value FROM key_value WHERE key LIKE ?") { Ok(stmt) => stmt, Err(err) => { return Err(DatastoreError::InternalError(format!( @@ -946,25 +942,30 @@ impl DatastoreInstance { } }; - let mut output = Vec::::new(); + let mut output = HashMap::::new(); // Rusqlite's get wants index and item type as parameters. - let result = stmt.query_map([pattern], |row| row.get::(0)); + let result = stmt.query_map([pattern], |row| { + Ok((row.get::(0)?, row.get::(1)?)) + }); match result { - Ok(keys) => { - for row in keys { + Ok(settings) => { + for row in settings { // Unwrap to String or panic on SQL row if type is invalid. Can't happen with a // properly initialized table. - output.push(row.unwrap()); + let (key, value) = row.unwrap(); + // Only return keys starting with "settings.". + if !key.starts_with("settings.") { + continue; + } + output.insert(key, value); } Ok(output) } Err(err) => match err { - rusqlite::Error::QueryReturnedNoRows => { - Err(DatastoreError::NoSuchKey(pattern.to_string())) - } - _ => Err(DatastoreError::InternalError(format!( - "Failed to get key_value rows starting with pattern {pattern}" - ))), + rusqlite::Error::QueryReturnedNoRows => Ok(output), + _ => Err(DatastoreError::InternalError( + "Failed to get settings".to_string(), + )), }, } } diff --git a/aw-datastore/src/worker.rs b/aw-datastore/src/worker.rs index 946c772f..11fa5c0a 100644 --- a/aw-datastore/src/worker.rs +++ b/aw-datastore/src/worker.rs @@ -52,7 +52,7 @@ pub enum Response { EventList(Vec), Count(i64), KeyValue(KeyValue), - StringVec(Vec), + KeyValues(HashMap) } #[allow(clippy::large_enum_variant)] @@ -74,9 +74,9 @@ pub enum Command { GetEventCount(String, Option>, Option>), DeleteEventsById(String, Vec), ForceCommit(), - InsertKeyValue(String, String), + GetKeyValues(String), GetKeyValue(String), - GetKeysStarting(String), + SetKeyValue(String, String), DeleteKeyValue(String), Close(), } @@ -275,7 +275,11 @@ impl DatastoreWorker { self.commit = true; Ok(Response::Empty()) } - Command::InsertKeyValue(key, data) => match ds.insert_key_value(tx, &key, &data) { + Command::GetKeyValues(pattern) => match ds.get_key_values(tx, pattern.as_str()) { + Ok(result) => Ok(Response::KeyValues(result)), + Err(e) => Err(e), + } + Command::SetKeyValue(key, data) => match ds.insert_key_value(tx, &key, &data) { Ok(()) => Ok(Response::Empty()), Err(e) => Err(e), }, @@ -283,10 +287,6 @@ impl DatastoreWorker { Ok(result) => Ok(Response::KeyValue(result)), Err(e) => Err(e), }, - Command::GetKeysStarting(pattern) => match ds.get_keys_starting(tx, &pattern) { - Ok(result) => Ok(Response::StringVec(result)), - Err(e) => Err(e), - }, Command::DeleteKeyValue(key) => match ds.delete_key_value(tx, &key) { Ok(()) => Ok(Response::Empty()), Err(e) => Err(e), @@ -475,46 +475,46 @@ impl Datastore { } } - pub fn insert_key_value(&self, key: &str, data: &str) -> Result<(), DatastoreError> { - let cmd = Command::InsertKeyValue(key.to_string(), data.to_string()); - let receiver = self.requester.request(cmd).unwrap(); - - _unwrap_response(receiver) - } - - pub fn delete_key_value(&self, key: &str) -> Result<(), DatastoreError> { - let cmd = Command::DeleteKeyValue(key.to_string()); - let receiver = self.requester.request(cmd).unwrap(); - - _unwrap_response(receiver) - } - - pub fn get_key_value(&self, key: &str) -> Result { - let cmd = Command::GetKeyValue(key.to_string()); + pub fn get_key_values(&self, pattern: &str) -> Result, DatastoreError> { + let cmd = Command::GetKeyValues(pattern.to_string()); let receiver = self.requester.request(cmd).unwrap(); match receiver.collect().unwrap() { Ok(r) => match r { - Response::KeyValue(value) => Ok(value), + Response::KeyValues(value) => Ok(value), _ => panic!("Invalid response"), }, Err(e) => Err(e), } } - pub fn get_keys_starting(&self, pattern: &str) -> Result, DatastoreError> { - let cmd = Command::GetKeysStarting(pattern.to_string()); + pub fn get_key_value(&self, key: &str) -> Result { + let cmd = Command::GetKeyValue(key.to_string()); let receiver = self.requester.request(cmd).unwrap(); match receiver.collect().unwrap() { Ok(r) => match r { - Response::StringVec(value) => Ok(value), + Response::KeyValue(kv) => Ok(kv), _ => panic!("Invalid response"), }, Err(e) => Err(e), } } + pub fn set_key_value(&self, key: &str, data: &str) -> Result<(), DatastoreError> { + let cmd = Command::SetKeyValue(key.to_string(), data.to_string()); + let receiver = self.requester.request(cmd).unwrap(); + + _unwrap_response(receiver) + } + + pub fn delete_key_value(&self, key: &str) -> Result<(), DatastoreError> { + let cmd = Command::DeleteKeyValue(key.to_string()); + let receiver = self.requester.request(cmd).unwrap(); + + _unwrap_response(receiver) + } + // Should block until worker has stopped pub fn close(&self) { info!("Sending close request to database"); diff --git a/aw-models/src/key_value.rs b/aw-models/src/key_value.rs index 6bfc87ce..60d13a88 100644 --- a/aw-models/src/key_value.rs +++ b/aw-models/src/key_value.rs @@ -12,7 +12,6 @@ pub struct Key { pub struct KeyValue { pub key: String, pub value: Value, - pub timestamp: Option>, } impl KeyValue { @@ -24,7 +23,6 @@ impl KeyValue { KeyValue { key: key.into(), value: value.into(), - timestamp: Some(timestamp), } } } diff --git a/aw-server/src/endpoints/mod.rs b/aw-server/src/endpoints/mod.rs index a080d2a0..f8ff3dda 100644 --- a/aw-server/src/endpoints/mod.rs +++ b/aw-server/src/endpoints/mod.rs @@ -139,9 +139,9 @@ pub fn build_rocket(server_state: ServerState, config: AWConfig) -> rocket::Rock "/api/0/settings", routes![ settings::setting_get, - settings::settings_list_get, settings::setting_set, - settings::setting_delete + settings::setting_delete, + settings::settings_get, ], ) .mount("/", rocket_cors::catch_all_options_routes()); diff --git a/aw-server/src/endpoints/settings.rs b/aw-server/src/endpoints/settings.rs index d28047da..a40367b9 100644 --- a/aw-server/src/endpoints/settings.rs +++ b/aw-server/src/endpoints/settings.rs @@ -1,11 +1,12 @@ use crate::endpoints::ServerState; +use aw_query::query; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; +use std::collections::HashMap; use std::sync::MutexGuard; use aw_datastore::Datastore; -use aw_models::{Key, KeyValue}; use crate::endpoints::HttpErrorJson; @@ -21,51 +22,65 @@ fn parse_key(key: String) -> Result { } } -#[post("/", data = "", format = "application/json")] -pub fn setting_set( - state: &State, - message: Json, -) -> Result { - let data = message.into_inner(); - - let setting_key = parse_key(data.key)?; - - let datastore: MutexGuard<'_, Datastore> = endpoints_get_lock!(state.datastore); - let result = datastore.insert_key_value(&setting_key, &data.value.to_string()); - - match result { - Ok(_) => Ok(Status::Created), - Err(err) => Err(err.into()), - } -} - #[get("/")] -pub fn settings_list_get(state: &State) -> Result>, HttpErrorJson> { +pub fn settings_get( + state: &State, +) -> Result>, HttpErrorJson> { let datastore = endpoints_get_lock!(state.datastore); - let queryresults = match datastore.get_keys_starting("settings.%") { + let queryresults = match datastore.get_key_values("settings.%") { Ok(result) => Ok(result), Err(err) => Err(err.into()), }; - let mut output = Vec::::new(); - for i in queryresults? { - output.push(Key { key: i }); + match queryresults { + Ok(settings) => { + // strip 'settings.' prefix from keys + let mut map: HashMap = HashMap::new(); + for (key, value) in settings.iter() { + map.insert(key.strip_prefix("settings.").unwrap_or(key).to_string(), serde_json::from_str(value.clone().as_str()).unwrap()); + } + Ok(Json(map)) + }, + Err(err) => Err(err), } - - Ok(Json(output)) } #[get("/")] pub fn setting_get( state: &State, key: String, -) -> Result, HttpErrorJson> { +) -> Result, HttpErrorJson> { let setting_key = parse_key(key)?; - let datastore = endpoints_get_lock!(state.datastore); match datastore.get_key_value(&setting_key) { - Ok(result) => Ok(Json(result)), + Ok(kv) => Ok(Json(kv.value)), + Err(err) => Err(err.into()), + } +} + +#[post("/", data = "", format = "application/json")] +pub fn setting_set( + state: &State, + key: String, + value: Json, +) -> Result { + let setting_key = parse_key(key)?; + let value_str = match serde_json::to_string(&value.0) { + Ok(value) => value, + Err(err) => { + return Err(HttpErrorJson::new( + Status::BadRequest, + format!("Invalid JSON: {}", err), + )) + } + }; + + let datastore: MutexGuard<'_, Datastore> = endpoints_get_lock!(state.datastore); + let result = datastore.set_key_value(&setting_key, &value_str); + + match result { + Ok(_) => Ok(Status::Created), Err(err) => Err(err.into()), } } diff --git a/aw-server/tests/api.rs b/aw-server/tests/api.rs index 18ecf67c..19e32d0f 100644 --- a/aw-server/tests/api.rs +++ b/aw-server/tests/api.rs @@ -480,15 +480,10 @@ mod api_tests { assert_eq!(res.into_string().unwrap(), r#"{"message":"EmptyQuery"}"#); } - fn set_setting_request(client: &Client, key: &str, value: Value) -> Status { - let body = serde_json::to_string(&KeyValue { - key: key.to_string(), - value, - timestamp: None, - }) - .unwrap(); + fn set_setting_request(client: &Client, key: &str, value: &Value) -> Status { + let body = serde_json::to_string(value).unwrap(); let res = client - .post("/api/0/settings/") + .post(format!("/api/0/settings/{}", key)) .header(ContentType::JSON) .header(Header::new("Host", "127.0.0.1:5600")) .body(body) @@ -496,33 +491,15 @@ mod api_tests { res.status() } - /// Asserts that 2 KeyValues are otherwise equal and first keyvalues timestamp is within - /// or equal with timestamp and second.timestamp - fn _equal_and_timestamp_in_range(before: DateTime, first: KeyValue, second: KeyValue) { - assert_eq!(first.key, second.key); - assert_eq!(first.value, second.value); - // Compare with second, not millisecond accuracy - assert!( - first.timestamp.unwrap().timestamp() >= before.timestamp(), - "{} wasn't after {}", - first.timestamp.unwrap().timestamp(), - before.timestamp() - ); - assert!( - first.timestamp < second.timestamp, - "{} wasn't before {}", - first.timestamp.unwrap(), - second.timestamp.unwrap() - ); - } - #[test] fn test_illegally_long_key() { let server = setup_testserver(); let client = Client::untracked(server).expect("valid instance"); // Test getting not found (getting nonexistent key) - let res = set_setting_request(&client, "thisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongk", json!("")); + let key = "thisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongkthisisaverylongk"; + let value = json!("test_value"); + let res = set_setting_request(&client, key, &value); assert_eq!(res, rocket::http::Status::BadRequest); } @@ -532,7 +509,9 @@ mod api_tests { let client = Client::untracked(server).expect("valid instance"); // Test value creation - let response_status = set_setting_request(&client, "test_key", json!("test_value")); + let key = "test_key"; + let value = json!("test_value"); + let response_status = set_setting_request(&client, key, &value); assert_eq!(response_status, rocket::http::Status::Created); } @@ -542,86 +521,90 @@ mod api_tests { let client = Client::untracked(server).expect("valid instance"); // Test getting not found (getting nonexistent key) + let key = "non_existent_key"; let res = client - .get("/api/0/settings/non_existent_key") + .get(format!("/api/0/settings/{}", key)) .header(Header::new("Host", "127.0.0.1:5600")) .dispatch(); assert_eq!(res.status(), rocket::http::Status::NotFound); } #[test] - fn settings_list_get() { + fn test_get_settings() { let server = setup_testserver(); let client = Client::untracked(server).expect("valid instance"); - let response1_status = set_setting_request(&client, "test_key", json!("")); + let key1 = "test_key"; + let key2 = "test_key_2"; + let value = json!("test_value"); + let response1_status = set_setting_request(&client, key1, &value); assert_eq!(response1_status, rocket::http::Status::Created); - let response2_status = set_setting_request(&client, "test_key_2", json!("")); + let response2_status = set_setting_request(&client, key2, &value); assert_eq!(response2_status, rocket::http::Status::Created); let res = client - .get("/api/0/settings/") + .get("/api/0/settings") .header(Header::new("Host", "127.0.0.1:5600")) .dispatch(); assert_eq!(res.status(), rocket::http::Status::Ok); - assert_eq!( - res.into_string().unwrap(), - r#"[{"key":"settings.test_key"},{"key":"settings.test_key_2"}]"# - ); + + let deserialized: Value = serde_json::from_str(&res.into_string().unwrap()).unwrap(); + let expected: Value = serde_json::from_str(r#"{"test_key_2":"test_value","test_key":"test_value"}"#).unwrap(); + assert_eq!(deserialized, expected); } #[test] - fn test_getting_setting() { + fn test_get_setting() { let server = setup_testserver(); let client = Client::untracked(server).expect("valid instance"); - let timestamp = Utc::now(); - let response_status = set_setting_request(&client, "test_key", json!("test_value")); + let key = "test_key"; + let value = json!("test_value"); + let response_status = set_setting_request(&client, key, &value); assert_eq!(response_status, rocket::http::Status::Created); // Test getting let res = client - .get("/api/0/settings/test_key") + .get(format!("/api/0/settings/{}", key)) .header(Header::new("Host", "127.0.0.1:5600")) .dispatch(); assert_eq!(res.status(), rocket::http::Status::Ok); - let deserialized: KeyValue = serde_json::from_str(&res.into_string().unwrap()).unwrap(); - _equal_and_timestamp_in_range( - timestamp, - deserialized, - KeyValue::new("settings.test_key", "test_value", Utc::now()), - ); + let deserialized: Value = serde_json::from_str(&res.into_string().unwrap()).unwrap(); + assert_eq!(deserialized, value); } #[test] - fn test_getting_setting_multiple_types() { + fn test_get_setting_list() { let server = setup_testserver(); let client = Client::untracked(server).expect("valid instance"); - let timestamp = Utc::now(); - // Test array - let response_status = set_setting_request(&client, "test_key_array", json!("[1,2,3]")); + let key = "test_key_array"; + let value = json!([1, 2, 3]); + let response_status = set_setting_request(&client, key, &value); assert_eq!(response_status, rocket::http::Status::Created); let res = client - .get("/api/0/settings/test_key_array") + .get(format!("/api/0/settings/{}", key)) .header(Header::new("Host", "127.0.0.1:5600")) .dispatch(); assert_eq!(res.status(), rocket::http::Status::Ok); - let deserialized: KeyValue = serde_json::from_str(&res.into_string().unwrap()).unwrap(); - _equal_and_timestamp_in_range( - timestamp, - deserialized, - KeyValue::new("settings.test_key_array", "[1,2,3]", Utc::now()), - ); + let deserialized: Value = serde_json::from_str(&res.into_string().unwrap()).unwrap(); + assert_eq!(deserialized, value); + } + + #[test] + fn test_get_setting_dict() { + let server = setup_testserver(); + let client = Client::untracked(server).expect("valid instance"); // Test dict + let key = "test_key_dict"; + let value = json!({"key": "value", "another_key": "another value"}); let response_status = set_setting_request( &client, - "test_key_dict", - json!("{key: 'value', another_key: 'another value'}"), + key, &value ); assert_eq!(response_status, rocket::http::Status::Created); @@ -630,42 +613,30 @@ mod api_tests { .header(Header::new("Host", "127.0.0.1:5600")) .dispatch(); assert_eq!(res.status(), rocket::http::Status::Ok); - let deserialized: KeyValue = serde_json::from_str(&res.into_string().unwrap()).unwrap(); - _equal_and_timestamp_in_range( - timestamp, - deserialized, - KeyValue::new( - "settings.test_key_dict", - "{key: 'value', another_key: 'another value'}", - Utc::now(), - ), - ); + let deserialized: Value = serde_json::from_str(&res.into_string().unwrap()).unwrap(); + assert_eq!(deserialized, value); } #[test] - fn test_updating_setting() { + fn test_set_setting() { let server = setup_testserver(); let client = Client::untracked(server).expect("valid instance"); - let timestamp = Utc::now(); - let post_1_status = set_setting_request(&client, "test_key", json!("test_value")); + let key = "test_key"; + let value1 = json!("test_value"); + let value2 = json!("changed_test_value"); + let post_1_status = set_setting_request(&client, key, &value1); assert_eq!(post_1_status, rocket::http::Status::Created); let res = client - .get("/api/0/settings/test_key") + .get(format!("/api/0/settings/{}", key)) .header(Header::new("Host", "127.0.0.1:5600")) .dispatch(); assert_eq!(res.status(), rocket::http::Status::Ok); - let deserialized: KeyValue = serde_json::from_str(&res.into_string().unwrap()).unwrap(); + let deserialized: Value = serde_json::from_str(&res.into_string().unwrap()).unwrap(); + assert_eq!(deserialized, value1); - _equal_and_timestamp_in_range( - timestamp, - deserialized, - KeyValue::new("settings.test_key", "test_value", Utc::now()), - ); - - let timestamp_2 = Utc::now(); - let post_2_status = set_setting_request(&client, "test_key", json!("changed_test_value")); + let post_2_status = set_setting_request(&client, key, &value2); assert_eq!(post_2_status, rocket::http::Status::Created); let res = client @@ -674,20 +645,17 @@ mod api_tests { .dispatch(); assert_eq!(res.status(), rocket::http::Status::Ok); - let new_deserialized: KeyValue = serde_json::from_str(&res.into_string().unwrap()).unwrap(); - _equal_and_timestamp_in_range( - timestamp_2, - new_deserialized, - KeyValue::new("settings.test_key", "changed_test_value", Utc::now()), - ); + let new_deserialized: Value = serde_json::from_str(&res.into_string().unwrap()).unwrap(); + assert_eq!(new_deserialized, value2); } #[test] - fn test_deleting_setting() { + fn test_delete_setting() { let server = setup_testserver(); let client = Client::untracked(server).expect("valid instance"); - let response_status = set_setting_request(&client, "test_key", json!("")); + let value = json!("test_value"); + let response_status = set_setting_request(&client, "test_key", &value); assert_eq!(response_status, rocket::http::Status::Created); // Test deleting