feat: create a model for subtasks
This commit is contained in:
parent
e1c553c5f1
commit
6e0826c5ec
4
migrations/2024-09-08-083610_create_subtasks/down.sql
Normal file
4
migrations/2024-09-08-083610_create_subtasks/down.sql
Normal file
@ -0,0 +1,4 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS "subtasks";
|
15
migrations/2024-09-08-083610_create_subtasks/up.sql
Normal file
15
migrations/2024-09-08-083610_create_subtasks/up.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Your SQL goes here
|
||||
|
||||
|
||||
CREATE TABLE "subtasks"(
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"task_id" INT4 NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"is_completed" BOOL NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
SELECT diesel_manage_updated_at('subtasks');
|
||||
|
@ -2,3 +2,4 @@ pub(crate) mod error;
|
||||
pub(crate) mod error_vec;
|
||||
pub(crate) mod project_error;
|
||||
pub(crate) mod task_error;
|
||||
pub(crate) mod subtask_error;
|
||||
|
70
src/errors/subtask_error.rs
Normal file
70
src/errors/subtask_error.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use crate::errors::error::Error;
|
||||
use crate::errors::error_vec::ErrorVec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use validator::{ValidationErrors, ValidationErrorsKind};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum SubtaskError {
|
||||
TitleLengthInvalid,
|
||||
TaskNotFound,
|
||||
Error(Error),
|
||||
}
|
||||
|
||||
impl From<ValidationErrors> for ErrorVec<SubtaskError> {
|
||||
fn from(validation_errors: ValidationErrors) -> Self {
|
||||
validation_errors.errors()
|
||||
.iter()
|
||||
.flat_map(|(&field, error_kind)| match field {
|
||||
"title" => match error_kind {
|
||||
ValidationErrorsKind::Field(validation_errors) => validation_errors
|
||||
.iter()
|
||||
.map(|validation_error| validation_error.code.as_ref())
|
||||
.map(|code| match code {
|
||||
"title_length" => SubtaskError::TitleLengthInvalid,
|
||||
_ => panic!("Unexpected validation error code: `{code}`."),
|
||||
})
|
||||
.collect::<Vec<SubtaskError>>(),
|
||||
_ => panic!("Unexpected validation error kind."),
|
||||
},
|
||||
_ => panic!("Unexpected validation field name: `{field}`."),
|
||||
})
|
||||
.collect::<Vec<SubtaskError>>()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for SubtaskError {
|
||||
fn from(diesel_error: diesel::result::Error) -> Self {
|
||||
match diesel_error {
|
||||
diesel::result::Error::DatabaseError(
|
||||
diesel::result::DatabaseErrorKind::ForeignKeyViolation, info
|
||||
) => {
|
||||
match info.constraint_name() {
|
||||
Some("subtasks_task_id_fkey") => Self::TaskNotFound,
|
||||
_ => Self::Error(Error::ServerInternal)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
Self::Error(Error::ServerInternal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Has to be implemented for Dioxus server functions.
|
||||
impl Display for SubtaskError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// Has to be implemented for Dioxus server functions.
|
||||
impl FromStr for SubtaskError {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(_: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self::Error(Error::ServerInternal))
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub(crate) mod project;
|
||||
pub(crate) mod category;
|
||||
pub(crate) mod task;
|
||||
pub(crate) mod subtask;
|
||||
|
67
src/models/subtask.rs
Normal file
67
src/models/subtask.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use crate::schema::subtasks;
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
const TITLE_LENGTH_MIN: u64 = 1;
|
||||
const TITLE_LENGTH_MAX: u64 = 255;
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)]
|
||||
#[diesel(table_name = subtasks)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct Subtask {
|
||||
id: i32,
|
||||
task_id: i32,
|
||||
title: String,
|
||||
is_completed: bool,
|
||||
created_at: NaiveDateTime,
|
||||
updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Subtask {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn task_id(&self) -> i32 {
|
||||
self.task_id
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn is_completed(&self) -> bool {
|
||||
self.is_completed
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> NaiveDateTime {
|
||||
self.created_at
|
||||
}
|
||||
|
||||
pub fn updated_at(&self) -> NaiveDateTime {
|
||||
self.updated_at
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)]
|
||||
#[diesel(table_name = subtasks)]
|
||||
pub struct NewSubtask {
|
||||
pub task_id: i32,
|
||||
#[validate(length(min = "TITLE_LENGTH_MIN", max = "TITLE_LENGTH_MAX", code = "title_length"))]
|
||||
pub title: String,
|
||||
pub is_completed: bool,
|
||||
}
|
||||
|
||||
impl NewSubtask {
|
||||
pub fn new(task_id: i32, title: String, is_completed: bool) -> Self {
|
||||
Self { task_id, title, is_completed }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Subtask> for NewSubtask {
|
||||
fn from(subtask: Subtask) -> Self {
|
||||
Self::new(subtask.task_id, subtask.title, subtask.is_completed)
|
||||
}
|
||||
}
|
@ -2,15 +2,18 @@ use crate::errors::error::Error;
|
||||
use crate::errors::error_vec::ErrorVec;
|
||||
use crate::models::category::Category;
|
||||
use crate::models::project::Project;
|
||||
use crate::models::subtask::Subtask;
|
||||
use crate::models::task::Task;
|
||||
|
||||
pub(crate) mod tasks;
|
||||
pub(crate) mod projects;
|
||||
pub(crate) mod subtasks;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) enum QueryValue {
|
||||
Tasks(Vec<Task>),
|
||||
Projects(Vec<Project>),
|
||||
Tasks(Vec<Task>),
|
||||
Subtasks(Vec<Subtask>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -20,7 +23,8 @@ pub(crate) enum QueryErrors {
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
pub(crate) enum QueryKey {
|
||||
Projects,
|
||||
Tasks,
|
||||
TasksInCategory(Category),
|
||||
Projects,
|
||||
SubtasksOfTaskId(i32),
|
||||
}
|
||||
|
21
src/query/subtasks.rs
Normal file
21
src/query/subtasks.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use crate::query::{QueryErrors, QueryKey, QueryValue};
|
||||
use crate::server::subtasks::get_subtasks_of_task;
|
||||
use dioxus::prelude::ServerFnError;
|
||||
use dioxus_query::prelude::{use_get_query, QueryResult, UseQuery};
|
||||
|
||||
pub(crate) fn use_subtasks_of_task_query(task_id: i32)
|
||||
-> UseQuery<QueryValue, QueryErrors, QueryKey> {
|
||||
use_get_query([QueryKey::SubtasksOfTaskId(task_id)], fetch_subtasks_of_task)
|
||||
}
|
||||
|
||||
async fn fetch_subtasks_of_task(keys: Vec<QueryKey>) -> QueryResult<QueryValue, QueryErrors> {
|
||||
if let Some(QueryKey::SubtasksOfTaskId(task_id)) = keys.first() {
|
||||
match get_subtasks_of_task(*task_id).await {
|
||||
Ok(subtasks) => Ok(QueryValue::Subtasks(subtasks)),
|
||||
Err(ServerFnError::WrappedServerError(errors)) => Err(QueryErrors::Error(errors)),
|
||||
Err(error) => panic!("Unexpected error: {:?}", error)
|
||||
}.into()
|
||||
} else {
|
||||
panic!("Unexpected query keys: {:?}", keys);
|
||||
}
|
||||
}
|
@ -9,6 +9,17 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
subtasks (id) {
|
||||
id -> Int4,
|
||||
task_id -> Int4,
|
||||
title -> Text,
|
||||
is_completed -> Bool,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
tasks (id) {
|
||||
id -> Int4,
|
||||
@ -21,9 +32,11 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(subtasks -> tasks (task_id));
|
||||
diesel::joinable!(tasks -> projects (project_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
projects,
|
||||
subtasks,
|
||||
tasks,
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod database_connection;
|
||||
pub(crate) mod projects;
|
||||
pub(crate) mod tasks;
|
||||
pub(crate) mod subtasks;
|
||||
|
115
src/server/subtasks.rs
Normal file
115
src/server/subtasks.rs
Normal file
@ -0,0 +1,115 @@
|
||||
use crate::errors::error::Error;
|
||||
use crate::errors::error_vec::ErrorVec;
|
||||
use crate::errors::subtask_error::SubtaskError;
|
||||
use crate::models::subtask::{NewSubtask, Subtask};
|
||||
use crate::server::database_connection::establish_database_connection;
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper};
|
||||
use dioxus::prelude::*;
|
||||
use validator::Validate;
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn create_subtask(new_subtask: NewSubtask)
|
||||
-> Result<Subtask, ServerFnError<ErrorVec<SubtaskError>>> {
|
||||
use crate::schema::subtasks;
|
||||
|
||||
new_subtask.validate()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(|errors| errors.into())?;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(
|
||||
|_| vec![SubtaskError::Error(Error::ServerInternal)].into()
|
||||
)?;
|
||||
|
||||
let created_subtask = diesel::insert_into(subtasks::table)
|
||||
.values(&new_subtask)
|
||||
.returning(Subtask::as_returning())
|
||||
.get_result(&mut connection)
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(|error| vec![error.into()].into())?;
|
||||
|
||||
Ok(created_subtask)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn get_subtasks_of_task(filtered_task_id: i32)
|
||||
-> Result<Vec<Subtask>, ServerFnError<ErrorVec<Error>>> {
|
||||
use crate::schema::subtasks::dsl::*;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.map_err::<ErrorVec<Error>, _>(
|
||||
|_| vec![Error::ServerInternal].into()
|
||||
)?;
|
||||
|
||||
let results = subtasks
|
||||
.select(Subtask::as_select())
|
||||
.filter(task_id.eq(filtered_task_id))
|
||||
.load::<Subtask>(&mut connection)
|
||||
.map_err::<ErrorVec<Error>, _>(
|
||||
|_| vec![Error::ServerInternal].into()
|
||||
)?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn edit_subtask(subtask_id: i32, new_subtask: NewSubtask)
|
||||
-> Result<Subtask, ServerFnError<ErrorVec<SubtaskError>>> {
|
||||
use crate::schema::subtasks::dsl::*;
|
||||
|
||||
new_subtask.validate()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(|errors| errors.into())?;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(
|
||||
|_| vec![SubtaskError::Error(Error::ServerInternal)].into()
|
||||
)?;
|
||||
|
||||
let updated_task = diesel::update(subtasks)
|
||||
.filter(id.eq(subtask_id))
|
||||
.set((
|
||||
title.eq(new_subtask.title),
|
||||
is_completed.eq(new_subtask.is_completed)
|
||||
))
|
||||
.returning(Subtask::as_returning())
|
||||
.get_result(&mut connection)
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(|error| vec![error.into()].into())?;
|
||||
|
||||
Ok(updated_task)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn restore_subtasks_of_task(filtered_task_id: i32) -> Result<
|
||||
Vec<Subtask>,
|
||||
ServerFnError<ErrorVec<SubtaskError>>
|
||||
> {
|
||||
use crate::schema::subtasks::dsl::*;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(
|
||||
|_| vec![SubtaskError::Error(Error::ServerInternal)].into()
|
||||
)?;
|
||||
|
||||
let updated_subtasks = diesel::update(subtasks)
|
||||
.filter(task_id.eq(filtered_task_id))
|
||||
.set(is_completed.eq(false))
|
||||
.returning(Subtask::as_returning())
|
||||
.get_results(&mut connection)
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(|error| vec![error.into()].into())?;
|
||||
|
||||
Ok(updated_subtasks)
|
||||
}
|
||||
|
||||
// TODO: Get rid of this suppression.
|
||||
//noinspection DuplicatedCode
|
||||
#[server]
|
||||
pub(crate) async fn delete_subtask(subtask_id: i32)
|
||||
-> Result<(), ServerFnError<ErrorVec<Error>>> {
|
||||
use crate::schema::subtasks::dsl::*;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?;
|
||||
|
||||
diesel::delete(subtasks.filter(id.eq(subtask_id))).execute(&mut connection)
|
||||
.map_err::<ErrorVec<Error>, _>(|error| vec![error.into()].into())?;
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user