diff --git a/backend/.gitignore b/backend/.gitignore index 35b859b..2430586 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,5 +1,3 @@ -target +/target *.db -*.db-shm -*.db-wal diff --git a/backend/Cargo.lock b/backend/Cargo.lock index c83be9d..b255661 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -119,47 +119,12 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-extra" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" -dependencies = [ - "axum", - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "serde", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "backend" version = "0.1.0" dependencies = [ "anyhow", "axum", - "axum-extra", - "axum-macros", "chrono", "serde", "sqlx", @@ -257,7 +222,6 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "serde", "wasm-bindgen", "windows-link", ] @@ -1892,7 +1856,6 @@ checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", "js-sys", - "serde", "wasm-bindgen", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 661bddf..78e617e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -6,13 +6,11 @@ edition = "2024" [dependencies] anyhow = "1.0.99" axum = "0.8.4" -axum-extra = "0.10.1" -axum-macros = "0.5.0" -chrono = { version = "0.4.41", features = ["serde"] } +chrono = "0.4.41" serde = "1.0.219" sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "uuid", "chrono"] } tokio = { version = "1.47.1", features = ["rt-multi-thread", "tracing"] } tracing = "0.1.41" tracing-subscriber = "0.3.19" -uuid = { version = "1.18.0", features = ["serde", "v4"] } +uuid = { version = "1.18.0", features = ["v4"] } diff --git a/backend/src/database/connection.rs b/backend/src/database/connection.rs index 5cf2e21..f2183ff 100644 --- a/backend/src/database/connection.rs +++ b/backend/src/database/connection.rs @@ -2,10 +2,20 @@ use anyhow::Result; use sqlx::{SqlitePool, sqlite::SqliteConnectOptions}; use std::str::FromStr; +/// Database configuration pub struct DatabaseConfig { pub database_url: String, } +impl Default for DatabaseConfig { + fn default() -> Self { + Self { + database_url: "sqlite:local.db".to_string(), + } + } +} + +/// Create a SQLx connection pool pub async fn create_pool(config: &DatabaseConfig) -> Result { let options = SqliteConnectOptions::from_str(&config.database_url)?.create_if_missing(true); @@ -16,6 +26,12 @@ pub async fn create_pool(config: &DatabaseConfig) -> Result { Ok(pool) } +/// Initialize database with connection pool +pub async fn initialize_database() -> Result { + let config = DatabaseConfig::default(); + create_pool(&config).await +} + #[cfg(test)] pub async fn create_test_pool() -> Result { let options = SqliteConnectOptions::from_str("sqlite::memory:")?.create_if_missing(true); @@ -26,3 +42,4 @@ pub async fn create_test_pool() -> Result { Ok(pool) } + diff --git a/backend/src/main.rs b/backend/src/main.rs index 2c1f671..8148b77 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,23 +1,13 @@ use axum::{Router, routing::get}; -mod database; mod models; -mod services; +mod database; #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); - let binding = database::DatabaseConfig { - database_url: "sqlite:local.db".to_string(), - }; - let pool = database::create_pool(&binding) - .await - .expect("Failed to create database pool"); - let app = Router::new() - .route("/health", get(health)) - .nest("/api/tasks", services::create_task_router()) - .with_state(pool); + let app = Router::new().route("/health", get(health)); let addr = "127.0.0.1:3000"; diff --git a/backend/src/models/task.rs b/backend/src/models/task.rs index 685f11c..a633a1b 100644 --- a/backend/src/models/task.rs +++ b/backend/src/models/task.rs @@ -4,8 +4,6 @@ use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; use uuid::Uuid; -use crate::services::AppError; - #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, sqlx::Type)] #[serde(rename_all = "snake_case")] #[sqlx(rename_all = "lowercase")] @@ -15,7 +13,7 @@ pub enum TaskStatus { Backlog, } -#[derive(sqlx::FromRow, Serialize)] +#[derive(sqlx::FromRow)] pub struct TaskModel { pub id: Uuid, pub title: String, @@ -48,19 +46,15 @@ impl TaskModel { Ok(result) } - pub async fn get_by_id(pool: &SqlitePool, id: Uuid) -> Result { - let model = sqlx::query_as("SELECT * FROM tasks WHERE id = $1") + pub async fn get_by_id(pool: &SqlitePool, id: Uuid) -> Result { + let result = sqlx::query_as("SELECT * FROM tasks WHERE id = $1") .bind(id) .fetch_one(pool) - .await - .map_err(|err| match err { - sqlx::Error::RowNotFound => AppError::NotFound, - e => anyhow::Error::from(e).into(), - })?; - Ok(model) + .await?; + Ok(result) } - pub async fn update(self, pool: &SqlitePool) -> Result { + pub async fn update(self, pool: &SqlitePool) -> Result { let now: DateTime = Utc::now(); let _ = sqlx::query( diff --git a/backend/src/services/mod.rs b/backend/src/services/mod.rs deleted file mode 100644 index 50b7492..0000000 --- a/backend/src/services/mod.rs +++ /dev/null @@ -1,82 +0,0 @@ -mod tasks; - -use axum::{ - Json, - extract::rejection::{JsonRejection, PathRejection}, - http::StatusCode, - response::IntoResponse, -}; - -// AppError -// Borrowed from example: https://github.com/tokio-rs/axum/blob/axum-v0.8.4/examples/anyhow-error-response/src/main.rs -pub enum AppError { - InternalError(anyhow::Error), - JsonExtractError(JsonRejection), - PathError(PathRejection), - NotFound, -} - -#[derive(Serialize)] -pub struct ErrorJson { - error: String, -} - -impl IntoResponse for AppError { - fn into_response(self) -> axum::response::Response { - match self { - Self::InternalError(anyhow_error) => ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong {}", anyhow_error), - ) - .into_response(), - Self::JsonExtractError(rej) => ( - StatusCode::UNPROCESSABLE_ENTITY, - Json(ErrorJson { - error: rej.status().to_string(), - }), - ) - .into_response(), - Self::PathError(rej) => ( - StatusCode::BAD_REQUEST, - Json(ErrorJson { - error: rej.status().to_string(), - }), - ) - .into_response(), - Self::NotFound => ( - StatusCode::NOT_FOUND, - Json(ErrorJson { - error: "Requeted Entity Not Found".to_string(), - }), - ) - .into_response(), - } - } -} - -impl From for AppError { - fn from(value: JsonRejection) -> Self { - Self::JsonExtractError(value) - } -} - -impl From for AppError { - fn from(value: PathRejection) -> Self { - Self::PathError(value) - } -} - -impl From for AppError { - fn from(err: anyhow::Error) -> Self { - Self::InternalError(err.into()) - } -} - -impl From for AppError { - fn from(err: sqlx::Error) -> Self { - Self::InternalError(err.into()) - } -} - -use serde::Serialize; -pub use tasks::*; diff --git a/backend/src/services/tasks.rs b/backend/src/services/tasks.rs deleted file mode 100644 index ebf7f3e..0000000 --- a/backend/src/services/tasks.rs +++ /dev/null @@ -1,47 +0,0 @@ -use axum::{ - Json, Router, - extract::{Path, State}, - http::StatusCode, - routing::{get, post}, -}; -use axum_extra::extract::WithRejection; -use axum_macros::debug_handler; -use serde::Deserialize; -use sqlx::{Pool, Sqlite}; -use uuid::Uuid; - -use crate::models::TaskModel; - -use super::AppError; - -pub fn create_task_router() -> Router> { - Router::new() - .route("/", post(create_task)) - .route("/{task_id}", get(get_task)) -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -pub struct CreateTaskRequest { - title: String, - description: Option, -} - -#[debug_handler] -pub async fn create_task( - State(pool): State>, - WithRejection(Json(input), _): WithRejection, AppError>, -) -> Result<(StatusCode, Json), AppError> { - let model = TaskModel::insert(&pool, &input.title, input.description.as_deref()).await?; - - Ok((StatusCode::CREATED, Json(model))) -} - -pub async fn get_task( - State(pool): State>, - WithRejection(Path(task_id), _): WithRejection, AppError>, -) -> Result<(StatusCode, Json), AppError> { - let model = TaskModel::get_by_id(&pool, task_id).await?; - - Ok((StatusCode::OK, Json(model))) -} diff --git a/backend/tests/api/tasks.hurl b/backend/tests/api/tasks.hurl deleted file mode 100644 index 20e8806..0000000 --- a/backend/tests/api/tasks.hurl +++ /dev/null @@ -1,94 +0,0 @@ -# Task API Tests - -# Test: Create a new task (POST /api/tasks) -POST {{host}}/api/tasks -Content-Type: application/json -{ - "title": "Test task", - "description": "A test task for the API" -} - -HTTP 201 -[Captures] -task_id: jsonpath "$.id" -[Asserts] -jsonpath "$.title" == "Test task" -jsonpath "$.description" == "A test task for the API" -jsonpath "$.status" == "todo" -jsonpath "$.id" exists -jsonpath "$.created_at" exists -jsonpath "$.updated_at" exists - -# Test: Create a minimal task (only required fields) -POST {{host}}/api/tasks -Content-Type: application/json -{ - "title": "Minimal task" -} - -HTTP 201 -[Captures] -minimal_task_id: jsonpath "$.id" -[Asserts] -jsonpath "$.title" == "Minimal task" -jsonpath "$.status" == "todo" -jsonpath "$.description" == null - -# Test: Create task with invalid data (missing title) -POST {{host}}/api/tasks -Content-Type: application/json -{ - "description": "Task without title" -} - -HTTP 422 -[Asserts] -jsonpath "$.error" exists - -# Test: Create task with invalid priority -POST {{host}}/api/tasks -Content-Type: application/json -{ - "title": "Invalid priority task", - "priority": "This field does not exist." -} - -HTTP 422 -[Asserts] -jsonpath "$.error" exists - -# Test: Get a specific task by ID (GET /api/tasks/{id}) -GET {{host}}/api/tasks/{{task_id}} - -HTTP 200 -[Asserts] -jsonpath "$.id" == "{{task_id}}" -jsonpath "$.title" == "Test task" -jsonpath "$.description" == "A test task for the API" -jsonpath "$.status" == "todo" -jsonpath "$.created_at" exists -jsonpath "$.updated_at" exists - -# Test: Get a minimal task by ID -GET {{host}}/api/tasks/{{minimal_task_id}} - -HTTP 200 -[Asserts] -jsonpath "$.id" == "{{minimal_task_id}}" -jsonpath "$.title" == "Minimal task" -jsonpath "$.status" == "todo" -jsonpath "$.description" == null - -# Test: Get non-existent task -GET {{host}}/api/tasks/00000000-0000-0000-0000-000000000000 - -HTTP 404 -[Asserts] -jsonpath "$.error" exists - -# Test: Get task with invalid UUID format -GET {{host}}/api/tasks/invalid-uuid - -HTTP 400 -[Asserts] -jsonpath "$.error" exists diff --git a/justfile b/justfile index ed86590..93b00fa 100644 --- a/justfile +++ b/justfile @@ -33,18 +33,3 @@ migrate: migrate-revert: sqlx migrate revert -test-integration: - #!/usr/bin/env bash - set -e - - cargo run & - SERVER_PID=$! - - trap 'echo "Stopping server..."; kill -TERM $SERVER_PID 2>/dev/null || true; wait $SERVER_PID 2>/dev/null || true' EXIT - - echo "Waiting for server to start..." - printf 'GET http://localhost:3000/health\nHTTP 200' | hurl --retry 30 > /dev/null - - echo "Running integration tests..." - hurl --test --error-format long --variable host=http://localhost:3000 tests/api/*.hurl -