use crate::db::Pool;
use crate::db::database::interact_err;
use crate::db::models::{Project, Session};
use anyhow::{Context, Result};
use rusqlite::params;
use uuid::Uuid;
#[derive(Clone)]
pub struct ProjectRepository {
pool: Pool,
}
impl ProjectRepository {
pub fn new(pool: Pool) -> Self {
Self { pool }
}
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Project>> {
let id_str = id.to_string();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.prepare_cached("SELECT * FROM projects WHERE id = ?1")?
.query_row(params![id_str], Project::from_row)
.optional()
})
.await
.map_err(interact_err)?
.context("Failed to find project")
}
pub async fn find_by_name(&self, name: &str) -> Result<Option<Project>> {
let n = name.to_string();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.prepare_cached("SELECT * FROM projects WHERE name = ?1")?
.query_row(params![n], Project::from_row)
.optional()
})
.await
.map_err(interact_err)?
.context("Failed to find project by name")
}
pub async fn list_all(&self) -> Result<Vec<Project>> {
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
let mut stmt =
conn.prepare_cached("SELECT * FROM projects ORDER BY updated_at DESC")?;
let rows = stmt.query_map([], Project::from_row)?;
rows.collect::<std::result::Result<Vec<_>, _>>()
})
.await
.map_err(interact_err)?
.context("Failed to list projects")
}
pub async fn create(&self, project: &Project) -> Result<()> {
let p = project.clone();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.execute(
"INSERT INTO projects (id, name, description, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![
p.id.to_string(),
p.name,
p.description,
p.created_at.timestamp(),
p.updated_at.timestamp(),
],
)
})
.await
.map_err(interact_err)?
.context("Failed to create project")?;
tracing::debug!("Created project: {} ({})", project.name, project.id);
Ok(())
}
pub async fn update(&self, project: &Project) -> Result<()> {
let p = project.clone();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.execute(
"UPDATE projects SET name = ?1, description = ?2, updated_at = ?3
WHERE id = ?4",
params![
p.name,
p.description,
p.updated_at.timestamp(),
p.id.to_string(),
],
)
})
.await
.map_err(interact_err)?
.context("Failed to update project")?;
tracing::debug!("Updated project: {}", project.id);
Ok(())
}
pub async fn delete(&self, id: Uuid) -> Result<()> {
let id_str = id.to_string();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.execute("DELETE FROM projects WHERE id = ?1", params![id_str])
})
.await
.map_err(interact_err)?
.context("Failed to delete project")?;
tracing::debug!("Deleted project: {}", id);
Ok(())
}
pub async fn assign_session(&self, session_id: Uuid, project_id: Uuid) -> Result<()> {
let sid = session_id.to_string();
let pid = project_id.to_string();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.execute(
"UPDATE sessions SET project_id = ?1, updated_at = strftime('%s', 'now')
WHERE id = ?2",
params![pid, sid],
)
})
.await
.map_err(interact_err)?
.context("Failed to assign session to project")?;
tracing::debug!("Assigned session {} to project {}", session_id, project_id);
Ok(())
}
pub async fn unassign_session(&self, session_id: Uuid) -> Result<()> {
let sid = session_id.to_string();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.execute(
"UPDATE sessions SET project_id = NULL, updated_at = strftime('%s', 'now')
WHERE id = ?1",
params![sid],
)
})
.await
.map_err(interact_err)?
.context("Failed to unassign session from project")?;
tracing::debug!("Unassigned session {} from project", session_id);
Ok(())
}
pub async fn find_sessions_by_project(&self, project_id: Uuid) -> Result<Vec<Session>> {
let pid = project_id.to_string();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
let mut stmt = conn.prepare_cached(
"SELECT * FROM sessions WHERE project_id = ?1 AND archived_at IS NULL
ORDER BY updated_at DESC",
)?;
let rows = stmt.query_map(params![pid], Session::from_row)?;
rows.collect::<std::result::Result<Vec<_>, _>>()
})
.await
.map_err(interact_err)?
.context("Failed to find sessions by project")
}
pub async fn find_unassigned_sessions(&self) -> Result<Vec<Session>> {
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
let mut stmt = conn.prepare_cached(
"SELECT * FROM sessions WHERE project_id IS NULL AND archived_at IS NULL
ORDER BY updated_at DESC",
)?;
let rows = stmt.query_map([], Session::from_row)?;
rows.collect::<std::result::Result<Vec<_>, _>>()
})
.await
.map_err(interact_err)?
.context("Failed to find unassigned sessions")
}
pub async fn count_sessions(&self, project_id: Uuid) -> Result<i64> {
let pid = project_id.to_string();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.query_row(
"SELECT COUNT(*) FROM sessions WHERE project_id = ?1 AND archived_at IS NULL",
params![pid],
|row| row.get(0),
)
})
.await
.map_err(interact_err)?
.context("Failed to count project sessions")
}
pub async fn count_files(&self, project_id: Uuid) -> Result<i64> {
let pid = project_id.to_string();
self.pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.query_row(
"SELECT COUNT(*) FROM files f
JOIN sessions s ON f.session_id = s.id
WHERE s.project_id = ?1",
params![pid],
|row| row.get(0),
)
})
.await
.map_err(interact_err)?
.context("Failed to count project files")
}
}
trait OptionalExt<T> {
fn optional(self) -> rusqlite::Result<Option<T>>;
}
impl<T> OptionalExt<T> for rusqlite::Result<T> {
fn optional(self) -> rusqlite::Result<Option<T>> {
match self {
Ok(v) => Ok(Some(v)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e),
}
}
}