336 lines
9.6 KiB
Rust
336 lines
9.6 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use rusqlite::{Connection, OptionalExtension};
|
|
use xdg::BaseDirectories;
|
|
|
|
use crate::domain;
|
|
use crate::repo::{Error, Repository};
|
|
|
|
struct Task {
|
|
id: i64,
|
|
name: String,
|
|
project: Option<String>,
|
|
link: Option<String>,
|
|
dir_path: Option<String>,
|
|
/*
|
|
current: bool,
|
|
finished_at: Option<time::OffsetDateTime>,
|
|
*/
|
|
created_at: time::OffsetDateTime,
|
|
|
|
idx: i64,
|
|
}
|
|
|
|
impl From<Task> for domain::Task {
|
|
fn from(repo: Task) -> Self {
|
|
Self {
|
|
name: repo.name,
|
|
project: repo.project,
|
|
link: repo.link,
|
|
dir_path: repo.dir_path.map(PathBuf::from),
|
|
created_at: repo.created_at,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'r> TryFrom<&'r rusqlite::Row<'_>> for Task {
|
|
type Error = rusqlite::Error;
|
|
|
|
fn try_from(row: &'r rusqlite::Row<'_>) -> Result<Self, Self::Error> {
|
|
Ok(Self {
|
|
id: row.get("id")?,
|
|
name: row.get("name")?,
|
|
project: row.get("project")?,
|
|
link: row.get("link")?,
|
|
dir_path: row.get("dir_path")?,
|
|
/*
|
|
current: row.get("current")?,
|
|
finished_at: row.get("finished_at")?,
|
|
*/
|
|
created_at: row.get("created_at")?,
|
|
idx: row.get("idx")?,
|
|
})
|
|
}
|
|
}
|
|
|
|
const SCHEMA_FILE: &str = "tas.db";
|
|
|
|
pub struct SqliteRepo {
|
|
conn: Connection,
|
|
}
|
|
|
|
impl SqliteRepo {
|
|
pub fn new(xdg_dirs: BaseDirectories) -> Result<Self, Error> {
|
|
let file_path = xdg_dirs.get_data_file(SCHEMA_FILE);
|
|
let conn = Connection::open(file_path).map_err(|_| Error::Connect)?;
|
|
|
|
Ok(Self { conn })
|
|
}
|
|
}
|
|
|
|
impl Repository for SqliteRepo {
|
|
fn get_current_task_opt(&self) -> Result<Option<domain::CurrentTaskInfo>, Error> {
|
|
let db_task = self.get_current_task_opt_impl()?;
|
|
|
|
Ok(db_task.map(|db_task| domain::CurrentTaskInfo {
|
|
task_idx: db_task.idx as usize,
|
|
task: db_task.into(),
|
|
}))
|
|
}
|
|
|
|
fn get_task_opt(&self, id: domain::TaskIdx) -> Result<Option<domain::Task>, Error> {
|
|
self.get_task_opt_impl(id).map(|t| t.map(From::from))
|
|
}
|
|
|
|
fn get_tasks(&self, finished: bool) -> Result<Vec<domain::Task>, Error> {
|
|
let mut stmt = if finished {
|
|
self.conn
|
|
.prepare("SELECT * FROM finished_tasks")
|
|
.map_err(|_| Error::PrepareQuery)?
|
|
} else {
|
|
self.conn
|
|
.prepare("SELECT * FROM active_tasks")
|
|
.map_err(|_| Error::PrepareQuery)?
|
|
};
|
|
|
|
let rows = stmt
|
|
.query_map([], |row| Task::try_from(row))
|
|
.map_err(|_| Error::QueryData)?;
|
|
|
|
rows.into_iter()
|
|
.map(|db_task| db_task.map(From::from).map_err(|_| Error::QueryData))
|
|
.collect::<Result<Vec<_>, Error>>()
|
|
.map_err(|_| Error::InvalidData)
|
|
}
|
|
|
|
fn remove_task(&self, idx: domain::TaskIdx) -> Result<domain::Task, Error> {
|
|
let db_task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?;
|
|
|
|
let mut stmt = self
|
|
.conn
|
|
.prepare("DELETE FROM tasks WHERE id = ?")
|
|
.map_err(|_| Error::PrepareQuery)?;
|
|
|
|
stmt.execute([&(db_task.id)])
|
|
.map_err(|_| Error::RemoveData)?;
|
|
|
|
Ok(db_task.into())
|
|
}
|
|
|
|
fn insert_task(&self, insert_data: super::InsertTaskData) -> Result<domain::Task, Error> {
|
|
let mut stmt = self
|
|
.conn
|
|
.prepare(
|
|
"INSERT INTO tasks (name, project, link, dir_path)
|
|
VALUES (?1, ?2, ?3, ?4)",
|
|
)
|
|
.map_err(|_| Error::PrepareQuery)?;
|
|
|
|
let id = stmt
|
|
.insert((
|
|
&insert_data.name,
|
|
&insert_data.project,
|
|
&insert_data.link,
|
|
&insert_data
|
|
.dir_path
|
|
.and_then(|p| p.into_os_string().into_string().ok()),
|
|
))
|
|
.map_err(|_| Error::InsertData)?;
|
|
|
|
self.get_task_by_id_impl(id).map(From::from)
|
|
}
|
|
|
|
fn update_task(
|
|
&self,
|
|
idx: domain::TaskIdx,
|
|
update_data: super::UpdateTaskData,
|
|
) -> Result<domain::Task, Error> {
|
|
let task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?;
|
|
|
|
let mut stmt = self
|
|
.conn
|
|
.prepare(
|
|
"UPDATE tasks
|
|
SET name = ?1,
|
|
project = ?2,
|
|
link = ?3,
|
|
dir_path = ?4
|
|
WHERE id = ?5
|
|
",
|
|
)
|
|
.map_err(|_| Error::PrepareQuery)?;
|
|
|
|
stmt.execute((
|
|
&update_data.name.unwrap_or(task.name),
|
|
&update_data.project.unwrap_or(task.project),
|
|
&update_data.link.unwrap_or(task.link),
|
|
&update_data
|
|
.dir_path
|
|
.unwrap_or(task.dir_path.map(PathBuf::from))
|
|
.and_then(|p| p.into_os_string().into_string().ok()),
|
|
&task.id,
|
|
))
|
|
.map_err(|_| Error::UpdateData)?;
|
|
|
|
self.get_task_opt(idx)?.ok_or(Error::NotFound)
|
|
}
|
|
|
|
fn start_task(&self, idx: domain::TaskIdx) -> Result<domain::Task, Error> {
|
|
let db_task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?;
|
|
|
|
self.conn
|
|
.execute(
|
|
"UPDATE tasks SET current = true WHERE id = ?1",
|
|
[&db_task.id],
|
|
)
|
|
.map_err(|_| Error::UpdateData)?;
|
|
|
|
Ok(db_task.into())
|
|
}
|
|
|
|
fn stop_task(&self) -> Result<domain::Task, Error> {
|
|
let db_task = self.get_current_task_opt_impl()?.ok_or(Error::NotFound)?;
|
|
|
|
self.conn
|
|
.execute(
|
|
"UPDATE tasks SET current = false WHERE id = ?1",
|
|
[&db_task.id],
|
|
)
|
|
.map_err(|_| Error::UpdateData)?;
|
|
|
|
Ok(db_task.into())
|
|
}
|
|
|
|
fn finish_task(&self) -> Result<domain::Task, Error> {
|
|
let db_task = self.get_current_task_opt_impl()?.ok_or(Error::NotFound)?;
|
|
|
|
self.conn
|
|
.execute(
|
|
"UPDATE tasks
|
|
SET current = false,
|
|
finished_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?1",
|
|
[&db_task.id],
|
|
)
|
|
.map_err(|_| Error::UpdateData)?;
|
|
|
|
Ok(db_task.into())
|
|
}
|
|
}
|
|
|
|
impl SqliteRepo {
|
|
fn get_task_by_id_impl(&self, id: i64) -> Result<Task, Error> {
|
|
let mut stmt = self
|
|
.conn
|
|
.prepare("SELECT * FROM active_tasks WHERE id = ?")
|
|
.map_err(|_| Error::PrepareQuery)?;
|
|
|
|
stmt.query_row([id], |row| Task::try_from(row))
|
|
.map_err(|_| Error::QueryData)
|
|
}
|
|
|
|
fn get_task_opt_impl(&self, idx: domain::TaskIdx) -> Result<Option<Task>, Error> {
|
|
let mut stmt = self
|
|
.conn
|
|
.prepare("SELECT * FROM active_tasks WHERE idx = ?")
|
|
.map_err(|_| Error::PrepareQuery)?;
|
|
|
|
stmt.query_row([idx as i64], |row| Task::try_from(row))
|
|
.optional()
|
|
.map_err(|_| Error::QueryData)
|
|
}
|
|
|
|
fn get_current_task_opt_impl(&self) -> Result<Option<Task>, Error> {
|
|
let mut stmt = self
|
|
.conn
|
|
.prepare("SELECT * FROM active_tasks WHERE current IS true")
|
|
.map_err(|_| Error::PrepareQuery)?;
|
|
|
|
let row = stmt
|
|
.query_row([], |row| Task::try_from(row))
|
|
.optional()
|
|
.map_err(|_| Error::QueryData)?;
|
|
|
|
Ok(row)
|
|
}
|
|
}
|
|
|
|
macro_rules! run_migration {
|
|
($this:ident, $ver:ident = $version:expr) => {
|
|
run_migration!($this, $ver = $version => concat!("migrations/", $version));
|
|
};
|
|
|
|
($this:ident, $ver:ident = $version:expr => $sql_name:expr) => {
|
|
$this
|
|
.conn
|
|
.execute_batch(&format!(
|
|
"BEGIN; {} COMMIT;",
|
|
include_str!(concat!("../../database/", $sql_name, ".sql"))
|
|
))
|
|
.map_err(|_| MigrationError::Upgrade)?;
|
|
|
|
$ver.replace($version);
|
|
};
|
|
}
|
|
|
|
// TODO: move migration to a separate repository.
|
|
#[derive(Debug)]
|
|
pub enum MigrationError {
|
|
Upgrade,
|
|
DeleteInfo,
|
|
InsertInfo,
|
|
}
|
|
|
|
impl std::fmt::Display for MigrationError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
MigrationError::Upgrade => f.write_str("Cannot upgrade the migration to a new version"),
|
|
MigrationError::DeleteInfo => {
|
|
f.write_str("Cannot delete the tas info from the database")
|
|
}
|
|
MigrationError::InsertInfo => {
|
|
f.write_str("Cannot insert a new tas information to the database")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for MigrationError {}
|
|
|
|
const LATEST_VERSION: i64 = 202208201623;
|
|
|
|
impl SqliteRepo {
|
|
pub fn upgrade(&self) -> Result<(), MigrationError> {
|
|
let mut version = self.version();
|
|
match version {
|
|
Some(LATEST_VERSION) => return Ok(()),
|
|
None => {
|
|
run_migration!(self, version = LATEST_VERSION => "schema");
|
|
}
|
|
Some(v) => {
|
|
if v == 202208162308 {
|
|
run_migration!(self, version = 202208201623);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.conn
|
|
.execute("DELETE FROM _tas_info", [])
|
|
.map_err(|_| MigrationError::DeleteInfo)?;
|
|
self.conn
|
|
.execute(
|
|
"INSERT INTO _tas_info (version) VALUES (?)",
|
|
[version.unwrap()],
|
|
)
|
|
.map_err(|_| MigrationError::InsertInfo)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn version(&self) -> Option<i64> {
|
|
self.conn
|
|
.query_row("SELECT version FROM _tas_info", [], |r| r.get("version"))
|
|
.unwrap_or_default()
|
|
}
|
|
}
|