diff --git a/src/core/timeline.rs b/src/core/timeline.rs index c9ee919..27b0dee 100644 --- a/src/core/timeline.rs +++ b/src/core/timeline.rs @@ -1,7 +1,10 @@ use std::cmp::Ordering; +use rusqlite::Connection; use serde::Serialize; +use super::error::Result; + /// The core timeline event. All pipeline stages produce or consume these. /// Spec ref: Section 3.3 "Event Model" #[derive(Debug, Clone, Serialize)] @@ -121,7 +124,7 @@ pub struct UnresolvedRef { pub source: EntityRef, pub target_project: Option, pub target_type: String, - pub target_iid: i64, + pub target_iid: Option, pub reference_type: String, } @@ -135,6 +138,45 @@ pub struct TimelineResult { pub unresolved_references: Vec, } +/// Resolve an entity's internal DB id to a full [`EntityRef`] with iid and project path. +/// +/// When `project_id` is `Some`, the query is scoped to that project. +/// Returns `Ok(None)` for unknown entity types or when no matching row exists. +pub fn resolve_entity_ref( + conn: &Connection, + entity_type: &str, + entity_id: i64, + project_id: Option, +) -> Result> { + let table = match entity_type { + "issue" => "issues", + "merge_request" => "merge_requests", + _ => return Ok(None), + }; + + let sql = format!( + "SELECT e.iid, p.path_with_namespace + FROM {table} e + JOIN projects p ON p.id = e.project_id + WHERE e.id = ?1 AND (?2 IS NULL OR e.project_id = ?2)" + ); + + let result = conn.query_row(&sql, rusqlite::params![entity_id, project_id], |row| { + Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)) + }); + + match result { + Ok((iid, project_path)) => Ok(Some(EntityRef { + entity_type: entity_type.to_owned(), + entity_id, + entity_iid: iid, + project_path, + })), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } +} + #[cfg(test)] mod tests { use super::*;