Skip to main content

nexus_common/models/
traits.rs

1use crate::db::{exec_single_row, get_neo4j_graph, RedisOps};
2use crate::types::DynError;
3use async_trait::async_trait;
4use core::fmt;
5use neo4rs::Query;
6use std::fmt::Debug;
7
8pub trait CollectionId {
9    fn to_string_id(self) -> String;
10}
11
12impl CollectionId for &str {
13    fn to_string_id(self) -> String {
14        String::from(self)
15    }
16}
17
18impl CollectionId for &[&str] {
19    fn to_string_id(self) -> String {
20        self.join(":")
21    }
22}
23
24#[async_trait]
25pub trait Collection<T>
26where
27    Self: RedisOps + Clone + Debug + Default,
28    T: CollectionId + fmt::Debug + Sync + Send + Copy,
29{
30    /// Retrieves records by their IDs, first attempting to fetch them from a cache (e.g., Redis),
31    /// and then querying a graph database (e.g., Neo4j) if necessary.
32    ///
33    /// # Arguments
34    ///
35    /// * `ids` - A slice of id slices representing the IDs to query.
36    ///
37    /// # Returns
38    ///
39    /// This function returns a `Result` containing a vector of `Option<Self>`. Each `Option` corresponds to
40    /// a queried ID, containing `Some(record)` if the record was found in either the cache or the graph database,
41    /// or `None` if it was not found in either.
42    async fn get_by_ids(ids: &[T]) -> Result<Vec<Option<Self>>, DynError> {
43        let key_parts_list: Vec<String> = ids.iter().map(|id| id.to_string_id()).collect();
44
45        let keys_refs: Vec<Vec<&str>> = key_parts_list.iter().map(|id| vec![id.as_str()]).collect();
46
47        let keys: Vec<&[&str]> = keys_refs.iter().map(|arr| &arr[..]).collect();
48
49        let mut collection = Self::get_from_index(keys).await?;
50
51        let mut missing_ids: Vec<(usize, T)> = Vec::new();
52        for (i, details) in collection.iter().enumerate() {
53            if details.is_none() {
54                missing_ids.push((i, ids[i]));
55            }
56        }
57
58        if !missing_ids.is_empty() {
59            let flat_missing_ids: Vec<T> = missing_ids.iter().map(|&(_, id)| id).collect();
60            let fetched_details = Self::get_from_graph(&flat_missing_ids).await?;
61
62            if !fetched_details.is_empty() {
63                for (i, (original_index, _)) in missing_ids.iter().enumerate() {
64                    collection[*original_index].clone_from(&fetched_details[i]);
65                }
66                Self::put_to_index(&flat_missing_ids, fetched_details).await?;
67            }
68        }
69
70        Ok(collection)
71    }
72
73    /// Queries a Neo4j graph database to retrieve records based on the provided IDs and collection type.
74    ///
75    /// # Arguments
76    ///
77    /// * `ids` - A slice of string slices representing the IDs.
78    ///
79    /// # Returns
80    ///
81    /// This function returns a `Result` containing a vector of `Option<Self>`. Each `Option` corresponds to
82    /// a queried ID, containing `Some(record)` if the record was found in the graph database, or `None` if it was not found.
83    async fn get_from_graph(ids: &[T]) -> Result<Vec<Option<Self>>, DynError> {
84        let mut result;
85        {
86            let graph = get_neo4j_graph()?;
87            let query = Self::collection_details_graph_query(ids);
88
89            let graph = graph.lock().await;
90            result = graph.execute(query).await?;
91        }
92
93        let mut records = Vec::with_capacity(ids.len());
94
95        while let Some(row) = result.next().await? {
96            let record: Option<Self> = row.get("record").ok();
97            records.push(record);
98        }
99        Ok(records)
100    }
101
102    async fn get_from_index(keys: Vec<&[&str]>) -> Result<Vec<Option<Self>>, DynError> {
103        Self::try_from_index_multiple_json(&keys).await
104    }
105
106    /// Indexes collection of records in Redis for faster access in future queries.
107    ///
108    /// # Arguments
109    ///
110    /// * `ids` - A slice of id slices representing the IDs of the records to index.
111    /// * `records` - A vector of `Option<Self>` containing the records to be indexed.
112    ///   Each `Option` corresponds to an ID.
113    ///
114    /// # Returns
115    ///
116    /// This function returns a `Result` indicating success or failure. A successful result indicates that the
117    /// records were successfully indexed in the cache.
118    async fn put_to_index(ids: &[T], records: Vec<Option<Self>>) -> Result<(), DynError> {
119        let mut found_records = Vec::with_capacity(records.len());
120        let mut found_record_ids = Vec::with_capacity(records.len());
121
122        for (detail, id) in records.iter().zip(ids.iter()) {
123            if let Some(value) = detail {
124                found_records.push(Some(value.clone()));
125                found_record_ids.push(*id);
126            }
127        }
128        let key_parts_list: Vec<String> = found_record_ids
129            .iter()
130            .map(|id| id.to_string_id())
131            .collect();
132
133        let keys_refs: Vec<Vec<&str>> = key_parts_list.iter().map(|id| vec![id.as_str()]).collect();
134
135        let keys: Vec<&[&str]> = keys_refs.iter().map(|arr| &arr[..]).collect();
136
137        Self::put_multiple_json_indexes(&keys, found_records).await?;
138        Self::extend_on_index_miss(&records).await?;
139        Ok(())
140    }
141
142    // Save new graph node
143    async fn put_to_graph(&self) -> Result<(), DynError> {
144        exec_single_row(self.put_graph_query()?).await
145    }
146
147    async fn reindex(collection_ids: &[T]) -> Result<(), DynError> {
148        match Self::get_from_graph(collection_ids).await {
149            Ok(collection_details_list) => {
150                if !collection_details_list.is_empty() {
151                    Self::put_to_index(collection_ids, collection_details_list).await?;
152                }
153            }
154            Err(e) => tracing::error!("Error: Could not find any element of the collection: {}", e),
155        }
156        Ok(())
157    }
158
159    /// Returns the neo4j query to return a list records by passing a list of ids.
160    /// The query should return each record in the "record" attribute of the node.
161    fn collection_details_graph_query(id_list: &[T]) -> Query;
162
163    /// Returns the neo4j query to put a record into the graph.
164    fn put_graph_query(&self) -> Result<Query, DynError>;
165
166    async fn extend_on_index_miss(elements: &[std::option::Option<Self>]) -> Result<(), DynError>;
167}