536 lines
18 KiB
Rust
536 lines
18 KiB
Rust
use migration::DbErr;
|
|
use sea_orm::{query::*, ActiveModelTrait, ColumnTrait, DbConn, EntityTrait, Set, ModelTrait};
|
|
|
|
use crate::shared::errors::CustomError;
|
|
use crate::shared::responses::CustomResponse;
|
|
use entity::todo;
|
|
|
|
pub async fn find_todos(
|
|
conn: &DbConn,
|
|
query_string: Option<String>,
|
|
items_per_page: Option<usize>,
|
|
page_num: Option<usize>,
|
|
) -> Result<Vec<todo::Model>, CustomError> {
|
|
let mut stmt = todo::Entity::find();
|
|
|
|
if let Some(query_string) = query_string {
|
|
stmt = stmt.filter(todo::Column::Title.contains(query_string.as_str()));
|
|
}
|
|
|
|
let results = stmt
|
|
.order_by_desc(todo::Column::UpdatedAt)
|
|
.paginate(conn, items_per_page.unwrap_or(10))
|
|
.fetch_page(page_num.unwrap_or(0))
|
|
.await
|
|
.map_err(|_| CustomError::ServerError)?;
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
pub async fn find_todo_by_id(conn: &DbConn, id: usize) -> Result<todo::Model, CustomError> {
|
|
let result = todo::Entity::find_by_id(id as i32)
|
|
.one(conn)
|
|
.await
|
|
.map_err(|_| CustomError::ServerError)?;
|
|
|
|
if result.is_none() {
|
|
return Err(CustomError::NotFound);
|
|
}
|
|
|
|
Ok(result.unwrap())
|
|
}
|
|
|
|
pub async fn insert_todo(
|
|
conn: &DbConn,
|
|
title: &str,
|
|
description: &str,
|
|
done: bool,
|
|
) -> Result<CustomResponse, CustomError> {
|
|
let res = todo::Entity::insert(todo::ActiveModel {
|
|
title: Set(title.to_string()),
|
|
description: Set(description.to_string()),
|
|
done: Set(done),
|
|
..Default::default()
|
|
})
|
|
.exec(conn)
|
|
.await
|
|
.map_err(|e| {
|
|
match e {
|
|
DbErr::Query(..) => CustomError::Conflict,
|
|
_ => CustomError::ServerError,
|
|
}
|
|
})?;
|
|
|
|
Ok(CustomResponse::Created {id: res.last_insert_id as usize})
|
|
}
|
|
|
|
pub async fn update_todo_by_id(
|
|
conn: &DbConn,
|
|
id: usize,
|
|
title: Option<String>,
|
|
description: Option<String>,
|
|
done: Option<bool>,
|
|
) -> Result<CustomResponse, CustomError> {
|
|
let todo = todo::Entity::find_by_id(id as i32)
|
|
.one(conn)
|
|
.await
|
|
.map_err(|_| CustomError::ServerError)?;
|
|
|
|
if todo.is_none() {
|
|
return Err(CustomError::NotFound);
|
|
}
|
|
|
|
let mut todo: todo::ActiveModel = todo.unwrap().into();
|
|
|
|
if let Some(title) = title {
|
|
todo.title = Set(title);
|
|
}
|
|
|
|
if let Some(description) = description {
|
|
todo.description = Set(description);
|
|
}
|
|
|
|
if let Some(done) = done {
|
|
todo.done = Set(done);
|
|
}
|
|
|
|
todo.update(conn)
|
|
.await
|
|
.map_err(|e| {
|
|
println!("Updated error: {:?}", e);
|
|
CustomError::ServerError
|
|
})?;
|
|
|
|
Ok(CustomResponse::Updated { id })
|
|
}
|
|
|
|
pub async fn delete_todo_by_id(conn: &DbConn, id: usize) -> Result<CustomResponse, CustomError> {
|
|
let found: todo::Model = find_todo_by_id(conn, id).await?;
|
|
|
|
found.delete(conn).await.map_err(|_| CustomError::ServerError)?;
|
|
|
|
Ok(CustomResponse::Deleted { id })
|
|
}
|
|
|
|
pub async fn bulk_delete_todos_by_ids(
|
|
conn: &DbConn,
|
|
ids: Vec<usize>,
|
|
) -> Result<CustomResponse, CustomError> {
|
|
let txn = conn.begin().await.map_err(|e| {
|
|
println!("Transaction error: {:?}", e);
|
|
CustomError::ServerError
|
|
})?;
|
|
|
|
for id in ids.clone() {
|
|
let found: todo::Model = find_todo_by_id(conn, id).await?;
|
|
found.delete(&txn).await.map_err(|_| CustomError::ServerError)?;
|
|
}
|
|
|
|
txn.commit().await.map_err(|e| {
|
|
println!("Transaction error: {:?}", e);
|
|
CustomError::ServerError
|
|
})?;
|
|
|
|
Ok(CustomResponse::BulkDeleted { ids })
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use chrono::{FixedOffset, TimeZone};
|
|
use entity::todo;
|
|
use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction};
|
|
|
|
#[async_std::test]
|
|
async fn test_find_todos() -> Result<(), CustomError> {
|
|
let datetime = FixedOffset::east(0).ymd(2016, 11, 08).and_hms(0, 0, 0);
|
|
let db = MockDatabase::new(DatabaseBackend::Postgres)
|
|
.append_query_results(vec![
|
|
// First query result
|
|
vec![
|
|
todo::Model {
|
|
id: 1,
|
|
title: "Todo 1".to_owned(),
|
|
description: "Todo 1 description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
},
|
|
todo::Model {
|
|
id: 2,
|
|
title: "Todo 2".to_owned(),
|
|
description: "Todo 2 description".to_owned(),
|
|
done: true,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
},
|
|
],
|
|
// Second query result
|
|
vec![
|
|
todo::Model {
|
|
id: 1,
|
|
title: "Apple pie".to_owned(),
|
|
description: "description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
},
|
|
todo::Model {
|
|
id: 3,
|
|
title: "Apple pizza".to_owned(),
|
|
description: "description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
},
|
|
],
|
|
// Third query result
|
|
vec![todo::Model {
|
|
id: 1,
|
|
title: "Apple pie".to_owned(),
|
|
description: "description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}],
|
|
])
|
|
.into_connection();
|
|
|
|
// testing find_todos with no query string
|
|
assert_eq!(
|
|
find_todos(&db, None, None, None).await?,
|
|
vec![
|
|
todo::Model {
|
|
id: 1,
|
|
title: "Todo 1".to_owned(),
|
|
description: "Todo 1 description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
},
|
|
todo::Model {
|
|
id: 2,
|
|
title: "Todo 2".to_owned(),
|
|
description: "Todo 2 description".to_owned(),
|
|
done: true,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}
|
|
]
|
|
);
|
|
|
|
// testing find_todos with query string
|
|
assert_eq!(
|
|
find_todos(&db, Some("Apple".to_owned()), None, None).await?,
|
|
vec![
|
|
todo::Model {
|
|
id: 1,
|
|
title: "Apple pie".to_owned(),
|
|
description: "description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
},
|
|
todo::Model {
|
|
id: 3,
|
|
title: "Apple pizza".to_owned(),
|
|
description: "description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}
|
|
]
|
|
);
|
|
|
|
// testing find_todos with query string and pagination
|
|
assert_eq!(
|
|
find_todos(&db, Some("Apple".to_owned()), Some(5), Some(1)).await?,
|
|
vec![todo::Model {
|
|
id: 1,
|
|
title: "Apple pie".to_owned(),
|
|
description: "description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}]
|
|
);
|
|
|
|
// Checking transaction log
|
|
assert_eq!(
|
|
db.into_transaction_log(),
|
|
vec![
|
|
Transaction::from_sql_and_values(
|
|
DatabaseBackend::Postgres,
|
|
r#"SELECT "todo"."id", "todo"."title", "todo"."description", "todo"."done", "todo"."created_at", "todo"."updated_at" FROM "todo" ORDER BY "todo"."updated_at" DESC LIMIT $1 OFFSET $2"#,
|
|
vec![10u64.into(), 0u64.into()]
|
|
),
|
|
Transaction::from_sql_and_values(
|
|
DatabaseBackend::Postgres,
|
|
r#"SELECT "todo"."id", "todo"."title", "todo"."description", "todo"."done", "todo"."created_at", "todo"."updated_at" FROM "todo" WHERE "todo"."title" LIKE $1 ORDER BY "todo"."updated_at" DESC LIMIT $2 OFFSET $3"#,
|
|
vec!["%Apple%".into(), 10u64.into(), 0u64.into()]
|
|
),
|
|
Transaction::from_sql_and_values(
|
|
DatabaseBackend::Postgres,
|
|
r#"SELECT "todo"."id", "todo"."title", "todo"."description", "todo"."done", "todo"."created_at", "todo"."updated_at" FROM "todo" WHERE "todo"."title" LIKE $1 ORDER BY "todo"."updated_at" DESC LIMIT $2 OFFSET $3"#,
|
|
vec!["%Apple%".into(), 5u64.into(), 5u64.into()]
|
|
),
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn test_find_todo_by_id() -> Result<(), CustomError> {
|
|
let datetime = FixedOffset::east(0).ymd(2016, 11, 08).and_hms(0, 0, 0);
|
|
let db = MockDatabase::new(DatabaseBackend::Postgres)
|
|
.append_query_results(vec![
|
|
// First query result
|
|
vec![todo::Model {
|
|
id: 1,
|
|
title: "Todo 1".to_owned(),
|
|
description: "Todo 1 description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}],
|
|
// Second query result
|
|
vec![],
|
|
])
|
|
.into_connection();
|
|
|
|
// testing find_todo_by_id with existing id
|
|
assert_eq!(
|
|
find_todo_by_id(&db, 1).await?,
|
|
todo::Model {
|
|
id: 1,
|
|
title: "Todo 1".to_owned(),
|
|
description: "Todo 1 description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}
|
|
);
|
|
|
|
// testing find_todo_by_id with non-existing id
|
|
assert_eq!(
|
|
find_todo_by_id(&db, 2).await.unwrap_err(),
|
|
CustomError::NotFound
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn test_insert_todo() -> Result<(), CustomError> {
|
|
let title = "Test Title";
|
|
let description = "Test Description";
|
|
let done = false;
|
|
let datetime = FixedOffset::east(0).ymd(2016, 11, 08).and_hms(0, 0, 0);
|
|
|
|
let db = MockDatabase::new(DatabaseBackend::Postgres)
|
|
.append_query_results(vec![vec![todo::Model {
|
|
id: 15,
|
|
title: title.to_owned(),
|
|
description: description.to_owned(),
|
|
done,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}]])
|
|
.append_exec_results(vec![MockExecResult {
|
|
last_insert_id: 15,
|
|
rows_affected: 1,
|
|
}])
|
|
.into_connection();
|
|
|
|
insert_todo(&db, title, description, done).await?;
|
|
|
|
assert_eq!(
|
|
db.into_transaction_log(),
|
|
vec![Transaction::from_sql_and_values(
|
|
DatabaseBackend::Postgres,
|
|
r#"INSERT INTO "todo" ("title", "description", "done") VALUES ($1, $2, $3) RETURNING "id""#,
|
|
vec![title.into(), description.into(), done.into()]
|
|
)]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn test_update_todo_by_id() -> Result<(), CustomError> {
|
|
let id = 1;
|
|
let old_title = "Old Title";
|
|
let title = "Test Title";
|
|
let description = "Test Description";
|
|
let done = false;
|
|
let datetime = FixedOffset::east(0).ymd(2016, 11, 08).and_hms(0, 0, 0);
|
|
|
|
let db = MockDatabase::new(DatabaseBackend::Postgres)
|
|
.append_query_results(vec![
|
|
// First query result
|
|
vec![todo::Model {
|
|
id,
|
|
title: old_title.to_owned(),
|
|
description: description.to_owned(),
|
|
done,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}],
|
|
// Second query result
|
|
vec![todo::Model {
|
|
id,
|
|
title: old_title.to_owned(),
|
|
description: description.to_owned(),
|
|
done,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}],
|
|
// Third query result
|
|
vec![],
|
|
])
|
|
.append_exec_results(vec![MockExecResult {
|
|
last_insert_id: 15,
|
|
rows_affected: 1,
|
|
}])
|
|
.into_connection();
|
|
|
|
// testing update_todo_by_id with existing id
|
|
update_todo_by_id(
|
|
&db,
|
|
id.try_into().unwrap(),
|
|
Some(title.to_owned()),
|
|
Some(description.to_owned()),
|
|
Some(done),
|
|
)
|
|
.await?;
|
|
|
|
// testing update_todo_by_id with non-existing id
|
|
assert_eq!(
|
|
update_todo_by_id(
|
|
&db,
|
|
2,
|
|
Some(title.to_owned()),
|
|
Some(description.to_owned()),
|
|
Some(done)
|
|
)
|
|
.await
|
|
.unwrap_err(),
|
|
CustomError::NotFound
|
|
);
|
|
|
|
assert_eq!(
|
|
db.into_transaction_log()[..2],
|
|
vec![
|
|
Transaction::from_sql_and_values(
|
|
DatabaseBackend::Postgres,
|
|
r#"SELECT "todo"."id", "todo"."title", "todo"."description", "todo"."done", "todo"."created_at", "todo"."updated_at" FROM "todo" WHERE "todo"."id" = $1 LIMIT $2"#,
|
|
vec![id.into(), 1u64.into()]
|
|
),
|
|
Transaction::from_sql_and_values(
|
|
DatabaseBackend::Postgres,
|
|
r#"UPDATE "todo" SET "title" = $1, "description" = $2, "done" = $3 WHERE "todo"."id" = $4 RETURNING "id", "title", "description", "done", "created_at", "updated_at""#,
|
|
vec![title.into(), description.into(), done.into(), id.into()]
|
|
),
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn test_delete_todo_by_id() -> Result<(), CustomError> {
|
|
let id = 1;
|
|
let datetime = FixedOffset::east(0).ymd(2016, 11, 08).and_hms(0, 0, 0);
|
|
|
|
let db = MockDatabase::new(DatabaseBackend::Postgres)
|
|
.append_query_results(vec![
|
|
vec![todo::Model {
|
|
id,
|
|
title: "Todo 1".to_owned(),
|
|
description: "Todo 1 description".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}],
|
|
vec![],
|
|
])
|
|
.append_exec_results(vec![MockExecResult {
|
|
last_insert_id: 1,
|
|
rows_affected: 1,
|
|
}])
|
|
.append_exec_results(vec![])
|
|
.into_connection();
|
|
// testing delete_todo_by_id with existing id
|
|
delete_todo_by_id(&db, id.try_into().unwrap()).await?;
|
|
// testing delete_todo_by_id with non-existing id
|
|
assert_eq!(
|
|
delete_todo_by_id(&db, 2).await.unwrap_err(),
|
|
CustomError::NotFound
|
|
);
|
|
assert_eq!(
|
|
db.into_transaction_log()[1..2],
|
|
vec![Transaction::from_sql_and_values(
|
|
DatabaseBackend::Postgres,
|
|
r#"DELETE FROM "todo" WHERE "todo"."id" = $1"#,
|
|
vec![id.into()]
|
|
)]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[async_std::test]
|
|
async fn test_bulk_delete_todos() -> Result<(), CustomError> {
|
|
let datetime = FixedOffset::east(0).ymd(2016, 11, 08).and_hms(0, 0, 0);
|
|
let db = MockDatabase::new(DatabaseBackend::Postgres)
|
|
.append_query_results(vec![
|
|
// First query result
|
|
vec![todo::Model {
|
|
id: 1,
|
|
title: "test1".to_owned(),
|
|
description: "test1".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}],
|
|
// Second query result
|
|
vec![todo::Model {
|
|
id: 2,
|
|
title: "test2".to_owned(),
|
|
description: "test2".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}],
|
|
// Third query result
|
|
vec![todo::Model {
|
|
id: 3,
|
|
title: "test3".to_owned(),
|
|
description: "test3".to_owned(),
|
|
done: false,
|
|
created_at: datetime,
|
|
updated_at: datetime,
|
|
}],
|
|
])
|
|
.append_exec_results(vec![MockExecResult {
|
|
last_insert_id: 1,
|
|
rows_affected: 1,
|
|
}])
|
|
.append_exec_results(vec![MockExecResult {
|
|
last_insert_id: 2,
|
|
rows_affected: 2,
|
|
}])
|
|
.append_exec_results(vec![MockExecResult {
|
|
last_insert_id: 3,
|
|
rows_affected: 3,
|
|
}])
|
|
.into_connection();
|
|
bulk_delete_todos_by_ids(&db, vec![1, 2, 3]).await?;
|
|
|
|
// skipped assertion as there is no non-trivial way to test a transaction with many statements
|
|
|
|
Ok(())
|
|
}
|
|
}
|