#![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 Project {
pub user_id: String,
pub name: String,
pub path: String,
pub metadata: serde_json::Value,
pub created_at: String,
pub updated_at: String,
pub synced_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProjectInput {
pub user_id: String,
pub name: String,
pub path: String,
#[serde(default)]
pub metadata: Option<serde_json::Value>,
}
pub fn upsert(conn: &Connection, input: ProjectInput) -> Result<Project> {
let now = Utc::now().to_rfc3339();
let meta_json = input
.metadata
.map(|m| m.to_string())
.unwrap_or_else(|| "{}".into());
conn.execute(
r#"INSERT INTO projects (user_id, name, path, metadata, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?5)
ON CONFLICT(user_id, name) DO UPDATE SET
path = excluded.path,
metadata = excluded.metadata,
updated_at = excluded.updated_at,
synced_at = NULL"#,
params![input.user_id, input.name, input.path, meta_json, now],
)
.context("upsert project")?;
get(conn, &input.user_id, &input.name)?.context("project missing after upsert")
}
pub fn get(conn: &Connection, user_id: &str, name: &str) -> Result<Option<Project>> {
let mut stmt = conn.prepare(
r#"SELECT user_id, name, path, metadata, created_at, updated_at, synced_at
FROM projects
WHERE user_id = ?1 AND name = ?2"#,
)?;
Ok(stmt.query_row(params![user_id, name], row_to_project).ok())
}
pub fn resolve_canonical_name(
conn: &Connection,
user_id: &str,
cwd: &str,
) -> Result<Option<String>> {
let mut stmt =
conn.prepare("SELECT name FROM projects WHERE user_id = ?1 AND path = ?2 LIMIT 1")?;
if let Ok(name) = stmt.query_row(params![user_id, cwd], |r| r.get::<_, String>(0)) {
return Ok(Some(name));
}
let cwd_with_slash = format!("{}/", cwd.trim_end_matches('/'));
let mut stmt = conn.prepare(
"SELECT name FROM projects
WHERE user_id = ?1
AND ?2 LIKE path || '/%'
ORDER BY length(path) DESC
LIMIT 1",
)?;
if let Ok(name) = stmt.query_row(params![user_id, cwd_with_slash], |r| r.get::<_, String>(0)) {
return Ok(Some(name));
}
let basename = match std::path::Path::new(cwd)
.file_name()
.and_then(|s| s.to_str())
{
Some(b) => b,
None => return Ok(None),
};
let mut stmt = conn.prepare(
"SELECT name FROM projects WHERE user_id = ?1 AND LOWER(name) = LOWER(?2) LIMIT 1",
)?;
Ok(stmt
.query_row(params![user_id, basename], |r| r.get::<_, String>(0))
.ok())
}
pub fn list(conn: &Connection, user_id: &str) -> Result<Vec<Project>> {
let mut stmt = conn.prepare(
r#"SELECT user_id, name, path, metadata, created_at, updated_at, synced_at
FROM projects
WHERE user_id = ?1
ORDER BY name ASC"#,
)?;
let rows: Vec<Project> = stmt
.query_map(params![user_id], row_to_project)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
pub fn list_unsynced(conn: &Connection, limit: usize) -> Result<Vec<Project>> {
let mut stmt = conn.prepare(
r#"SELECT user_id, name, path, metadata, created_at, updated_at, synced_at
FROM projects
WHERE synced_at IS NULL OR updated_at > synced_at
ORDER BY updated_at ASC
LIMIT ?1"#,
)?;
let rows: Vec<Project> = stmt
.query_map(params![limit as i64], row_to_project)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
pub fn mark_synced(
conn: &Connection,
keys: &[(String, String)], when: &str,
) -> Result<()> {
let tx = conn.unchecked_transaction()?;
for (user_id, name) in keys {
tx.execute(
"UPDATE projects SET synced_at = ?1 WHERE user_id = ?2 AND name = ?3",
params![when, user_id, name],
)?;
}
tx.commit()?;
Ok(())
}
fn row_to_project(row: &rusqlite::Row<'_>) -> rusqlite::Result<Project> {
let meta_str: String = row.get(3)?;
let metadata = serde_json::from_str(&meta_str).unwrap_or_else(|_| serde_json::json!({}));
Ok(Project {
user_id: row.get(0)?,
name: row.get(1)?,
path: row.get(2)?,
metadata,
created_at: row.get(4)?,
updated_at: row.get(5)?,
synced_at: row.get(6)?,
})
}