Skip to main content

nexus_common/models/user/
counts.rs

1use crate::db::kv::JsonAction;
2use crate::db::{get_neo4j_graph, queries, RedisOps};
3use crate::models::tag::user::USER_TAGS_KEY_PARTS;
4use crate::types::DynError;
5use serde::{Deserialize, Serialize};
6use utoipa::ToSchema;
7
8use super::UserStream;
9
10/// Represents total counts of relationships of a user.
11#[derive(Serialize, Deserialize, ToSchema, Debug, Default)]
12pub struct UserCounts {
13    // The number of tags assigned to other entities by the user (e.g. user, posts)
14    pub tagged: u32,
15    // User received tags counts
16    pub tags: u32,
17    // Distinct tags where the user was referenced
18    pub unique_tags: u32,
19    pub posts: u32,
20    pub replies: u32,
21    pub following: u32,
22    pub followers: u32,
23    pub friends: u32,
24    pub bookmarks: u32,
25}
26
27impl RedisOps for UserCounts {}
28
29impl UserCounts {
30    /// Retrieves counts by user ID, first trying to get from Redis, then from Neo4j if not found.
31    pub async fn get_by_id(user_id: &str) -> Result<Option<UserCounts>, DynError> {
32        match Self::get_from_index(user_id).await? {
33            Some(counts) => Ok(Some(counts)),
34            None => {
35                let graph_response = Self::get_from_graph(user_id).await?;
36                if let Some(user_counts) = graph_response {
37                    user_counts.put_to_index(user_id).await?;
38                    return Ok(Some(user_counts));
39                }
40                Ok(None)
41            }
42        }
43    }
44
45    /// Retrieves the counts from Neo4j.
46    pub async fn get_from_graph(user_id: &str) -> Result<Option<UserCounts>, DynError> {
47        let mut result;
48        {
49            let graph = get_neo4j_graph()?;
50            let query = queries::get::user_counts(user_id);
51
52            let graph = graph.lock().await;
53            result = graph.execute(query).await?;
54        }
55
56        if let Some(row) = result.next().await? {
57            let user_exists: bool = row.get("exists").unwrap_or(false);
58            if user_exists {
59                match row.get("counts") {
60                    Ok(user_counts) => return Ok(Some(user_counts)),
61                    // Like this we give a chance, in the next request to populate index
62                    // If we populate the cache with default value, from that point we will have
63                    // inconsistent state
64                    Err(_e) => return Ok(None),
65                }
66            }
67        }
68        Ok(None)
69    }
70
71    pub async fn get_from_index(user_id: &str) -> Result<Option<UserCounts>, DynError> {
72        if let Some(user_counts) = Self::try_from_index_json(&[user_id], None).await? {
73            return Ok(Some(user_counts));
74        }
75        Ok(None)
76    }
77
78    pub async fn put_to_index(&self, user_id: &str) -> Result<(), DynError> {
79        self.put_index_json(&[user_id], None, None).await?;
80        UserStream::add_to_most_followed_sorted_set(user_id, self).await?;
81        UserStream::add_to_influencers_sorted_set(user_id, self).await?;
82        Ok(())
83    }
84
85    pub async fn update_index_field(
86        author_id: &str,
87        field: &str,
88        action: JsonAction,
89    ) -> Result<(), DynError> {
90        Self::modify_json_field(&[author_id], field, action).await?;
91        Ok(())
92    }
93
94    /// Updates a user's counts index field and conditionally updates ranking sets
95    /// based on follower, tag, or post counts.
96    ///
97    /// # Arguments
98    ///
99    /// * `user_id` - The unique identifier of the user whose index field is being updated.
100    /// * `field` - The name of the user-related field to update (e.g., `"followers"`, `"tags"`, `"posts"`).
101    /// * `action` - The action to perform on the field (increment or decrement).
102    /// * `tag_label` - An optional tag label used to check membership in the user's tag-related sorted set. Important if we want to update the unique_tags field
103    ///
104    /// # Behavior
105    ///
106    /// - Conditional Update Based on `tag_label`
107    /// - Update User Counts Index
108    /// - Update Ranking Sets for Specific Fields
109    pub async fn update(
110        user_id: &str,
111        field: &str,
112        action: JsonAction,
113        tag_label: Option<&str>,
114    ) -> Result<(), DynError> {
115        // This condition applies only when updating `unique_tags`
116        if let Some(label) = tag_label {
117            let index_parts = [&USER_TAGS_KEY_PARTS[..], &[user_id]].concat();
118            let score = Self::check_sorted_set_member(None, &index_parts, &[label]).await?;
119            match (score, &action) {
120                // If tag value is less than 1, `unique_tags` can be incremented or decremented
121                (Some(tag_value), _) if tag_value < 1 => (),
122
123                // Incrementing `unique_tags` is also allowed when the tag value doesn't exist yet in the sorted set
124                (None, JsonAction::Increment(_)) => (),
125
126                // Do not update the index
127                _ => return Ok(()),
128            }
129        }
130        // Update user counts index
131        Self::update_index_field(user_id, field, action).await?;
132        // Just update influencer and most followed indexes, when that fields are updated
133        if field == "followers" || field == "tags" || field == "posts" {
134            let exist_count = Self::get_by_id(user_id).await?;
135            if let Some(user_counts) = exist_count {
136                UserStream::add_to_influencers_sorted_set(user_id, &user_counts).await?;
137                // Increment followers
138                if field == "followers" {
139                    UserStream::add_to_most_followed_sorted_set(user_id, &user_counts).await?
140                }
141            }
142        }
143        Ok(())
144    }
145
146    pub async fn reindex(author_id: &str) -> Result<(), DynError> {
147        match Self::get_from_graph(author_id).await? {
148            Some(counts) => counts.put_to_index(author_id).await?,
149            None => tracing::error!("{}: Could not found user counts in the graph", author_id),
150        }
151        Ok(())
152    }
153
154    pub async fn delete(user_id: &str) -> Result<(), DynError> {
155        // Delete user_details on Redis
156        Self::remove_from_index_multiple_json(&[&[user_id]]).await?;
157
158        Ok(())
159    }
160}