feat: ability to create a task #14

Merged
matous-volf merged 12 commits from feat/task-create into main 2024-08-22 21:40:47 +00:00
8 changed files with 211 additions and 0 deletions
Showing only changes of commit 12ea8b5de2 - Show all commits
migrations/2024-08-19-105140_create_tasks
src

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS "tasks";

@ -0,0 +1,11 @@
-- Your SQL goes here
CREATE TABLE "tasks"(
"id" SERIAL NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"deadline" DATE,
"category" JSONB NOT NULL,
"project_id" INT4,
FOREIGN KEY ("project_id") REFERENCES "projects"("id")
);
coderabbitai[bot] commented 2024-08-22 20:20:54 +00:00 (Migrated from github.com)
Review

Ensure proper indexing and constraints.

The tasks table is created with a primary key and a foreign key. Consider adding an index on project_id to improve query performance if you frequently query tasks by project. Additionally, ensure that the category JSONB column is used correctly, as it can store complex data structures.

If you need further assistance with indexing strategies or JSONB usage, let me know!

**Ensure proper indexing and constraints.** The `tasks` table is created with a primary key and a foreign key. Consider adding an index on `project_id` to improve query performance if you frequently query tasks by project. Additionally, ensure that the `category` JSONB column is used correctly, as it can store complex data structures. If you need further assistance with indexing strategies or JSONB usage, let me know! <!-- This is an auto-generated comment by CodeRabbit -->

@ -1,3 +1,4 @@
pub(crate) mod error;
pub(crate) mod error_vec;
pub(crate) mod project_create_error;
pub(crate) mod task_create_error;

@ -0,0 +1,52 @@
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 TaskCreateError {
TitleLengthInvalid,
ProjectNotFound,
Error(Error),
}
impl From<ValidationErrors> for ErrorVec<TaskCreateError> {
fn from(e: ValidationErrors) -> Self {
e.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" => TaskCreateError::TitleLengthInvalid,
_ => panic!("Unexpected validation error code: `{code}`."),
})
.collect::<Vec<TaskCreateError>>(),
_ => panic!("Unexpected validation error kind."),
},
_ => panic!("Unexpected validation field name: `{field}`."),
coderabbitai[bot] commented 2024-08-22 20:20:54 +00:00 (Migrated from github.com)
Review

Consider alternatives to panic for unexpected validation errors.

Using panic for unexpected validation errors may not be ideal in production code. Consider logging the error and returning a default error variant or using a custom error type.

