cal-redis 0.1.80

Callable Redis Implementation
Documentation
// File: cal-redis/src/cache/contact.rs

use super::CallableCache;
use crate::common::{deserialize_from_json, get_items, serialize_to_json};
use crate::constants::AccountKeys;
use cal_core::contact::Contact;
use redis::{AsyncCommands, RedisError};
use std::sync::Arc;

impl CallableCache {
    /// Inserts a contact into Redis.
    ///
    /// # Arguments
    /// * `contact` - The contact to insert
    ///
    /// # Returns
    /// * `Result<(), RedisError>` - Ok if successful, or a Redis error
    pub async fn insert_contact(&self, contact: Contact) -> Result<(), RedisError> {
        println!(
            "[CallableCache::insert_contact] Inserting contact: {} for account: {}",
            contact.id, contact.account.id
        );

        let mut con = self.redis_connection();
        let hash_key = AccountKeys::contacts(&contact.account.id);
        let value = serialize_to_json(&contact)?;

        // Store in account-scoped contacts hash
        con.hset(&hash_key, &contact.id, value).await?;

        // Also add to contact identifiers for lookup by phone numbers
        let idents_key = AccountKeys::contact_idents(&contact.account.id);

        // Map primary phone to contact ID
        con.hset(&idents_key, &contact.primary, &contact.id).await?;

        // Map secondary phone if exists
        if let Some(ref secondary) = contact.secondary {
            con.hset(&idents_key, secondary, &contact.id).await?;
        }

        // Map tertiary phone if exists
        if let Some(ref tertiary) = contact.tertiary {
            con.hset(&idents_key, tertiary, &contact.id).await?;
        }

        // Map email if exists
        if let Some(ref email) = contact.email {
            con.hset(&idents_key, email, &contact.id).await?;
        }

        println!("[CallableCache::insert_contact] Successfully inserted contact");
        Ok(())
    }

    /// Retrieves a contact by ID.
    ///
    /// # Arguments
    /// * `account_id` - The account ID
    /// * `contact_id` - The contact ID
    ///
    /// # Returns
    /// * `Result<Option<Arc<Contact>>, RedisError>` - The contact if found
    pub async fn get_contact_by_id(
        &self,
        account_id: &str,
        contact_id: &str,
    ) -> Result<Option<Arc<Contact>>, RedisError> {
        println!(
            "[CallableCache::get_contact_by_id] Getting contact {} for account: {}",
            contact_id, account_id
        );

        let con = self.redis_connection();
        let hash_key = AccountKeys::contacts(account_id);

        let value: Option<String> = con.clone().hget(&hash_key, contact_id).await?;

        match value {
            Some(json_str) => {
                let contact: Contact = deserialize_from_json(&json_str)?;
                println!("[CallableCache::get_contact_by_id] Found contact: {}", contact.name);
                Ok(Some(Arc::new(contact)))
            }
            None => {
                println!("[CallableCache::get_contact_by_id] Contact not found");
                Ok(None)
            }
        }
    }

    /// Retrieves a contact by identifier (phone number or email).
    ///
    /// # Arguments
    /// * `account_id` - The account ID
    /// * `identifier` - The phone number or email
    ///
    /// # Returns
    /// * `Result<Option<Arc<Contact>>, RedisError>` - The contact if found
    pub async fn get_contact_by_ident(
        &self,
        account_id: &str,
        identifier: &str,
    ) -> Result<Option<Arc<Contact>>, RedisError> {
        println!(
            "[CallableCache::get_contact_by_ident] Getting contact by identifier: {} for account: {}",
            identifier, account_id
        );

        let con = self.redis_connection();
        let idents_key = AccountKeys::contact_idents(account_id);

        // First get the contact ID from the identifier
        let contact_id: Option<String> = con.clone().hget(&idents_key, identifier).await?;

        match contact_id {
            Some(id) => {
                // Now get the actual contact
                self.get_contact_by_id(account_id, &id).await
            }
            None => {
                println!("[CallableCache::get_contact_by_ident] No contact found for identifier");
                Ok(None)
            }
        }
    }

    /// Retrieves all contacts for an account.
    ///
    /// # Arguments
    /// * `account_id` - The account ID
    ///
    /// # Returns
    /// * `Result<Vec<Arc<Contact>>, RedisError>` - List of contacts
    pub async fn get_contacts_by_account(
        &self,
        account_id: &str,
    ) -> Result<Vec<Arc<Contact>>, RedisError> {
        println!(
            "[CallableCache::get_contacts_by_account] Getting all contacts for account: {}",
            account_id
        );

        let con = self.redis_connection();
        let hash_key = AccountKeys::contacts(account_id);

        let contacts: Vec<Contact> = get_items(con, &hash_key).await?;
        let arc_contacts: Vec<Arc<Contact>> = contacts.into_iter().map(Arc::new).collect();

        println!(
            "[CallableCache::get_contacts_by_account] Found {} contacts",
            arc_contacts.len()
        );
        Ok(arc_contacts)
    }

