#![allow(dead_code)]
use anyhow::{Context, Result};
use chrono::Utc;
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Issue {
pub id: String,
pub user_id: String,
pub title: String,
pub summary: String,
pub projects: Vec<String>,
pub status: String,
pub started_at: String,
pub ended_at: Option<String>,
pub event_count: i64,
pub metadata: serde_json::Value,
pub created_at: String,
pub updated_at: String,
pub synced_at: Option<String>,
}
#[derive(Debug, Clone)]
pub struct IssueInput {
pub user_id: String,
pub title: String,
pub summary: String,
pub projects: Vec<String>,
pub started_at: String,
pub ended_at: Option<String>,
pub event_count: i64,
pub metadata: serde_json::Value,
}
pub fn insert(conn: &Connection, input: IssueInput) -> Result<Issue> {
let id = super::uuid_like();
let now = Utc::now().to_rfc3339();
conn.execute(
r#"INSERT INTO issues
(id, user_id, title, summary, projects, status, started_at, ended_at,
event_count, metadata, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, 'completed', ?6, ?7, ?8, ?9, ?10, ?10)"#,
params![
id,
input.user_id,
input.title,
input.summary,
serde_json::to_string(&input.projects)?,
input.started_at,
input.ended_at,
input.event_count,
input.metadata.to_string(),
now,
],
)
.context("insert issue")?;
get(conn, &input.user_id, &id)?.context("issue missing after insert")
}
pub fn get(conn: &Connection, user_id: &str, id: &str) -> Result<Option<Issue>> {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, title, summary, projects, status,
started_at, ended_at, event_count, metadata,
created_at, updated_at, synced_at
FROM issues
WHERE user_id = ?1 AND id = ?2"#,
)?;
Ok(stmt.query_row(params![user_id, id], row_to_issue).ok())
}
pub fn list(conn: &Connection, user_id: &str, limit: usize) -> Result<Vec<Issue>> {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, title, summary, projects, status,
started_at, ended_at, event_count, metadata,
created_at, updated_at, synced_at
FROM issues
WHERE user_id = ?1
ORDER BY started_at DESC LIMIT ?2"#,
)?;
let rows: Vec<Issue> = stmt
.query_map(params![user_id, limit as i64], row_to_issue)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
pub fn last_ended_at(conn: &Connection, user_id: &str) -> Result<Option<String>> {
let mut stmt = conn.prepare(
r#"SELECT COALESCE(ended_at, started_at) FROM issues
WHERE user_id = ?1
ORDER BY started_at DESC LIMIT 1"#,
)?;
Ok(stmt.query_row(params![user_id], |r| r.get(0)).ok())
}
pub fn list_unsynced(conn: &Connection, limit: usize) -> Result<Vec<Issue>> {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, title, summary, projects, status,
started_at, ended_at, event_count, metadata,
created_at, updated_at, synced_at
FROM issues
WHERE synced_at IS NULL OR updated_at > synced_at
ORDER BY updated_at ASC LIMIT ?1"#,
)?;
let rows: Vec<Issue> = stmt
.query_map(params![limit as i64], row_to_issue)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
pub fn mark_synced(conn: &Connection, ids: &[&str], when: &str) -> Result<()> {
if ids.is_empty() {
return Ok(());
}
let placeholders = ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
let sql = format!(
"UPDATE issues SET synced_at = ? WHERE id IN ({})",
placeholders
);
let mut stmt = conn.prepare(&sql)?;
let mut binds: Vec<rusqlite::types::Value> = Vec::with_capacity(ids.len() + 1);
binds.push(when.to_string().into());
for id in ids {
binds.push((*id).to_string().into());
}
stmt.execute(rusqlite::params_from_iter(binds.iter()))?;
Ok(())
}
fn row_to_issue(row: &rusqlite::Row<'_>) -> rusqlite::Result<Issue> {
let projects_str: String = row.get(4)?;
let metadata_str: String = row.get(9)?;
Ok(Issue {
id: row.get(0)?,
user_id: row.get(1)?,
title: row.get(2)?,
summary: row.get(3)?,
projects: serde_json::from_str(&projects_str).unwrap_or_default(),
status: row.get(5)?,
started_at: row.get(6)?,
ended_at: row.get(7)?,
event_count: row.get(8)?,
metadata: serde_json::from_str(&metadata_str).unwrap_or_else(|_| serde_json::json!({})),
created_at: row.get(10)?,
updated_at: row.get(11)?,
synced_at: row.get(12)?,
})
}