**Consider alternatives to panic for unexpected validation errors.** Using panic for unexpected validation errors may not be ideal in production code. Consider logging the error and returning a default error variant or using a custom error type. <!-- This is an auto-generated comment by CodeRabbit -->
})
.collect::<Vec<TaskCreateError>>()
.into()
}
}
// has to be implemented for Dioxus server functions
impl Display for TaskCreateError {
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 TaskCreateError {
type Err = ();
fn from_str(_: &str) -> Result<Self, Self::Err> {
Ok(TaskCreateError::TitleLengthInvalid)
}
}

65
src/models/category.rs Normal file

@ -0,0 +1,65 @@
use chrono::{Duration, NaiveDate, NaiveTime};
use diesel::deserialize::FromSql;
use diesel::pg::{Pg, PgValue};
use diesel::serialize::{Output, ToSql};
use diesel::sql_types::Jsonb;
use diesel::{AsExpression, FromSqlRow};
use serde::{Deserialize, Serialize};
use serde_with::DurationSeconds;
use std::io::Write;
#[serde_with::serde_as]
#[derive(AsExpression, FromSqlRow, Serialize, Deserialize, Clone, Debug)]
#[diesel(sql_type = Jsonb)]
pub enum Category {
Inbox,
SomedayMaybe,
WaitingFor(String),
NextSteps,
Calendar {
date: NaiveDate,
#[serde_as(as = "Option<DurationSeconds<i64>>")]
reoccurance_interval: Option<Duration>,
time: Option<CalendarTime>,
},
LongTerm,
Done,
Trash,
}
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CalendarTime {
time: NaiveTime,
#[serde_as(as = "Option<DurationSeconds<i64>>")]
reminder_offset: Option<Duration>,
}
impl CalendarTime {
pub fn new(time: NaiveTime, reminder_offset: Option<Duration>) -> Self {
Self { time, reminder_offset }
}
}
impl ToSql<Jsonb, Pg> for Category {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> diesel::serialize::Result {
let json = serde_json::to_string(self)?;
// Prepend the JSONB version byte.
out.write_all(&[1])?;
out.write_all(json.as_bytes())?;
Ok(diesel::serialize::IsNull::No)
}
}
impl FromSql<Jsonb, Pg> for Category {
fn from_sql(bytes: PgValue) -> diesel::deserialize::Result<Self> {
let bytes = bytes.as_bytes();
if bytes.is_empty() {
return Err("Unexpected empty bytes (missing the JSONB version number).".into());
}
let str = std::str::from_utf8(&bytes[1..])?;
serde_json::from_str(str).map_err(Into::into)
}
}

@ -1 +1,3 @@
pub(crate) mod project;
pub(crate) mod category;
pub(crate) mod task;

60
src/models/task.rs Normal file

@ -0,0 +1,60 @@
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use validator::Validate;
use crate::models::category::Category;
use crate::schema::tasks;
const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255;
#[derive(Queryable, Selectable, Serialize, Deserialize, Clone, Debug)]
#[diesel(table_name = crate::schema::tasks)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Task {
id: i32,
title: String,
deadline: Option<chrono::NaiveDate>,
category: Category,
project_id: Option<i32>,
}
impl Task {
pub fn id(&self) -> i32 {
self.id
}
pub fn title(&self) -> &str {
&self.title
}
pub fn deadline(&self) -> Option<chrono::NaiveDate> {
self.deadline
}
pub fn category(&self) -> &Category {
&self.category
}
pub fn project_id(&self) -> Option<i32> {
self.project_id
}
}
coderabbitai[bot] commented 2024-08-22 20:20:54 +00:00 (Migrated from github.com)
Review

Consider improving encapsulation and method naming.

The getter methods are straightforward, but consider using Rust's idiomatic approach by implementing the Deref trait or using public fields if appropriate. Additionally, method names could be more descriptive, such as get_id instead of id.

**Consider improving encapsulation and method naming.** The getter methods are straightforward, but consider using Rust's idiomatic approach by implementing the `Deref` trait or using public fields if appropriate. Additionally, method names could be more descriptive, such as `get_id` instead of `id`. <!-- This is an auto-generated comment by CodeRabbit -->
#[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)]
#[diesel(table_name = tasks)]
pub struct NewTask {
#[validate(length(min = "TITLE_LENGTH_MIN", max = "TITLE_LENGTH_MAX", code = "title_length"))]
pub title: String,
pub deadline: Option<chrono::NaiveDate>,
pub category: Category,
pub project_id: Option<i32>,
}
impl NewTask {
pub fn new(
title: String, deadline: Option<chrono::NaiveDate>,
category: Category, project_id: Option<i32>,
) -> Self {
Self { title, deadline, category, project_id }
}
coderabbitai[bot] commented 2024-08-22 20:20:54 +00:00 (Migrated from github.com)
Review

Consider enhancing validation error handling.

While the validation logic is clear, consider implementing custom error handling to provide more informative feedback to the user when validation fails.

**Consider enhancing validation error handling.** While the validation logic is clear, consider implementing custom error handling to provide more informative feedback to the user when validation fails. <!-- This is an auto-generated comment by CodeRabbit -->
}

@ -6,3 +6,20 @@ diesel::table! {
title -> Text,
}
}
diesel::table! {
tasks (id) {
id -> Int4,
title -> Text,
deadline -> Nullable<Date>,
category -> Jsonb,
project_id -> Nullable<Int4>,
}
}
diesel::joinable!(tasks -> projects (project_id));
diesel::allow_tables_to_appear_in_same_query!(
projects,
tasks,
);