diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc644d..3c4c8f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * Add different file size limits for pictures (avatar/logo - 20k) and documents (invoices, registration, passport - 1mb) as well as an upper limit for bill files (100) * This limit is checked at creation/update time, not at the time of uploading a temporary file * Add the address of the signer for the calls to `endorsements` and `past_endorsees` +* Add api call `active_notifications_for_node_ids` on `notification` API, which returns for a set of node ids, whether they have active notifications + * If the set of node ids is empty, only the node ids that have active notifications are returned # 0.4.2 diff --git a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs index 5ae20806..075eb0ac 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs @@ -750,6 +750,17 @@ impl NotificationServiceApi for DefaultNotificationService { .collect() } + async fn get_active_notification_status_for_node_ids( + &self, + node_ids: &[NodeId], + ) -> Result> { + Ok(self + .notification_store + .get_active_status_for_node_ids(node_ids) + .await + .unwrap_or_default()) + } + async fn check_bill_notification_sent( &self, bill_id: &BillId, diff --git a/crates/bcr-ebill-api/src/tests/mod.rs b/crates/bcr-ebill-api/src/tests/mod.rs index 3137d01b..0cd425d1 100644 --- a/crates/bcr-ebill-api/src/tests/mod.rs +++ b/crates/bcr-ebill-api/src/tests/mod.rs @@ -315,6 +315,10 @@ pub mod tests { #[async_trait] impl NotificationStoreApi for NotificationStoreApiMock { + async fn get_active_status_for_node_ids( + &self, + node_ids: &[NodeId], + ) -> Result>; async fn add(&self, notification: Notification) -> Result; async fn list(&self, filter: NotificationFilter) -> Result>; async fn get_latest_by_references( @@ -426,6 +430,10 @@ pub mod tests { async fn mark_notification_as_done(&self, notification_id: &str) -> bcr_ebill_transport::Result<()>; async fn get_active_bill_notification(&self, bill_id: &BillId) -> Option; async fn get_active_bill_notifications(&self, bill_ids: &[BillId]) -> HashMap; + async fn get_active_notification_status_for_node_ids( + &self, + node_ids: &[NodeId], + ) -> bcr_ebill_transport::Result>; async fn check_bill_notification_sent( &self, bill_id: &BillId, diff --git a/crates/bcr-ebill-persistence/src/db/notification.rs b/crates/bcr-ebill-persistence/src/db/notification.rs index a458410d..ec05569c 100644 --- a/crates/bcr-ebill-persistence/src/db/notification.rs +++ b/crates/bcr-ebill-persistence/src/db/notification.rs @@ -40,6 +40,39 @@ impl ServiceTraitBounds for SurrealNotificationStore {} #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl NotificationStoreApi for SurrealNotificationStore { + /// Returns node ids with an active notification for the given node ids + async fn get_active_status_for_node_ids( + &self, + node_ids: &[NodeId], + ) -> Result> { + let mut bindings = Bindings::default(); + bindings.add("table", Self::TABLE)?; + bindings.add("node_ids", node_ids.to_owned())?; + + let node_id_filter = if node_ids.is_empty() { + "" + } else { + "and node_id in $node_ids" + }; + + let result: Vec = self.db.query(&format!("SELECT node_id from notifications where active = true {node_id_filter} GROUP BY node_id"), bindings).await?; + let mut res: HashMap = HashMap::new(); + + if node_ids.is_empty() { + for node_id_db in result { + res.insert(node_id_db.node_id.to_owned(), true); + } + } else { + for node_id in node_ids { + res.insert( + node_id.to_owned(), + result.iter().any(|n| n.node_id == *node_id), + ); + } + } + Ok(res) + } + /// Stores a new notification into the database async fn add(&self, notification: Notification) -> Result { let id = notification.id.to_owned(); @@ -205,6 +238,11 @@ impl NotificationStoreApi for SurrealNotificationStore { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeIdDb { + pub node_id: NodeId, +} + #[derive(Debug, Clone, Serialize, Deserialize)] struct NotificationDb { pub id: Thing, @@ -274,7 +312,7 @@ mod tests { use super::*; use crate::{ db::get_memory_db, - tests::tests::{bill_id_test, bill_id_test_other, node_id_test}, + tests::tests::{bill_id_test, bill_id_test_other, node_id_test, node_id_test_other}, util::date::now, }; @@ -533,6 +571,86 @@ mod tests { }); } + #[tokio::test] + async fn test_returns_active_status_for_node_ids() { + let store = get_store().await; + let notification1 = test_notification(&bill_id_test(), Some(test_payload())); + let mut notification2 = test_notification(&bill_id_test_other(), Some(test_payload())); + notification2.node_id = Some(node_id_test_other()); + let notification3 = test_general_notification(); + let _ = store + .add(notification1.clone()) + .await + .expect("notification created"); + let _ = store + .add(notification2.clone()) + .await + .expect("notification created"); + let _ = store + .add(notification3.clone()) + .await + .expect("notification created"); + + let status = store + .get_active_status_for_node_ids(&[]) + .await + .expect("returns status"); + + assert_eq!(status.len(), 2, "should have all node ids in list"); + assert!(status.get(&node_id_test()).unwrap()); + assert!(status.get(&node_id_test_other()).unwrap()); + + let status = store + .get_active_status_for_node_ids(&[node_id_test()]) + .await + .expect("returns status"); + + assert_eq!( + status.len(), + 1, + "should have all given node ids in the list" + ); + assert!(status.get(&node_id_test()).unwrap()); + + store + .mark_as_done(¬ification2.clone().id) + .await + .expect("notification marked done"); + + let status = store + .get_active_status_for_node_ids(&[node_id_test_other()]) + .await + .expect("returns status"); + + assert_eq!( + status.len(), + 1, + "should have all given node ids in the list" + ); + assert!(!status.get(&node_id_test_other()).unwrap()); + + let status = store + .get_active_status_for_node_ids(&[node_id_test(), node_id_test_other()]) + .await + .expect("returns status"); + + assert_eq!(status.len(), 2, "should have all given node ids in list"); + assert!(status.get(&node_id_test()).unwrap()); + assert!(!status.get(&node_id_test_other()).unwrap()); + + let status = store + .get_active_status_for_node_ids(&[]) + .await + .expect("returns status"); + + assert_eq!( + status.len(), + 1, + "should have all active notif node ids in list" + ); + assert!(status.get(&node_id_test()).unwrap()); + } + fn test_notification(bill_id: &BillId, payload: Option) -> Notification { Notification::new_bill_notification(bill_id, &node_id_test(), "test_notification", payload) } diff --git a/crates/bcr-ebill-persistence/src/notification.rs b/crates/bcr-ebill-persistence/src/notification.rs index 5644050e..3d60d192 100644 --- a/crates/bcr-ebill-persistence/src/notification.rs +++ b/crates/bcr-ebill-persistence/src/notification.rs @@ -12,6 +12,11 @@ use bcr_ebill_core::{ #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NotificationStoreApi: ServiceTraitBounds { + /// Returns node ids with an active notification for the given node ids + async fn get_active_status_for_node_ids( + &self, + node_ids: &[NodeId], + ) -> Result>; /// Stores a new notification into the database async fn add(&self, notification: Notification) -> Result; /// Returns all currently active notifications from the database diff --git a/crates/bcr-ebill-transport/src/handler/mod.rs b/crates/bcr-ebill-transport/src/handler/mod.rs index 429e8057..63265c44 100644 --- a/crates/bcr-ebill-transport/src/handler/mod.rs +++ b/crates/bcr-ebill-transport/src/handler/mod.rs @@ -241,6 +241,10 @@ mod test_utils { #[async_trait] impl NotificationStoreApi for NotificationStore { + async fn get_active_status_for_node_ids( + &self, + node_ids: &[NodeId], + ) -> Result>; async fn add(&self, notification: Notification) -> Result; async fn list(&self, filter: NotificationFilter) -> Result>; async fn get_latest_by_references( diff --git a/crates/bcr-ebill-transport/src/notification_service.rs b/crates/bcr-ebill-transport/src/notification_service.rs index 2155dc8f..6fa5fae8 100644 --- a/crates/bcr-ebill-transport/src/notification_service.rs +++ b/crates/bcr-ebill-transport/src/notification_service.rs @@ -161,6 +161,11 @@ pub trait NotificationServiceApi: ServiceTraitBounds { bill_ids: &[BillId], ) -> HashMap; + async fn get_active_notification_status_for_node_ids( + &self, + node_ids: &[NodeId], + ) -> Result>; + /// Returns whether a notification was already sent for the given bill id and action async fn check_bill_notification_sent( &self, diff --git a/crates/bcr-ebill-wasm/index.html b/crates/bcr-ebill-wasm/index.html index 2df3c3d5..55e0ba8e 100644 --- a/crates/bcr-ebill-wasm/index.html +++ b/crates/bcr-ebill-wasm/index.html @@ -29,6 +29,7 @@

