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 credential;
|
||||||
pub mod project;
|
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