use cgcx_core::{AdminAction, Content, ContentFile, ContentId, ContentStatus, ForwardDefinition, ForwardSubmission, Punishment, Report, ReportStatus, Result, CgcxError, User}; use rusqlite::{params, OptionalExtension, Connection}; use std::sync::Arc; use tokio::sync::Mutex; pub struct UserRepo { conn: Arc>, } impl UserRepo { pub fn new(conn: Arc>) -> Self { Self { conn } } pub async fn ensure_exists(&self, id: i64, username: Option<&str>, first_name: &str) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "INSERT INTO users (id, telegram_username, first_name) VALUES (?1, ?2, ?3) ON CONFLICT(id) DO UPDATE SET telegram_username=excluded.telegram_username, first_name=excluded.first_name", params![id, username, first_name], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn get(&self, id: i64) -> Result> { let conn = self.conn.lock().await; let row = conn.query_row( "SELECT id, telegram_username, first_name, role, accepted_terms_at, created_at FROM users WHERE id = ?1", params![id], |row| { let role: String = row.get(3)?; Ok(User { id: row.get(0)?, telegram_username: row.get(1)?, first_name: row.get(2)?, role: match role.as_str() { "admin" => cgcx_core::UserRole::Admin, "banned" => cgcx_core::UserRole::Banned, _ => cgcx_core::UserRole::User, }, accepted_terms_at: row.get(4)?, created_at: row.get(5)?, }) }, ).optional().map_err(|e| CgcxError::Database(e.to_string()))?; Ok(row) } pub async fn set_role(&self, id: i64, role: &str) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "UPDATE users SET role = ?1 WHERE id = ?2", params![role, id], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn set_accepted_terms(&self, id: i64) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "UPDATE users SET accepted_terms_at = datetime('now') WHERE id = ?1", params![id], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } } pub struct ContentRepo { conn: Arc>, } impl ContentRepo { pub fn new(conn: Arc>) -> Self { Self { conn } } pub async fn insert(&self, content: &Content) -> Result<()> { let conn = self.conn.lock().await; let status = format!("{:?}", content.status).to_lowercase(); conn.execute( "INSERT INTO contents (id, user_id, status, view_count, max_views, allow_download, password_hash, created_at, deleted_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) ON CONFLICT(id) DO NOTHING", params![ content.id.as_str(), content.user_id, status, content.view_count as i64, content.max_views.map(|v| v as i64), content.allow_download as i64, content.password_hash.as_ref(), content.created_at, content.deleted_at, ], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn get(&self, id: &ContentId) -> Result> { let conn = self.conn.lock().await; let row = conn.query_row( "SELECT id, user_id, status, view_count, max_views, allow_download, password_hash, created_at, deleted_at FROM contents WHERE id = ?1", params![id.as_str()], |row| { let status: String = row.get(2)?; Ok(Content { id: ContentId::new_unchecked(row.get(0)?), user_id: row.get(1)?, status: match status.as_str() { "staged" => ContentStatus::Staged, "deleted" => ContentStatus::Deleted, "blacklisted" => ContentStatus::Blacklisted, _ => ContentStatus::Active, }, view_count: row.get::<_, i64>(3)? as u64, max_views: row.get::<_, Option>(4)?.map(|v| v as u64), allow_download: row.get::<_, i64>(5)? != 0, password_hash: row.get(6)?, created_at: row.get(7)?, deleted_at: row.get(8)?, }) }, ).optional().map_err(|e| CgcxError::Database(e.to_string()))?; Ok(row) } pub async fn list_by_user(&self, user_id: i64, limit: usize, offset: usize) -> Result> { let conn = self.conn.lock().await; let mut stmt = conn.prepare( "SELECT id, user_id, status, view_count, max_views, allow_download, password_hash, created_at, deleted_at FROM contents WHERE user_id = ?1 AND status != 'deleted' ORDER BY created_at DESC LIMIT ?2 OFFSET ?3" ).map_err(|e| CgcxError::Database(e.to_string()))?; let rows = stmt.query_map(params![user_id, limit as i64, offset as i64], |row| { let status: String = row.get(2)?; Ok(Content { id: ContentId::new_unchecked(row.get(0)?), user_id: row.get(1)?, status: match status.as_str() { "staged" => ContentStatus::Staged, "deleted" => ContentStatus::Deleted, "blacklisted" => ContentStatus::Blacklisted, _ => ContentStatus::Active, }, view_count: row.get::<_, i64>(3)? as u64, max_views: row.get::<_, Option>(4)?.map(|v| v as u64), allow_download: row.get::<_, i64>(5)? != 0, password_hash: row.get(6)?, created_at: row.get(7)?, deleted_at: row.get(8)?, }) }).map_err(|e| CgcxError::Database(e.to_string()))?; let mut out = Vec::new(); for r in rows { out.push(r.map_err(|e| CgcxError::Database(e.to_string()))?); } Ok(out) } pub async fn count_by_user(&self, user_id: i64) -> Result { let conn = self.conn.lock().await; let count: i64 = conn.query_row( "SELECT COUNT(*) FROM contents WHERE user_id = ?1 AND status != 'deleted'", params![user_id], |row| row.get(0), ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(count as usize) } pub async fn increment_views(&self, id: &ContentId) -> Result { let conn = self.conn.lock().await; let new: i64 = conn.query_row( "UPDATE contents SET view_count = view_count + 1 WHERE id = ?1 RETURNING view_count", params![id.as_str()], |row| row.get(0), ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(new as u64) } pub async fn set_status(&self, id: &ContentId, status: ContentStatus) -> Result<()> { let conn = self.conn.lock().await; let s = format!("{:?}", status).to_lowercase(); conn.execute( "UPDATE contents SET status = ?1, deleted_at = CASE WHEN ?1 IN ('deleted','blacklisted') THEN datetime('now') ELSE NULL END WHERE id = ?2", params![s, id.as_str()], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn delete_permanent(&self, id: &ContentId) -> Result<()> { let mut conn = self.conn.lock().await; let tx = conn.transaction().map_err(|e| CgcxError::Database(e.to_string()))?; tx.execute("DELETE FROM content_files WHERE content_id = ?1", params![id.as_str()]) .map_err(|e| CgcxError::Database(e.to_string()))?; tx.execute("DELETE FROM contents WHERE id = ?1", params![id.as_str()]) .map_err(|e| CgcxError::Database(e.to_string()))?; tx.commit().map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn update_password_hash(&self, id: &ContentId, password_hash: Option<&str>) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "UPDATE contents SET password_hash = ?1 WHERE id = ?2", params![password_hash, id.as_str()], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } } pub struct ContentFileRepo { conn: Arc>, } impl ContentFileRepo { pub fn new(conn: Arc>) -> Self { Self { conn } } pub async fn insert(&self, file: &ContentFile) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "INSERT INTO content_files (content_id, file_index, original_name, stored_path, mime_type, size_bytes, ciphertext_size_bytes, encrypted_key_wrapped, encrypted_hash, render_flags, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) ON CONFLICT(content_id, file_index) DO NOTHING", params![ file.content_id.as_str(), file.file_index as i64, &file.original_name, file.stored_path.to_str(), &file.mime_type, file.size_bytes as i64, file.ciphertext_size_bytes as i64, &file.encrypted_key_wrapped, &file.encrypted_hash, file.render_flags as i64, file.created_at, ], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn list_by_content(&self, content_id: &ContentId) -> Result> { let conn = self.conn.lock().await; let mut stmt = conn.prepare( "SELECT content_id, file_index, original_name, stored_path, mime_type, size_bytes, ciphertext_size_bytes, encrypted_key_wrapped, encrypted_hash, render_flags, created_at FROM content_files WHERE content_id = ?1 ORDER BY file_index" ).map_err(|e| CgcxError::Database(e.to_string()))?; let rows = stmt.query_map(params![content_id.as_str()], |row| { Ok(ContentFile { content_id: ContentId::new_unchecked(row.get(0)?), file_index: row.get::<_, i64>(1)? as u32, original_name: row.get(2)?, stored_path: std::path::PathBuf::from(row.get::<_, String>(3)?), mime_type: row.get(4)?, size_bytes: row.get::<_, i64>(5)? as u64, ciphertext_size_bytes: row.get::<_, i64>(6)? as u64, encrypted_key_wrapped: row.get(7)?, encrypted_hash: row.get(8)?, render_flags: row.get::<_, i64>(9)? as u32, created_at: row.get(10)?, }) }).map_err(|e| CgcxError::Database(e.to_string()))?; let mut out = Vec::new(); for r in rows { out.push(r.map_err(|e| CgcxError::Database(e.to_string()))?); } Ok(out) } pub async fn find_orphan_files(&self) -> Result> { let conn = self.conn.lock().await; let mut stmt = conn.prepare( "SELECT cf.stored_path FROM content_files cf JOIN contents c ON c.id = cf.content_id WHERE c.status IN ('deleted', 'blacklisted')" ).map_err(|e| CgcxError::Database(e.to_string()))?; let rows = stmt.query_map([], |row| { row.get::<_, String>(0) }).map_err(|e| CgcxError::Database(e.to_string()))?; let mut out = Vec::new(); for r in rows { out.push(r.map_err(|e| CgcxError::Database(e.to_string()))?); } Ok(out) } } pub struct ReportRepo { conn: Arc>, } impl ReportRepo { pub fn new(conn: Arc>) -> Self { Self { conn } } pub async fn insert(&self, content_id: &ContentId, reporter_user_id: i64, reason: &str) -> Result { let conn = self.conn.lock().await; conn.execute( "INSERT INTO reports (content_id, reporter_user_id, reason) VALUES (?1, ?2, ?3)", params![content_id.as_str(), reporter_user_id, reason], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(conn.last_insert_rowid()) } pub async fn get(&self, id: i64) -> Result> { let conn = self.conn.lock().await; let row = conn.query_row( "SELECT id, content_id, reporter_user_id, reason, status, created_at, resolved_at, resolver_id FROM reports WHERE id = ?1", params![id], |row| { let status: String = row.get(4)?; Ok(Report { id: row.get(0)?, content_id: ContentId::new_unchecked(row.get(1)?), reporter_user_id: row.get(2)?, reason: row.get(3)?, status: match status.as_str() { "dismissed" => ReportStatus::Dismissed, "actioned" => ReportStatus::Actioned, _ => ReportStatus::Open, }, created_at: row.get(5)?, resolved_at: row.get(6)?, resolver_id: row.get(7)?, }) }, ).optional().map_err(|e| CgcxError::Database(e.to_string()))?; Ok(row) } pub async fn list(&self, limit: usize, offset: usize) -> Result> { let conn = self.conn.lock().await; let mut stmt = conn.prepare( "SELECT id, content_id, reporter_user_id, reason, status, created_at, resolved_at, resolver_id FROM reports ORDER BY created_at DESC LIMIT ?1 OFFSET ?2" ).map_err(|e| CgcxError::Database(e.to_string()))?; let rows = stmt.query_map(params![limit as i64, offset as i64], |row| { let status: String = row.get(4)?; Ok(Report { id: row.get(0)?, content_id: ContentId::new_unchecked(row.get(1)?), reporter_user_id: row.get(2)?, reason: row.get(3)?, status: match status.as_str() { "dismissed" => ReportStatus::Dismissed, "actioned" => ReportStatus::Actioned, _ => ReportStatus::Open, }, created_at: row.get(5)?, resolved_at: row.get(6)?, resolver_id: row.get(7)?, }) }).map_err(|e| CgcxError::Database(e.to_string()))?; let mut out = Vec::new(); for r in rows { out.push(r.map_err(|e| CgcxError::Database(e.to_string()))?); } Ok(out) } pub async fn resolve(&self, id: i64, status: ReportStatus, resolver_id: i64) -> Result<()> { let conn = self.conn.lock().await; let s = format!("{:?}", status).to_lowercase(); conn.execute( "UPDATE reports SET status = ?1, resolver_id = ?2, resolved_at = datetime('now') WHERE id = ?3", params![s, resolver_id, id], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } } pub struct AdminActionRepo { conn: Arc>, } impl AdminActionRepo { pub fn new(conn: Arc>) -> Self { Self { conn } } pub async fn insert(&self, action: &AdminAction) -> Result { let conn = self.conn.lock().await; conn.execute( "INSERT INTO admin_actions (admin_user_id, target_type, target_id, action) VALUES (?1, ?2, ?3, ?4)", params![ action.admin_user_id, &action.target_type, &action.target_id, &action.action, ], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(conn.last_insert_rowid()) } } pub struct ForwardRepo { conn: Arc>, } impl ForwardRepo { pub fn new(conn: Arc>) -> Self { Self { conn } } pub async fn insert(&self, creator_user_id: i64, source_chat_id: i64, destination_chat_id: i64, review_group_id: i64, forward_message: &str, code: &str, share_mode: &str) -> Result { let conn = self.conn.lock().await; conn.execute( "INSERT INTO forward_definitions (creator_user_id, source_chat_id, destination_chat_id, review_group_id, forward_message, code, share_mode) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", params![creator_user_id, source_chat_id, destination_chat_id, review_group_id, forward_message, code, share_mode], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(conn.last_insert_rowid()) } pub async fn get_by_code(&self, code: &str) -> Result> { let conn = self.conn.lock().await; let row = conn.query_row( "SELECT id, creator_user_id, source_chat_id, destination_chat_id, review_group_id, forward_message, code, share_mode, revoked_at, created_at FROM forward_definitions WHERE code = ?1", params![code], |row| { Ok(ForwardDefinition { id: row.get(0)?, creator_user_id: row.get(1)?, source_chat_id: row.get(2)?, destination_chat_id: row.get(3)?, review_group_id: row.get(4)?, forward_message: row.get(5)?, code: row.get(6)?, share_mode: row.get(7)?, revoked_at: row.get(8)?, created_at: row.get(9)?, }) }, ).optional().map_err(|e| CgcxError::Database(e.to_string()))?; Ok(row) } pub async fn list_by_source_chat(&self, source_chat_id: i64, limit: usize, offset: usize) -> Result> { let conn = self.conn.lock().await; let mut stmt = conn.prepare( "SELECT id, creator_user_id, source_chat_id, destination_chat_id, review_group_id, forward_message, code, share_mode, revoked_at, created_at FROM forward_definitions WHERE source_chat_id = ?1 ORDER BY created_at DESC LIMIT ?2 OFFSET ?3" ).map_err(|e| CgcxError::Database(e.to_string()))?; let rows = stmt.query_map(params![source_chat_id, limit as i64, offset as i64], |row| { Ok(ForwardDefinition { id: row.get(0)?, creator_user_id: row.get(1)?, source_chat_id: row.get(2)?, destination_chat_id: row.get(3)?, review_group_id: row.get(4)?, forward_message: row.get(5)?, code: row.get(6)?, share_mode: row.get(7)?, revoked_at: row.get(8)?, created_at: row.get(9)?, }) }).map_err(|e| CgcxError::Database(e.to_string()))?; let mut out = Vec::new(); for r in rows { out.push(r.map_err(|e| CgcxError::Database(e.to_string()))?); } Ok(out) } pub async fn revoke(&self, id: i64) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "UPDATE forward_definitions SET revoked_at = datetime('now') WHERE id = ?1", params![id], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn is_allowed(&self, forward_id: i64, user_id: i64) -> Result { let conn = self.conn.lock().await; let def: Option = conn.query_row( "SELECT id, creator_user_id, source_chat_id, destination_chat_id, review_group_id, forward_message, code, share_mode, revoked_at, created_at FROM forward_definitions WHERE id = ?1", params![forward_id], |row| { Ok(ForwardDefinition { id: row.get(0)?, creator_user_id: row.get(1)?, source_chat_id: row.get(2)?, destination_chat_id: row.get(3)?, review_group_id: row.get(4)?, forward_message: row.get(5)?, code: row.get(6)?, share_mode: row.get(7)?, revoked_at: row.get(8)?, created_at: row.get(9)?, }) }, ).optional().map_err(|e| CgcxError::Database(e.to_string()))?; if let Some(def) = def { if def.creator_user_id == user_id { return Ok(true); } let list_entry: Option = conn.query_row( "SELECT list_type FROM forward_lists WHERE forward_id = ?1 AND user_id = ?2", params![forward_id, user_id], |row| row.get(0), ).optional().map_err(|e| CgcxError::Database(e.to_string()))?; match def.share_mode.as_str() { "w" => Ok(list_entry.map(|t| t == "allow").unwrap_or(false)), _ => Ok(list_entry.map(|t| t != "block").unwrap_or(true)), } } else { Ok(false) } } pub async fn insert_submission(&self, forward_id: i64, user_id: i64, content_id: &str) -> Result { let conn = self.conn.lock().await; conn.execute( "INSERT INTO forward_submissions (forward_id, user_id, content_id) VALUES (?1, ?2, ?3)", params![forward_id, user_id, content_id], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(conn.last_insert_rowid()) } pub async fn get_submission(&self, id: i64) -> Result> { let conn = self.conn.lock().await; let row = conn.query_row( "SELECT id, forward_id, user_id, content_id, status, review_message_id, created_at, resolved_at, resolver_id FROM forward_submissions WHERE id = ?1", params![id], |row| { Ok(ForwardSubmission { id: row.get(0)?, forward_id: row.get(1)?, user_id: row.get(2)?, content_id: ContentId::new_unchecked(row.get(3)?), status: row.get(4)?, review_message_id: row.get(5)?, created_at: row.get(6)?, resolved_at: row.get(7)?, resolver_id: row.get(8)?, }) }, ).optional().map_err(|e| CgcxError::Database(e.to_string()))?; Ok(row) } pub async fn set_review_message_id(&self, id: i64, message_id: i32) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "UPDATE forward_submissions SET review_message_id = ?1 WHERE id = ?2", params![message_id, id], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn update_status(&self, id: i64, status: &str) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "UPDATE forward_submissions SET status = ?1, resolved_at = datetime('now') WHERE id = ?2", params![status, id], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn get_definition(&self, id: i64) -> Result> { let conn = self.conn.lock().await; let row = conn.query_row( "SELECT id, creator_user_id, source_chat_id, destination_chat_id, review_group_id, forward_message, code, share_mode, revoked_at, created_at FROM forward_definitions WHERE id = ?1", params![id], |row| { Ok(ForwardDefinition { id: row.get(0)?, creator_user_id: row.get(1)?, source_chat_id: row.get(2)?, destination_chat_id: row.get(3)?, review_group_id: row.get(4)?, forward_message: row.get(5)?, code: row.get(6)?, share_mode: row.get(7)?, revoked_at: row.get(8)?, created_at: row.get(9)?, }) }, ).optional().map_err(|e| CgcxError::Database(e.to_string()))?; Ok(row) } pub async fn add_to_list(&self, forward_id: i64, user_id: i64, list_type: &str) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "INSERT INTO forward_lists (forward_id, user_id, list_type) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING", params![forward_id, user_id, list_type], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn remove_from_list(&self, forward_id: i64, user_id: i64, list_type: &str) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "DELETE FROM forward_lists WHERE forward_id = ?1 AND user_id = ?2 AND list_type = ?3", params![forward_id, user_id, list_type], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } } pub struct PunishmentRepo { conn: Arc>, } impl PunishmentRepo { pub fn new(conn: Arc>) -> Self { Self { conn } } pub async fn insert( &self, chat_id: i64, target_user_id: i64, action_type: &str, duration_seconds: Option, reason: Option<&str>, created_by: i64, ) -> Result { let conn = self.conn.lock().await; conn.execute( "INSERT INTO punishments (chat_id, target_user_id, action_type, duration_seconds, reason, created_by) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![chat_id, target_user_id, action_type, duration_seconds, reason, created_by], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(conn.last_insert_rowid()) } pub async fn get_active_for_chat_target(&self, chat_id: i64, target_user_id: i64, action_type: &str) -> Result> { let conn = self.conn.lock().await; let mut stmt = conn.prepare( "SELECT id, chat_id, target_user_id, action_type, duration_seconds, reason, created_by, created_at, revoked_at, revoked_by, active FROM punishments WHERE chat_id = ?1 AND target_user_id = ?2 AND action_type = ?3 AND active = 1" ).map_err(|e| CgcxError::Database(e.to_string()))?; let rows = stmt.query_map(params![chat_id, target_user_id, action_type], |row| { Ok(Punishment { id: row.get(0)?, chat_id: row.get(1)?, target_user_id: row.get(2)?, action_type: row.get(3)?, duration_seconds: row.get(4)?, reason: row.get(5)?, created_by: row.get(6)?, created_at: row.get(7)?, revoked_at: row.get(8)?, revoked_by: row.get(9)?, active: row.get::<_, i64>(10)? != 0, }) }).map_err(|e| CgcxError::Database(e.to_string()))?; let mut results = vec![]; for r in rows { results.push(r.map_err(|e| CgcxError::Database(e.to_string()))?); } Ok(results) } pub async fn revoke(&self, id: i64, revoked_by: i64) -> Result<()> { let conn = self.conn.lock().await; conn.execute( "UPDATE punishments SET active = 0, revoked_at = datetime('now'), revoked_by = ?1 WHERE id = ?2", params![revoked_by, id], ).map_err(|e| CgcxError::Database(e.to_string()))?; Ok(()) } pub async fn list_expired(&self) -> Result> { let conn = self.conn.lock().await; let mut stmt = conn.prepare( "SELECT id, chat_id, target_user_id, action_type, duration_seconds, reason, created_by, created_at, revoked_at, revoked_by, active FROM punishments WHERE active = 1 AND duration_seconds IS NOT NULL AND datetime(created_at, '+' || duration_seconds || ' seconds') <= datetime('now')" ).map_err(|e| CgcxError::Database(e.to_string()))?; let rows = stmt.query_map([], |row| { Ok(Punishment { id: row.get(0)?, chat_id: row.get(1)?, target_user_id: row.get(2)?, action_type: row.get(3)?, duration_seconds: row.get(4)?, reason: row.get(5)?, created_by: row.get(6)?, created_at: row.get(7)?, revoked_at: row.get(8)?, revoked_by: row.get(9)?, active: row.get::<_, i64>(10)? != 0, }) }).map_err(|e| CgcxError::Database(e.to_string()))?; let mut results = vec![]; for r in rows { results.push(r.map_err(|e| CgcxError::Database(e.to_string()))?); } Ok(results) } }