Uploads

Notification Testing

+
diff --git a/crates/bcr-ebill-wasm/main.js b/crates/bcr-ebill-wasm/main.js index 05d08f3f..e0fc4f0b 100644 --- a/crates/bcr-ebill-wasm/main.js +++ b/crates/bcr-ebill-wasm/main.js @@ -2,6 +2,7 @@ import * as wasm from '../pkg/index.js'; document.getElementById("fileInput").addEventListener("change", uploadFile); document.getElementById("notif").addEventListener("click", triggerNotif); +document.getElementById("get_active_notif_status").addEventListener("click", getActiveNotif); document.getElementById("company_create").addEventListener("click", createCompany); document.getElementById("contact_test").addEventListener("click", triggerContact); document.getElementById("contact_test_anon").addEventListener("click", triggerAnonContact); @@ -591,3 +592,10 @@ async function deleteContact() { await measured(); } +async function getActiveNotif() { + let measured = measure(async () => { + return await notificationTriggerApi.active_notifications_for_node_ids([]); + }); + await measured(); +} + diff --git a/crates/bcr-ebill-wasm/src/api/notification.rs b/crates/bcr-ebill-wasm/src/api/notification.rs index a44c38ea..f01a58bd 100644 --- a/crates/bcr-ebill-wasm/src/api/notification.rs +++ b/crates/bcr-ebill-wasm/src/api/notification.rs @@ -1,9 +1,12 @@ use super::Result; use crate::{ context::get_ctx, - data::{NotificationFilters, notification::NotificationWeb}, + data::{ + NotificationFilters, + notification::{NotificationStatusWeb, NotificationWeb}, + }, }; -use bcr_ebill_api::NotificationFilter; +use bcr_ebill_api::{NotificationFilter, data::NodeId}; use log::{error, info}; use wasm_bindgen::prelude::*; @@ -17,6 +20,24 @@ impl Notification { Notification } + #[wasm_bindgen(unchecked_return_type = "NotificationStatusWeb[]")] + pub async fn active_notifications_for_node_ids( + &self, + #[wasm_bindgen(unchecked_param_type = "Vec")] node_ids: JsValue, + ) -> Result { + let node_ids_parsed: Vec = serde_wasm_bindgen::from_value(node_ids)?; + let notification_status = get_ctx() + .notification_service + .get_active_notification_status_for_node_ids(&node_ids_parsed) + .await?; + let web: Vec = notification_status + .into_iter() + .map(|(node_id, active)| NotificationStatusWeb { node_id, active }) + .collect(); + let res = serde_wasm_bindgen::to_value(&web)?; + Ok(res) + } + #[wasm_bindgen] pub async fn subscribe(&self, callback: js_sys::Function) { wasm_bindgen_futures::spawn_local(async move { diff --git a/crates/bcr-ebill-wasm/src/data/notification.rs b/crates/bcr-ebill-wasm/src/data/notification.rs index 668db011..371fe9e1 100644 --- a/crates/bcr-ebill-wasm/src/data/notification.rs +++ b/crates/bcr-ebill-wasm/src/data/notification.rs @@ -7,6 +7,14 @@ use serde_json::Value; use tsify::Tsify; use wasm_bindgen::prelude::*; +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct NotificationStatusWeb { + #[tsify(type = "string")] + pub node_id: NodeId, + pub active: bool, +} + #[derive(Tsify, Debug, Clone, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct NotificationWeb {