initial commit

This commit is contained in:
Georg Bauer
2026-07-24 11:27:25 +02:00
commit 2e1de825ab
23 changed files with 6169 additions and 0 deletions

163
src/database.rs Normal file
View File

@@ -0,0 +1,163 @@
use diesel::prelude::*;
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use crate::schema::{projects, sessions};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = projects)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Project {
pub id: i32,
pub name: String,
pub path: String,
}
#[derive(Insertable)]
#[diesel(table_name = projects)]
struct NewProject<'a> {
name: &'a str,
path: &'a str,
}
#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(belongs_to(Project))]
#[diesel(table_name = sessions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Session {
pub id: i32,
pub project_id: i32,
pub title: String,
}
#[derive(Insertable)]
#[diesel(table_name = sessions)]
struct NewSession<'a> {
project_id: i32,
title: &'a str,
}
#[derive(Debug)]
pub struct ProjectWithSessions {
pub project: Project,
pub sessions: Vec<Session>,
}
pub struct Database {
connection: SqliteConnection,
}
impl Database {
pub fn open(path: &Path) -> Result<Self, String> {
let parent = path
.parent()
.ok_or_else(|| "database path has no parent directory".to_owned())?;
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
let url = path
.to_str()
.ok_or_else(|| "database path is not valid UTF-8".to_owned())?;
let mut connection = SqliteConnection::establish(url).map_err(|error| error.to_string())?;
connection
.run_pending_migrations(MIGRATIONS)
.map_err(|error| error.to_string())?;
Ok(Self { connection })
}
pub fn load_projects(&mut self) -> Result<Vec<ProjectWithSessions>, String> {
let project_rows = projects::table
.order(projects::id.asc())
.select(Project::as_select())
.load(&mut self.connection)
.map_err(|error| error.to_string())?;
let session_rows = sessions::table
.order(sessions::id.asc())
.select(Session::as_select())
.load::<Session>(&mut self.connection)
.map_err(|error| error.to_string())?;
let mut sessions_by_project: HashMap<i32, Vec<Session>> = HashMap::new();
for session in session_rows {
sessions_by_project
.entry(session.project_id)
.or_default()
.push(session);
}
Ok(project_rows
.into_iter()
.map(|project| ProjectWithSessions {
sessions: sessions_by_project.remove(&project.id).unwrap_or_default(),
project,
})
.collect())
}
pub fn create_project(&mut self, name: &str, path: &str) -> Result<Project, String> {
diesel::insert_into(projects::table)
.values(NewProject { name, path })
.returning(Project::as_returning())
.get_result(&mut self.connection)
.map_err(|error| error.to_string())
}
pub fn delete_project(&mut self, project_id: i32) -> Result<(), String> {
self.connection
.transaction(|connection| {
diesel::delete(sessions::table.filter(sessions::project_id.eq(project_id)))
.execute(connection)?;
diesel::delete(projects::table.find(project_id)).execute(connection)?;
Ok(())
})
.map_err(|error: diesel::result::Error| error.to_string())
}
pub fn create_session(&mut self, project_id: i32, title: &str) -> Result<Session, String> {
diesel::insert_into(sessions::table)
.values(NewSession { project_id, title })
.returning(Session::as_returning())
.get_result(&mut self.connection)
.map_err(|error| error.to_string())
}
pub fn delete_session(&mut self, session_id: i32) -> Result<(), String> {
diesel::delete(sessions::table.find(session_id))
.execute(&mut self.connection)
.map(|_| ())
.map_err(|error| error.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn migrations_and_crud_work() {
let id = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!("ds4-server-{id}.sqlite3"));
let mut database = Database::open(&path).unwrap();
let project = database.create_project("DS4", "/tmp/ds4").unwrap();
let first = database
.create_session(project.id, "First session")
.unwrap();
database
.create_session(project.id, "Second session")
.unwrap();
database.delete_session(first.id).unwrap();
let loaded = database.load_projects().unwrap();
assert_eq!(loaded[0].sessions[0].title, "Second session");
database.delete_project(project.id).unwrap();
assert!(database.load_projects().unwrap().is_empty());
drop(database);
fs::remove_file(path).unwrap();
}
}