feat: WatchedTracker model with CRUD, filters, and agent config
This commit is contained in:
parent
73f6909be4
commit
1e69cf16a9
2 changed files with 360 additions and 0 deletions
|
|
@ -1,2 +1,3 @@
|
|||
pub mod credential;
|
||||
pub mod project;
|
||||
pub mod tracker;
|
||||
|
|
|
|||
359
src-tauri/src/models/tracker.rs
Normal file
359
src-tauri/src/models/tracker.rs
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
use rusqlite::{params, Connection, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentConfig {
|
||||
pub analyst_command: String,
|
||||
pub analyst_args: Vec<String>,
|
||||
pub developer_command: String,
|
||||
pub developer_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FilterGroup {
|
||||
pub conditions: Vec<Filter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Filter {
|
||||
pub field: String,
|
||||
pub operator: String, // "In", "NotIn", "Equals", "NotEquals"
|
||||
pub value: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WatchedTracker {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub tracker_id: i32,
|
||||
pub tracker_label: String,
|
||||
pub polling_interval: i32,
|
||||
pub agent_config: AgentConfig,
|
||||
pub filters: Vec<FilterGroup>,
|
||||
pub enabled: bool,
|
||||
pub last_polled_at: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
fn from_row(row: &rusqlite::Row) -> rusqlite::Result<WatchedTracker> {
|
||||
let agent_config_json: String = row.get(5)?;
|
||||
let filters_json: String = row.get(6)?;
|
||||
let enabled_int: i32 = row.get(7)?;
|
||||
|
||||
let agent_config: AgentConfig = serde_json::from_str(&agent_config_json)
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(e)))?;
|
||||
let filters: Vec<FilterGroup> = serde_json::from_str(&filters_json)
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(6, rusqlite::types::Type::Text, Box::new(e)))?;
|
||||
|
||||
Ok(WatchedTracker {
|
||||
id: row.get(0)?,
|
||||
project_id: row.get(1)?,
|
||||
tracker_id: row.get(2)?,
|
||||
tracker_label: row.get(3)?,
|
||||
polling_interval: row.get(4)?,
|
||||
agent_config,
|
||||
filters,
|
||||
enabled: enabled_int != 0,
|
||||
last_polled_at: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
})
|
||||
}
|
||||
|
||||
impl WatchedTracker {
|
||||
pub fn insert(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
tracker_id: i32,
|
||||
tracker_label: &str,
|
||||
polling_interval: i32,
|
||||
agent_config: AgentConfig,
|
||||
filters: Vec<FilterGroup>,
|
||||
) -> Result<WatchedTracker> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let agent_config_json = serde_json::to_string(&agent_config)
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||
let filters_json = serde_json::to_string(&filters)
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO watched_trackers (id, project_id, tracker_id, tracker_label, polling_interval, agent_config_json, filters_json, created_at) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![id, project_id, tracker_id, tracker_label, polling_interval, agent_config_json, filters_json, now],
|
||||
)?;
|
||||
|
||||
Ok(WatchedTracker {
|
||||
id,
|
||||
project_id: project_id.to_string(),
|
||||
tracker_id,
|
||||
tracker_label: tracker_label.to_string(),
|
||||
polling_interval,
|
||||
agent_config,
|
||||
filters,
|
||||
enabled: true,
|
||||
last_polled_at: None,
|
||||
created_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_by_project(conn: &Connection, project_id: &str) -> Result<Vec<WatchedTracker>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, project_id, tracker_id, tracker_label, polling_interval, agent_config_json, filters_json, enabled, last_polled_at, created_at \
|
||||
FROM watched_trackers WHERE project_id = ?1 ORDER BY created_at DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![project_id], from_row)?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn list_all_enabled(conn: &Connection) -> Result<Vec<WatchedTracker>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, project_id, tracker_id, tracker_label, polling_interval, agent_config_json, filters_json, enabled, last_polled_at, created_at \
|
||||
FROM watched_trackers WHERE enabled = 1 ORDER BY created_at DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], from_row)?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn get_by_id(conn: &Connection, id: &str) -> Result<WatchedTracker> {
|
||||
conn.query_row(
|
||||
"SELECT id, project_id, tracker_id, tracker_label, polling_interval, agent_config_json, filters_json, enabled, last_polled_at, created_at \
|
||||
FROM watched_trackers WHERE id = ?1",
|
||||
params![id],
|
||||
from_row,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
conn: &Connection,
|
||||
id: &str,
|
||||
polling_interval: i32,
|
||||
agent_config: AgentConfig,
|
||||
filters: Vec<FilterGroup>,
|
||||
enabled: bool,
|
||||
) -> Result<()> {
|
||||
let agent_config_json = serde_json::to_string(&agent_config)
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||
let filters_json = serde_json::to_string(&filters)
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||
let enabled_int = if enabled { 1i32 } else { 0i32 };
|
||||
|
||||
let affected = conn.execute(
|
||||
"UPDATE watched_trackers SET polling_interval = ?1, agent_config_json = ?2, filters_json = ?3, enabled = ?4 WHERE id = ?5",
|
||||
params![polling_interval, agent_config_json, filters_json, enabled_int, id],
|
||||
)?;
|
||||
|
||||
if affected == 0 {
|
||||
return Err(rusqlite::Error::QueryReturnedNoRows);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_last_polled(conn: &Connection, id: &str) -> Result<()> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let affected = conn.execute(
|
||||
"UPDATE watched_trackers SET last_polled_at = ?1 WHERE id = ?2",
|
||||
params![now, id],
|
||||
)?;
|
||||
if affected == 0 {
|
||||
return Err(rusqlite::Error::QueryReturnedNoRows);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete(conn: &Connection, id: &str) -> Result<()> {
|
||||
let affected = conn.execute("DELETE FROM watched_trackers WHERE id = ?1", params![id])?;
|
||||
if affected == 0 {
|
||||
return Err(rusqlite::Error::QueryReturnedNoRows);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db;
|
||||
use crate::models::project::Project;
|
||||
|
||||
fn setup() -> Connection {
|
||||
let conn = db::init_in_memory().expect("db init should succeed");
|
||||
Project::insert(&conn, "Test Project", "/path/test", None, "main").unwrap();
|
||||
conn
|
||||
}
|
||||
|
||||
fn project_id(conn: &Connection) -> String {
|
||||
Project::list(conn).unwrap().into_iter().next().unwrap().id
|
||||
}
|
||||
|
||||
fn sample_agent_config() -> AgentConfig {
|
||||
AgentConfig {
|
||||
analyst_command: "analyst".to_string(),
|
||||
analyst_args: vec!["--mode".to_string(), "analyze".to_string()],
|
||||
developer_command: "developer".to_string(),
|
||||
developer_args: vec!["--fix".to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_filters() -> Vec<FilterGroup> {
|
||||
vec![FilterGroup {
|
||||
conditions: vec![Filter {
|
||||
field: "status".to_string(),
|
||||
operator: "In".to_string(),
|
||||
value: vec!["Open".to_string(), "In Progress".to_string()],
|
||||
}],
|
||||
}]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_tracker() {
|
||||
let conn = setup();
|
||||
let pid = project_id(&conn);
|
||||
|
||||
let tracker = WatchedTracker::insert(
|
||||
&conn,
|
||||
&pid,
|
||||
42,
|
||||
"Bug Tracker",
|
||||
15,
|
||||
sample_agent_config(),
|
||||
sample_filters(),
|
||||
)
|
||||
.expect("insert should succeed");
|
||||
|
||||
assert!(!tracker.id.is_empty());
|
||||
assert_eq!(tracker.project_id, pid);
|
||||
assert_eq!(tracker.tracker_id, 42);
|
||||
assert_eq!(tracker.tracker_label, "Bug Tracker");
|
||||
assert_eq!(tracker.polling_interval, 15);
|
||||
assert!(tracker.enabled);
|
||||
assert!(tracker.last_polled_at.is_none());
|
||||
assert!(!tracker.created_at.is_empty());
|
||||
assert_eq!(tracker.agent_config.analyst_command, "analyst");
|
||||
assert_eq!(tracker.filters.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_by_project() {
|
||||
let conn = setup();
|
||||
let pid = project_id(&conn);
|
||||
|
||||
WatchedTracker::insert(&conn, &pid, 1, "Tracker A", 10, sample_agent_config(), vec![]).unwrap();
|
||||
WatchedTracker::insert(&conn, &pid, 2, "Tracker B", 20, sample_agent_config(), vec![]).unwrap();
|
||||
|
||||
let trackers = WatchedTracker::list_by_project(&conn, &pid).expect("list should succeed");
|
||||
assert_eq!(trackers.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_all_enabled() {
|
||||
let conn = setup();
|
||||
let pid = project_id(&conn);
|
||||
|
||||
let t1 = WatchedTracker::insert(&conn, &pid, 1, "Enabled", 10, sample_agent_config(), vec![]).unwrap();
|
||||
let t2 = WatchedTracker::insert(&conn, &pid, 2, "Disabled", 10, sample_agent_config(), vec![]).unwrap();
|
||||
|
||||
// Disable t2
|
||||
WatchedTracker::update(
|
||||
&conn,
|
||||
&t2.id,
|
||||
t2.polling_interval,
|
||||
sample_agent_config(),
|
||||
vec![],
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let enabled = WatchedTracker::list_all_enabled(&conn).expect("list_all_enabled should succeed");
|
||||
assert_eq!(enabled.len(), 1);
|
||||
assert_eq!(enabled[0].id, t1.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_by_id() {
|
||||
let conn = setup();
|
||||
let pid = project_id(&conn);
|
||||
|
||||
let created = WatchedTracker::insert(
|
||||
&conn,
|
||||
&pid,
|
||||
99,
|
||||
"My Tracker",
|
||||
30,
|
||||
sample_agent_config(),
|
||||
sample_filters(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let found = WatchedTracker::get_by_id(&conn, &created.id).expect("get_by_id should succeed");
|
||||
assert_eq!(found.id, created.id);
|
||||
assert_eq!(found.tracker_id, 99);
|
||||
assert_eq!(found.tracker_label, "My Tracker");
|
||||
assert_eq!(found.polling_interval, 30);
|
||||
assert_eq!(found.filters.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_tracker() {
|
||||
let conn = setup();
|
||||
let pid = project_id(&conn);
|
||||
|
||||
let created = WatchedTracker::insert(
|
||||
&conn,
|
||||
&pid,
|
||||
10,
|
||||
"Original",
|
||||
5,
|
||||
sample_agent_config(),
|
||||
sample_filters(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let new_filters = vec![FilterGroup {
|
||||
conditions: vec![Filter {
|
||||
field: "priority".to_string(),
|
||||
operator: "Equals".to_string(),
|
||||
value: vec!["High".to_string()],
|
||||
}],
|
||||
}];
|
||||
|
||||
WatchedTracker::update(&conn, &created.id, 60, sample_agent_config(), new_filters, false)
|
||||
.expect("update should succeed");
|
||||
|
||||
let updated = WatchedTracker::get_by_id(&conn, &created.id).unwrap();
|
||||
assert_eq!(updated.polling_interval, 60);
|
||||
assert!(!updated.enabled);
|
||||
assert_eq!(updated.filters[0].conditions[0].field, "priority");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_last_polled() {
|
||||
let conn = setup();
|
||||
let pid = project_id(&conn);
|
||||
|
||||
let created =
|
||||
WatchedTracker::insert(&conn, &pid, 5, "Poller", 10, sample_agent_config(), vec![]).unwrap();
|
||||
|
||||
assert!(created.last_polled_at.is_none());
|
||||
|
||||
WatchedTracker::update_last_polled(&conn, &created.id).expect("update_last_polled should succeed");
|
||||
|
||||
let updated = WatchedTracker::get_by_id(&conn, &created.id).unwrap();
|
||||
assert!(updated.last_polled_at.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_tracker() {
|
||||
let conn = setup();
|
||||
let pid = project_id(&conn);
|
||||
|
||||
let created =
|
||||
WatchedTracker::insert(&conn, &pid, 7, "ToDelete", 10, sample_agent_config(), vec![]).unwrap();
|
||||
|
||||
WatchedTracker::delete(&conn, &created.id).expect("delete should succeed");
|
||||
|
||||
let result = WatchedTracker::get_by_id(&conn, &created.id);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue