From c55c5b3ccf9f06775fb00a2bcb2912f218691758 Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Tue, 7 May 2024 12:37:30 -0500 Subject: feat(api,wip): impl tasks CRUD endpoint --- migrations/20240417203222_create_task.down.sql | 1 + migrations/20240417203222_create_task.up.sql | 12 +++ src/api.rs | 1 + src/api/error.rs | 7 +- src/api/tasks.rs | 104 +++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 migrations/20240417203222_create_task.down.sql create mode 100644 migrations/20240417203222_create_task.up.sql create mode 100644 src/api/tasks.rs diff --git a/migrations/20240417203222_create_task.down.sql b/migrations/20240417203222_create_task.down.sql new file mode 100644 index 0000000..36cf80d --- /dev/null +++ b/migrations/20240417203222_create_task.down.sql @@ -0,0 +1 @@ +DROP TABLE task; diff --git a/migrations/20240417203222_create_task.up.sql b/migrations/20240417203222_create_task.up.sql new file mode 100644 index 0000000..fd8eb9c --- /dev/null +++ b/migrations/20240417203222_create_task.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE task ( + id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES user_(id) ON DELETE SET NULL, + title TEXT NOT NULL, + description TEXT, + remote BOOLEAN NOT NULL, + location TEXT, + start_at TIMESTAMP WITH TIME ZONE, + end_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); diff --git a/src/api.rs b/src/api.rs index a35fba5..6013a98 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,6 +2,7 @@ use crate::state::AppState; pub mod account; pub mod error; +pub mod tasks; pub mod users; pub fn router() -> axum::Router { diff --git a/src/api/error.rs b/src/api/error.rs index 3733d50..2af7228 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -9,6 +9,9 @@ pub enum Error { #[error("User not found")] UserNotFound, + #[error("Task not found")] + TaskNotFound, + #[error("Required header not found: {0}")] HeaderNotFound(axum::http::HeaderName), @@ -47,7 +50,9 @@ impl axum::response::IntoResponse for Error { use axum::http::StatusCode; let status = match self { - Self::RouteNotFound(_) | Self::UserNotFound => StatusCode::NOT_FOUND, + Self::RouteNotFound(_) | Self::UserNotFound | Self::TaskNotFound => { + StatusCode::NOT_FOUND + } Self::EmailExists => StatusCode::CONFLICT, Self::InvalidToken => StatusCode::UNAUTHORIZED, Self::HeaderNotFound(ref h) if h == AUTHORIZATION => StatusCode::UNAUTHORIZED, diff --git a/src/api/tasks.rs b/src/api/tasks.rs new file mode 100644 index 0000000..9a3e1ab --- /dev/null +++ b/src/api/tasks.rs @@ -0,0 +1,104 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, +}; +use axum_extra::routing::Resource; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::{auth::AccessClaims, state::AppState}; + +use super::error::Error; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Task { + pub id: Uuid, + pub user_id: Option, + pub title: String, + pub description: Option, + pub remote: bool, + pub location: Option, + pub start_at: Option, + pub end_at: Option, + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateTaskSchema { + pub user_id: Uuid, + pub title: String, + pub description: Option, + pub remote: bool, + pub location: Option, + pub start_at: Option, + pub end_at: Option, +} + +pub fn router() -> Resource { + Resource::named("tasks") + .create(create) + .show(show) + .destroy(destroy) +} + +pub async fn create( + State(pool): State, + _: AccessClaims, + Json(CreateTaskSchema { + user_id, + title, + description, + remote, + location, + start_at, + end_at, + }): Json, +) -> Result<(StatusCode, Json), Error> { + let task = sqlx::query_as!(Task, + "INSERT INTO task (user_id, title, description, remote, location, start_at, end_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *", + user_id, + title, + description, + remote, + location, + start_at, + end_at, + ) + .fetch_one(&pool) + .await?; + + Ok((StatusCode::CREATED, Json(task))) +} + +pub async fn show( + State(pool): State, + Path(uuid): Path, + _: AccessClaims, +) -> Result, Error> { + sqlx::query_as!(Task, "SELECT * FROM task WHERE id = $1 LIMIT 1", uuid) + .fetch_optional(&pool) + .await? + .ok_or_else(|| Error::TaskNotFound) + .map(Json) +} + +pub async fn destroy( + State(pool): State, + Path(uuid): Path, + AccessClaims { sub, .. }: AccessClaims, +) -> Result, Error> { + sqlx::query_as!( + Task, + "SELECT * FROM task WHERE id = $1 AND user_id = $2 LIMIT 1", + uuid, + sub + ) + .fetch_optional(&pool) + .await? + .ok_or_else(|| Error::TaskNotFound) + .map(Json) +} -- cgit v1.2.3-70-g09d2