use crate::db::models::Event;
use crate::error::{IntentError, Result};
use chrono::Utc;
use neo4rs::{query, Graph};
use super::task_manager::neo4j_err;
pub struct Neo4jEventManager {
graph: Graph,
project_id: String,
}
impl Neo4jEventManager {
pub fn new(graph: Graph, project_id: String) -> Self {
Self { graph, project_id }
}
pub async fn add_event(
&self,
task_id: i64,
log_type: String,
discussion_data: String,
) -> Result<Event> {
let mut check = self
.graph
.execute(
query("MATCH (t:Task {project_id: $pid, id: $tid}) RETURN t.id AS id")
.param("pid", self.project_id.clone())
.param("tid", task_id),
)
.await
.map_err(|e| neo4j_err("add_event check task", e))?;
if check
.next()
.await
.map_err(|e| neo4j_err("add_event check task fetch", e))?
.is_none()
{
return Err(IntentError::TaskNotFound(task_id));
}
let id = super::next_id(&self.graph, &self.project_id, "event").await?;
let now = Utc::now();
let timestamp_str = now.to_rfc3339();
let mut result = self
.graph
.execute(
query(
"MATCH (t:Task {project_id: $pid, id: $tid}) \
CREATE (e:Event {project_id: $pid, id: $eid, task_id: $tid, \
log_type: $log_type, discussion_data: $data, \
timestamp: $ts})-[:BELONGS_TO]->(t) \
RETURN e",
)
.param("pid", self.project_id.clone())
.param("tid", task_id)
.param("eid", id)
.param("log_type", log_type.clone())
.param("data", discussion_data.clone())
.param("ts", timestamp_str),
)
.await
.map_err(|e| neo4j_err("add_event create", e))?;
match result
.next()
.await
.map_err(|e| neo4j_err("add_event fetch", e))?
{
Some(_) => Ok(Event {
id,
task_id,
timestamp: now,
log_type,
discussion_data,
}),
None => Err(IntentError::OtherError(anyhow::anyhow!(
"add_event: CREATE did not return a node"
))),
}
}
pub async fn list_events(
&self,
task_id: Option<i64>,
limit: Option<i64>,
log_type: Option<String>,
since: Option<String>,
) -> Result<Vec<Event>> {
if let Some(tid) = task_id {
let mut check = self
.graph
.execute(
query("MATCH (t:Task {project_id: $pid, id: $tid}) RETURN t.id AS id")
.param("pid", self.project_id.clone())
.param("tid", tid),
)
.await
.map_err(|e| neo4j_err("list_events check task", e))?;
if check
.next()
.await
.map_err(|e| neo4j_err("list_events check task fetch", e))?
.is_none()
{
return Err(IntentError::TaskNotFound(tid));
}
}
let limit = limit.unwrap_or(50);
let since_timestamp = if let Some(duration_str) = &since {
Some(crate::time_utils::parse_duration(duration_str)?)
} else {
None
};
let mut where_parts = vec!["e.project_id = $pid".to_string()];
let mut has_task_filter = false;
let mut has_type_filter = false;
let mut has_since_filter = false;
if task_id.is_some() {
where_parts.push("e.task_id = $filter_task_id".to_string());
has_task_filter = true;
}
if log_type.is_some() {
where_parts.push("e.log_type = $filter_log_type".to_string());
has_type_filter = true;
}
if since_timestamp.is_some() {
where_parts.push("e.timestamp >= $filter_since".to_string());
has_since_filter = true;
}
let cypher = format!(
"MATCH (e:Event) WHERE {} \
RETURN e \
ORDER BY e.timestamp DESC \
LIMIT $limit",
where_parts.join(" AND ")
);
let mut q = query(&cypher)
.param("pid", self.project_id.clone())
.param("limit", limit);
if has_task_filter {
q = q.param("filter_task_id", task_id.unwrap());
}
if has_type_filter {
q = q.param("filter_log_type", log_type.unwrap());
}
if has_since_filter {
q = q.param("filter_since", since_timestamp.unwrap().to_rfc3339());
}
let mut result = self
.graph
.execute(q)
.await
.map_err(|e| neo4j_err("list_events query", e))?;
let mut events = Vec::new();
while let Some(row) = result
.next()
.await
.map_err(|e| neo4j_err("list_events iterate", e))?
{
let node: neo4rs::Node = row
.get("e")
.map_err(|e| neo4j_err("list_events get node", e))?;
events.push(node_to_event(&node)?);
}
Ok(events)
}
pub async fn get_events_summary(
&self,
task_id: i64,
) -> Result<crate::db::models::EventsSummary> {
let mut result = self
.graph
.execute(
query(
"MATCH (e:Event {project_id: $pid, task_id: $tid}) \
WITH e ORDER BY e.timestamp DESC \
WITH collect(e) AS all_events \
RETURN size(all_events) AS cnt, all_events[0..10] AS recent",
)
.param("pid", self.project_id.clone())
.param("tid", task_id),
)
.await
.map_err(|e| neo4j_err("get_events_summary", e))?;
match result
.next()
.await
.map_err(|e| neo4j_err("get_events_summary fetch", e))?
{
Some(row) => {
let total_count: i64 = row
.get("cnt")
.map_err(|e| neo4j_err("get_events_summary cnt", e))?;
let recent_nodes: Vec<neo4rs::Node> = row
.get("recent")
.map_err(|e| neo4j_err("get_events_summary recent", e))?;
let mut recent_events = Vec::with_capacity(recent_nodes.len());
for node in &recent_nodes {
recent_events.push(node_to_event(node)?);
}
Ok(crate::db::models::EventsSummary {
total_count,
recent_events,
})
},
None => Ok(crate::db::models::EventsSummary {
total_count: 0,
recent_events: Vec::new(),
}),
}
}
}
impl crate::backend::EventBackend for Neo4jEventManager {
fn add_event(
&self,
task_id: i64,
log_type: String,
discussion_data: String,
) -> impl std::future::Future<Output = Result<Event>> + Send {
self.add_event(task_id, log_type, discussion_data)
}
fn list_events(
&self,
task_id: Option<i64>,
limit: Option<i64>,
log_type: Option<String>,
since: Option<String>,
) -> impl std::future::Future<Output = Result<Vec<Event>>> + Send {
self.list_events(task_id, limit, log_type, since)
}
}
pub(crate) fn node_to_event(node: &neo4rs::Node) -> Result<Event> {
let id: i64 = node.get("id").map_err(|e| neo4j_err("event.id", e))?;
let task_id: i64 = node
.get("task_id")
.map_err(|e| neo4j_err("event.task_id", e))?;
let log_type: String = node
.get("log_type")
.map_err(|e| neo4j_err("event.log_type", e))?;
let discussion_data: String = node
.get("discussion_data")
.map_err(|e| neo4j_err("event.discussion_data", e))?;
let timestamp_str: String = node
.get("timestamp")
.map_err(|e| neo4j_err("event.timestamp", e))?;
let timestamp = chrono::DateTime::parse_from_rfc3339(×tamp_str)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| {
IntentError::OtherError(anyhow::anyhow!(
"Failed to parse event timestamp '{}': {}",
timestamp_str,
e
))
})?;
Ok(Event {
id,
task_id,
timestamp,
log_type,
discussion_data,
})
}