use crate::adapter::sqlite_repos::{DEFAULT_EVENT_LIST_LIMIT, SqliteEventRepository};
use crate::adapter::unit_of_work::UnitOfWork;
use crate::error::CarryCtxError;
use crate::repository::event::{EventFilter, EventRecord, EventRepository};
#[derive(serde::Serialize)]
pub struct CursorList {
pub events: Vec<EventRecord>,
pub next_cursor: Option<String>,
}
pub fn list_events(
project_id: &str,
filter: &EventFilter,
cursor: Option<&str>,
uow: &UnitOfWork,
) -> Result<CursorList, CarryCtxError> {
let conn = uow.connection();
let repo = SqliteEventRepository::new(conn);
let (before_ts, before_id) = match cursor {
Some(token) => {
let (ts, id) = decode_cursor(token)?;
(Some(ts), Some(id))
}
None => (None, None),
};
let effective_limit = filter.limit.unwrap_or(DEFAULT_EVENT_LIST_LIMIT);
let fetch_limit = effective_limit
.checked_add(1)
.ok_or_else(|| CarryCtxError::validation_error("Event list limit is too large."))?;
let adjusted_filter = EventFilter {
project_id: project_id.to_string(),
task_id: filter.task_id.clone(),
agent_id: filter.agent_id.clone(),
session_id: filter.session_id.clone(),
event_type: filter.event_type.clone(),
since: filter.since.clone(),
until: filter.until.clone(),
limit: Some(fetch_limit),
};
let mut events =
repo.list_before_cursor(&adjusted_filter, before_ts.as_deref(), before_id.as_deref())?;
let next_cursor = if events.len() > effective_limit as usize {
events.truncate(effective_limit as usize);
events.last().map(|e| encode_cursor(&e.occurred_at, &e.id))
} else {
None
};
Ok(CursorList {
events,
next_cursor,
})
}
fn encode_cursor(occurred_at: &str, id: &str) -> String {
format!("{occurred_at}|{id}")
}
fn decode_cursor(token: &str) -> Result<(String, String), CarryCtxError> {
let (ts, id) = token.split_once('|').ok_or_else(|| {
CarryCtxError::validation_error(
"Invalid event cursor format; use a cursor previously returned by this command.",
)
})?;
if ts.is_empty() || id.is_empty() {
return Err(CarryCtxError::validation_error(
"Invalid event cursor format; use a cursor previously returned by this command.",
));
}
Ok((ts.to_string(), id.to_string()))
}
pub fn show_event(
project_id: &str,
event_id: &str,
uow: &UnitOfWork,
) -> Result<EventRecord, CarryCtxError> {
let conn = uow.connection();
let repo = SqliteEventRepository::new(conn);
let event = repo.find_by_id(project_id, event_id)?;
event.ok_or_else(|| CarryCtxError::resource_not_found(format!("Event '{event_id}' not found.")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_round_trip() {
let token = encode_cursor("2026-08-24T10:00:00+00:00", "01JABCDEF");
assert_eq!(
decode_cursor(&token).unwrap(),
(
"2026-08-24T10:00:00+00:00".to_string(),
"01JABCDEF".to_string()
)
);
}
#[test]
fn cursor_rejects_garbage() {
assert!(decode_cursor("no-separator").is_err());
assert!(decode_cursor("|only-id").is_err());
assert!(decode_cursor("only-ts|").is_err());
assert!(decode_cursor("").is_err());
}
}