//! Create, update and delete for the contact row itself. //! //! Sub-collection mutations live in `subcollections`; reads live in `query`. use goingson_core::{Contact, ContactId, CoreError, NewContact, Result, UpdateContact, UserId}; use rusqlite::{Connection, params, params_from_iter}; use crate::utils::{bind_placeholders, escape_like, execute, format_datetime, format_datetime_now}; use super::query; pub(super) fn create(conn: &Connection, user_id: UserId, contact: &NewContact) -> Result { let id = ContactId::new(); let now = format_datetime_now(); let tags_json = serde_json::to_string(&contact.tags).unwrap_or_else(|_| "[]".to_string()); let birthday_str = contact.birthday.map(|d| d.format("%Y-%m-%d").to_string()); execute( conn, r" INSERT INTO contacts (id, user_id, display_name, nickname, company, title, notes, tags, birthday, timezone, is_implicit, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ", params![ id.to_string(), user_id.to_string(), &contact.display_name, &contact.nickname, &contact.company, &contact.title, &contact.notes, &tags_json, &birthday_str, &contact.timezone, i32::from(contact.is_implicit), &now, &now ], )?; query::get_by_id(conn, id, user_id)? .ok_or_else(|| CoreError::internal("Failed to retrieve created contact")) } pub(super) fn restore(conn: &Connection, user_id: UserId, contact: &Contact) -> Result<()> { let tags_json = serde_json::to_string(&contact.tags).unwrap_or_else(|_| "[]".to_string()); let birthday_str = contact.birthday.map(|d| d.format("%Y-%m-%d").to_string()); execute( conn, r" INSERT OR IGNORE INTO contacts (id, user_id, display_name, nickname, company, title, notes, tags, birthday, timezone, external_source, external_id, is_implicit, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ", params![ contact.id.to_string(), user_id.to_string(), &contact.display_name, &contact.nickname, &contact.company, &contact.title, &contact.notes, &tags_json, &birthday_str, &contact.timezone, &contact.external_source, &contact.external_id, i32::from(contact.is_implicit), format_datetime(&contact.created_at), format_datetime(&contact.updated_at) ], )?; Ok(()) } pub(super) fn update( conn: &Connection, id: ContactId, user_id: UserId, contact: &UpdateContact, ) -> Result> { let now = format_datetime_now(); let tags_json = serde_json::to_string(&contact.tags).unwrap_or_else(|_| "[]".to_string()); let birthday_str = contact.birthday.map(|d| d.format("%Y-%m-%d").to_string()); let result = execute( conn, r" UPDATE contacts SET display_name = ?, nickname = ?, company = ?, title = ?, notes = ?, tags = ?, birthday = ?, timezone = ?, updated_at = ? WHERE id = ? AND user_id = ? ", params![ &contact.display_name, &contact.nickname, &contact.company, &contact.title, &contact.notes, &tags_json, &birthday_str, &contact.timezone, &now, id.to_string(), user_id.to_string() ], )?; if result > 0 { query::get_by_id(conn, id, user_id) } else { Ok(None) } } pub(super) fn delete(conn: &Connection, id: ContactId, user_id: UserId) -> Result { let result = execute( conn, "DELETE FROM contacts WHERE id = ? AND user_id = ?", params![id.to_string(), user_id.to_string()], )?; Ok(result > 0) } pub(super) fn set_external_ref( conn: &Connection, id: ContactId, user_id: UserId, source: &str, external_id: &str, ) -> Result<()> { execute( conn, "UPDATE contacts SET external_source = ?, external_id = ? WHERE id = ? AND user_id = ?", params![source, external_id, id.to_string(), user_id.to_string()], )?; Ok(()) } pub(super) fn delete_many(conn: &Connection, ids: &[ContactId], user_id: UserId) -> Result { if ids.is_empty() { return Ok(0); } let user_id_str = user_id.to_string(); let placeholders = bind_placeholders(ids.len()); let sql = format!("DELETE FROM contacts WHERE user_id = ? AND id IN ({placeholders})"); let mut binds: Vec = Vec::with_capacity(ids.len() + 1); binds.push(user_id_str); binds.extend(ids.iter().map(std::string::ToString::to_string)); let result = execute(conn, &sql, params_from_iter(binds))?; Ok(result as u64) } pub(super) fn tag_many( conn: &Connection, ids: &[ContactId], user_id: UserId, tag: &str, ) -> Result { if ids.is_empty() || tag.is_empty() { return Ok(0); } let user_id_str = user_id.to_string(); let like_pattern = format!("%\"{}\"%", escape_like(tag)); let placeholders = bind_placeholders(ids.len()); // Append tag to JSON array where not already present. let sql = format!( r"UPDATE contacts SET tags = CASE WHEN tags IS NULL OR tags = '' OR tags = '[]' THEN json_array(?) ELSE json_insert(tags, '$[#]', ?) END, updated_at = datetime('now') WHERE user_id = ? AND id IN ({placeholders}) AND (tags IS NULL OR tags NOT LIKE ? ESCAPE '\')", ); let mut binds: Vec = Vec::with_capacity(ids.len() + 4); binds.push(tag.to_string()); binds.push(tag.to_string()); binds.push(user_id_str); binds.extend(ids.iter().map(std::string::ToString::to_string)); binds.push(like_pattern); let result = execute(conn, &sql, params_from_iter(binds))?; Ok(result as u64) }