    /// Retrieves contacts by group.
    ///
    /// # Arguments
    /// * `account_id` - The account ID
    /// * `group` - The group name
    ///
    /// # Returns
    /// * `Result<Vec<Arc<Contact>>, RedisError>` - List of contacts in the group
    pub async fn get_contacts_by_group(
        &self,
        account_id: &str,
        group: &str,
    ) -> Result<Vec<Arc<Contact>>, RedisError> {
        println!(
            "[CallableCache::get_contacts_by_group] Getting contacts for group: {} in account: {}",
            group, account_id
        );

        let all_contacts = self.get_contacts_by_account(account_id).await?;

        let filtered: Vec<Arc<Contact>> = all_contacts
            .into_iter()
            .filter(|contact| contact.is_in_group(group))
            .collect();

        println!(
            "[CallableCache::get_contacts_by_group] Found {} contacts in group",
            filtered.len()
        );
        Ok(filtered)
    }

    /// Deletes a contact from Redis.
    ///
    /// # Arguments
    /// * `account_id` - The account ID
    /// * `contact_id` - The contact ID
    ///
    /// # Returns
    /// * `Result<bool, RedisError>` - true if deleted, false if not found
    pub async fn delete_contact(
        &self,
        account_id: &str,
        contact_id: &str,
    ) -> Result<bool, RedisError> {
        println!(
            "[CallableCache::delete_contact] Deleting contact {} from account: {}",
            contact_id, account_id
        );

        // First get the contact to remove its identifiers
        if let Some(contact) = self.get_contact_by_id(account_id, contact_id).await? {
            let mut con = self.redis_connection();
            let idents_key = AccountKeys::contact_idents(account_id);

            // Remove all identifiers
            con.hdel(&idents_key, &contact.primary).await?;

            if let Some(ref secondary) = contact.secondary {
                con.hdel(&idents_key, secondary).await?;
            }

            if let Some(ref tertiary) = contact.tertiary {
                con.hdel(&idents_key, tertiary).await?;
            }

            if let Some(ref email) = contact.email {
                con.hdel(&idents_key, email).await?;
            }

            // Remove the contact itself
            let hash_key = AccountKeys::contacts(account_id);
            let deleted: bool = con.hdel(&hash_key, contact_id).await?;

            if deleted {
                println!("[CallableCache::delete_contact] Successfully deleted contact");
            }

            Ok(deleted)
        } else {
            println!("[CallableCache::delete_contact] Contact not found");
            Ok(false)
        }
    }

    /// Batch insert multiple contacts.
    ///
    /// # Arguments
    /// * `contacts` - Vector of contacts to insert
    ///
    /// # Returns
    /// * `Result<(), RedisError>` - Ok if successful, or a Redis error
    pub async fn insert_contacts(&self, contacts: Vec<Contact>) -> Result<(), RedisError> {
        println!(
            "[CallableCache::insert_contacts] Inserting {} contacts",
            contacts.len()
        );

        if contacts.is_empty() {
            return Ok(());
        }

        let mut con = self.redis_connection();
        let mut pipe = redis::pipe();

        // Group contacts by account
        let mut contacts_by_account: std::collections::HashMap<String, Vec<Contact>> = std::collections::HashMap::new();

        for contact in contacts {
            contacts_by_account
                .entry(contact.account.id.clone())
                .or_insert_with(Vec::new)
                .push(contact);
        }

        // Process each account's contacts
        for (account_id, account_contacts) in contacts_by_account {
            let hash_key = AccountKeys::contacts(&account_id);
            let idents_key = AccountKeys::contact_idents(&account_id);

            for contact in account_contacts {
                let value = serialize_to_json(&contact)?;
                pipe.hset(&hash_key, &contact.id, &value);

                // Add identifiers
                pipe.hset(&idents_key, &contact.primary, &contact.id);

                if let Some(ref secondary) = contact.secondary {
                    pipe.hset(&idents_key, secondary, &contact.id);
                }

                if let Some(ref tertiary) = contact.tertiary {
                    pipe.hset(&idents_key, tertiary, &contact.id);
                }

                if let Some(ref email) = contact.email {
                    pipe.hset(&idents_key, email, &contact.id);
                }
            }
        }

        pipe.query_async(&mut con).await?;

        println!("[CallableCache::insert_contacts] Successfully inserted all contacts");
        Ok(())
    }
}