implement repo and add some tests
This commit is contained in:
parent
0384364579
commit
851c74cd2d
90
Cargo.lock
generated
90
Cargo.lock
generated
@ -202,6 +202,12 @@ version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.9.0"
|
||||
@ -219,6 +225,8 @@ dependencies = [
|
||||
"futures",
|
||||
"futures-util",
|
||||
"problem_details",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rusqlite",
|
||||
"rusqlite_migration",
|
||||
"serde",
|
||||
@ -227,6 +235,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1180,6 +1189,15 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "problem_details"
|
||||
version = "0.7.0"
|
||||
@ -1224,6 +1242,58 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r2d2"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
|
||||
dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"scheduled-thread-pool",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r2d2_sqlite"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb14dba8247a6a15b7fdbc7d389e2e6f03ee9f184f87117706d509c092dfe846"
|
||||
dependencies = [
|
||||
"r2d2",
|
||||
"rusqlite",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.8"
|
||||
@ -1361,6 +1431,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scheduled-thread-pool"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@ -1862,6 +1941,16 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
@ -2213,6 +2302,7 @@ version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
|
||||
@ -23,3 +23,6 @@ problem_details = { version = "0.7.0", features = ["axum"] }
|
||||
# database
|
||||
rusqlite = { version = "0.32.1", features = ["bundled"] }
|
||||
rusqlite_migration = "1.3.1"
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_sqlite = "0.25.0"
|
||||
uuid = { version = "1.12.1", features = ["v4"] }
|
||||
|
||||
@ -4,12 +4,29 @@ use problem_details::ProblemDetails;
|
||||
|
||||
pub trait Repository {
|
||||
fn migrate(&self) -> impl std::future::Future<Output = Result<(), ProblemDetails>> + Send;
|
||||
fn get_or_create_user(&self, id: String) -> impl std::future::Future<Output = Result<UserModel, ProblemDetails>> + Send;
|
||||
fn create_file(&self, file: File) -> impl std::future::Future<Output = Result<FileModel, ProblemDetails>> + Send;
|
||||
fn get_user(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> impl std::future::Future<Output = Result<Option<UserModel>, ProblemDetails>> + Send;
|
||||
fn create_user(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> impl std::future::Future<Output = Result<UserModel, ProblemDetails>> + Send;
|
||||
fn create_file(
|
||||
&self,
|
||||
file: File,
|
||||
) -> impl std::future::Future<Output = Result<FileModel, ProblemDetails>> + Send;
|
||||
|
||||
fn bump_version(&self, file_model: FileModel) -> impl std::future::Future<Output = Result<(), ProblemDetails>> + Send;
|
||||
fn bump_version(
|
||||
&self,
|
||||
file_model: FileModel,
|
||||
) -> impl std::future::Future<Output = Result<(), ProblemDetails>> + Send;
|
||||
|
||||
fn get_file(&self, user_id: String, path: String) -> impl std::future::Future<Output = Result<FileModel, ProblemDetails>> + Send;
|
||||
fn get_file(
|
||||
&self,
|
||||
user_id: &str,
|
||||
path: &str,
|
||||
) -> impl std::future::Future<Output = Result<Option<FileModel>, ProblemDetails>> + Send;
|
||||
}
|
||||
|
||||
pub mod models {
|
||||
@ -28,7 +45,6 @@ pub mod models {
|
||||
pub mod insert {
|
||||
pub struct File {
|
||||
pub user_id: String,
|
||||
pub path: String,
|
||||
pub version: u64,
|
||||
pub path: String
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,33 @@
|
||||
use std::path::PathBuf;
|
||||
use crate::db::repository::insert::File;
|
||||
use crate::db::repository::models::{FileModel, UserModel};
|
||||
use crate::db::repository::Repository;
|
||||
use crate::errors::to_internal_error;
|
||||
use axum::http::StatusCode;
|
||||
use problem_details::ProblemDetails;
|
||||
use rusqlite::Connection;
|
||||
use r2d2::PooledConnection;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::{Connection, OptionalExtension};
|
||||
use rusqlite_migration::{Migrations, M};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::task::spawn_blocking;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Sqlite {
|
||||
connection: Arc<Mutex<Connection>>,
|
||||
pool: r2d2::Pool<SqliteConnectionManager>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Sqlite {
|
||||
pub fn from_path(path: &PathBuf) -> Sqlite {
|
||||
Sqlite {
|
||||
connection: Arc::new(Mutex::new(Connection::open(path).unwrap())),
|
||||
pool: r2d2::Pool::new(SqliteConnectionManager::file(path)).unwrap(),
|
||||
path: path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_connection(&self) -> PooledConnection<SqliteConnectionManager> {
|
||||
let pool = self.pool.clone();
|
||||
pool.get().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Repository for Sqlite {
|
||||
@ -29,51 +37,139 @@ impl Repository for Sqlite {
|
||||
M::up("CREATE TABLE files(id INTEGER PRIMARY KEY, user_id TEXT, file_path TEXT, version INTEGER, FOREIGN KEY(user_id) REFERENCES users(uuid));"),
|
||||
M::up("CREATE INDEX files_user_path_idx ON files(user_id, file_path);"),
|
||||
]);
|
||||
let connection = self.connection.clone();
|
||||
spawn_blocking(move || match connection.lock() {
|
||||
Ok(mut conn) => {
|
||||
migrations.to_latest(&mut conn).unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Failed to lock connection: {err}");
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(to_internal_error)
|
||||
let mut connection = Connection::open(&self.path).unwrap();
|
||||
migrations
|
||||
.to_latest(&mut connection)
|
||||
.map_err(to_internal_error)
|
||||
}
|
||||
|
||||
async fn get_or_create_user(&self, id: String) -> Result<UserModel, ProblemDetails> {
|
||||
todo!()
|
||||
async fn get_user(&self, id: &str) -> Result<Option<UserModel>, ProblemDetails> {
|
||||
let conn = self.get_connection();
|
||||
let mut statement = conn
|
||||
.prepare("SELECT uuid FROM users WHERE uuid = ?1")
|
||||
.unwrap();
|
||||
statement
|
||||
.query_row([id], |row| {
|
||||
Ok(UserModel {
|
||||
uuid: row.get(0).unwrap(),
|
||||
})
|
||||
})
|
||||
.optional()
|
||||
.map_err(to_internal_error)
|
||||
}
|
||||
|
||||
async fn create_user(&self, id: &str) -> Result<UserModel, ProblemDetails> {
|
||||
self.get_connection()
|
||||
.execute("INSERT INTO users (uuid) VALUES (?);", (id,))
|
||||
.unwrap();
|
||||
self.get_user(id).await.map(|option| option.unwrap())
|
||||
}
|
||||
|
||||
async fn create_file(&self, file: File) -> Result<FileModel, ProblemDetails> {
|
||||
todo!()
|
||||
self.get_connection()
|
||||
.execute(
|
||||
"INSERT INTO files (user_id, file_path, version) VALUES (?, ?, ?);",
|
||||
(&file.user_id, &file.path, 0),
|
||||
)
|
||||
.unwrap();
|
||||
self.get_file(&file.user_id, &file.path)
|
||||
.await
|
||||
.map(|option| option.unwrap())
|
||||
}
|
||||
|
||||
async fn bump_version(&self, file_model: FileModel) -> Result<(), ProblemDetails> {
|
||||
todo!()
|
||||
let conn = self.get_connection();
|
||||
let updated = conn
|
||||
.execute(
|
||||
"UPDATE files SET version = ?1 WHERE id = ?2 AND version = ?3",
|
||||
(
|
||||
file_model.version + 1,
|
||||
file_model.id,
|
||||
file_model.version,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
if updated != 1 {
|
||||
Err(ProblemDetails::from_status_code(StatusCode::CONFLICT)
|
||||
.with_detail("File version was updated by another application!"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_file(&self, user_id: String, path: String) -> Result<FileModel, ProblemDetails> {
|
||||
let conn = self.connection.clone();
|
||||
spawn_blocking(move || {
|
||||
match conn.lock() {
|
||||
Ok(connection) => {
|
||||
let mut statement = connection.prepare("SELECT id, user_id, path, version FROM files WHERE user_id = ?1 AND path = ?2").unwrap();
|
||||
statement.query_row([user_id, path], |row| {
|
||||
Ok(FileModel {
|
||||
id: row.get(0).unwrap(),
|
||||
user_id: row.get(1).unwrap(),
|
||||
path: row.get(2).unwrap(),
|
||||
version: row.get(3).unwrap(),
|
||||
})
|
||||
}).unwrap()
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Failed to lock connection: {err}");
|
||||
}
|
||||
}
|
||||
}).await
|
||||
async fn get_file(
|
||||
&self,
|
||||
user_id: &str,
|
||||
path: &str,
|
||||
) -> Result<Option<FileModel>, ProblemDetails> {
|
||||
let conn = self.get_connection();
|
||||
let mut statement = conn
|
||||
.prepare(
|
||||
"SELECT id, user_id, file_path, version FROM files WHERE user_id = ?1 AND file_path = ?2",
|
||||
)
|
||||
.unwrap();
|
||||
statement
|
||||
.query_row([user_id, path], |row| {
|
||||
Ok(FileModel {
|
||||
id: row.get(0).unwrap(),
|
||||
user_id: row.get(1).unwrap(),
|
||||
path: row.get(2).unwrap(),
|
||||
version: row.get(3).unwrap(),
|
||||
})
|
||||
})
|
||||
.optional()
|
||||
.map_err(to_internal_error)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::repository::{insert, Repository};
|
||||
use crate::db::sqlite::Sqlite;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
async fn create_repository() -> Sqlite {
|
||||
let sqlite = Sqlite::from_path(&PathBuf::from(
|
||||
"/tmp/".to_owned() + &*Uuid::new_v4().to_string(),
|
||||
));
|
||||
sqlite.migrate().await.unwrap();
|
||||
sqlite
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_file_creation() {
|
||||
let repo = create_repository().await;
|
||||
let user_id = Uuid::new_v4().to_string();
|
||||
repo.create_user(&user_id).await.unwrap();
|
||||
const FILE_PATH: &'static str = "test";
|
||||
let created = repo
|
||||
.create_file(insert::File {
|
||||
user_id: user_id.clone(),
|
||||
path: FILE_PATH.to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(user_id, created.user_id);
|
||||
assert_eq!(FILE_PATH, created.path);
|
||||
assert_eq!(0, created.version);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_file_version_bump() {
|
||||
let repo = create_repository().await;
|
||||
let user_id = Uuid::new_v4().to_string();
|
||||
repo.create_user(&user_id).await.unwrap();
|
||||
const FILE_PATH: &'static str = "test";
|
||||
let created = repo
|
||||
.create_file(insert::File {
|
||||
user_id: user_id.clone(),
|
||||
path: FILE_PATH.to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(repo.bump_version(created).await.is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user