feat: ability to create a task #14
migrations/2024-08-19-105140_create_tasks
src
3
migrations/2024-08-19-105140_create_tasks/down.sql
Normal file
3
migrations/2024-08-19-105140_create_tasks/down.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
|
||||
DROP TABLE IF EXISTS "tasks";
|
11
migrations/2024-08-19-105140_create_tasks/up.sql
Normal file
11
migrations/2024-08-19-105140_create_tasks/up.sql
Normal file
@ -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")
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
|
52
src/errors/task_create_error.rs
Normal file
52
src/errors/task_create_error.rs
Normal file
@ -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}`."),
|
||||
![]() 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
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
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
|
||||
}
|
||||
}
|
||||
![]() Consider improving encapsulation and method naming. The getter methods are straightforward, but consider using Rust's idiomatic approach by implementing the **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 }
|
||||
}
|
||||
![]() 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,
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user
Ensure proper indexing and constraints.
The
tasks
table is created with a primary key and a foreign key. Consider adding an index onproject_id
to improve query performance if you frequently query tasks by project. Additionally, ensure that thecategory
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!