Compare commits

...

12 Commits

Author SHA1 Message Date
Paul van Tilburg fe13723323 Move GraphQL-related model code to a separate module 2019-07-10 20:58:37 +02:00
Paul van Tilburg 63fc7a0721 Some documentation tweaks 2019-07-10 20:58:33 +02:00
Paul van Tilburg c3a6b5f572 Get rid of all non-GraphQL handlers
Also move the GraphQL handlers to the parent handlers model.
2019-07-10 20:54:46 +02:00
Paul van Tilburg 26c86fe03e Add GraphQL handlers and add GraphQL schema
Remove all REST and JSON related handlers and error catchers.
2019-07-09 12:24:52 +02:00
Paul van Tilburg 5e24738bba Turn all models into GraphQL(Input)Objects 2019-07-09 12:23:58 +02:00
Paul van Tilburg f8bff689e6 Add relation resolving methods to all models 2019-07-09 12:23:31 +02:00
Paul van Tilburg 368a3ff10e Add some ORM/Diesel helper macros 2019-07-09 12:21:12 +02:00
Paul van Tilburg 68ca95f166 Add juniper crates to the dependencies 2019-07-09 12:19:01 +02:00
Paul van Tilburg 0b7471467e Switch FLOAT types to DOUBLE in the database
SQLite has no NUMERIC(8,3) type, which it should acutally be.
Switch to DOUBLE, because FLOAT maps to f32 which is not supported
by GraphQL.
2019-07-09 12:18:54 +02:00
Paul van Tilburg 33c9520d5d Automatically run database migrations on start 2019-07-09 12:17:28 +02:00
Paul van Tilburg 5d6a26a3ed Ignore the databases and .env 2019-07-08 20:39:06 +02:00
Paul van Tilburg ef4507e25e Add a database pool through a fairing 2019-07-08 20:36:59 +02:00
25 changed files with 874 additions and 393 deletions

7
.gitignore vendored
View File

@ -1,2 +1,9 @@
# Ignore Rust stuff
/target
**/*.rs.bk
# Ignore database
/db/*.db
# Ignore dotenv environment file
/.env

557
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,11 +8,14 @@ A web application for task/project time registration and invoicing.
edition = "2018"
[dependencies]
diesel_migrations = "1.4.0"
rocket = "0.4.2"
serde = "1.0.92"
serde_json = "1.0.39"
serde_derive = "1.0.92"
dotenv = "0.14.1"
juniper = "0.12.0"
juniper_rocket = "0.3.0"
juniper_codegen = "0.12.0"
[dependencies.chrono]
version = "0.4.6"
@ -25,4 +28,7 @@ features = ["chrono", "sqlite"]
[dependencies.rocket_contrib]
version = "0.4.2"
default-features = false
features = ["json", "serve"]
features = ["diesel_sqlite_pool", "json", "serve"]
[dev-dependencies]
dotenv = "0.14.1"

5
Rocket.toml Normal file
View File

@ -0,0 +1,5 @@
[development.databases]
stoptime_db = { url = "db/stoptime-dev.db" }
[production.databases]
stoptime_db = { url = "db/stoptime.db" }

0
db/.gitkeep Normal file
View File

View File

@ -5,7 +5,7 @@ CREATE TABLE customers (
"address_street" VARCHAR(255) NOT NULL,
"email" VARCHAR(255) NOT NULL,
"financial_contact" VARCHAR(255) NOT NULL,
"hourly_rate" FLOAT,
"hourly_rate" DOUBLE,
"name" VARCHAR(255) NOT NULL,
"phone" VARCHAR(255) NOT NULL,
"short_name" VARCHAR(255) NOT NULL,

View File

@ -1,12 +1,12 @@
CREATE TABLE tasks (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"customer_id" INTEGER NOT NULL,
"fixed_cost" FLOAT,
"hourly_rate" FLOAT,
"fixed_cost" DOUBLE,
"hourly_rate" DOUBLE,
"invoice_comment" VARCHAR(255) NOT NULL,
"invoice_id" INTEGER,
"name" VARCHAR(255) NOT NULL,
"vat_rate" FLOAT NOT NULL,
"vat_rate" DOUBLE NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)

View File

@ -1,4 +1,4 @@
//! The application error catchers
//! The application error catchers.
use rocket::catch;
use rocket_contrib::json;
@ -12,12 +12,3 @@ pub fn not_found() -> JsonValue {
"reason": "Resource was not found",
})
}
/// Catches an HTTP 422 (Unprocessable Entity) error.
#[catch(422)]
pub fn unprocessable_entity() -> JsonValue {
json!({
"status": "error",
"reason": "Could not parse JSON body or fields were missing",
})
}

92
src/graphql.rs Normal file
View File

@ -0,0 +1,92 @@
//! The GraphQL schema implementation.
//!
//! Provides the schema with the root query and mutation.
use crate::models::{
CompanyInfo, Customer, Invoice, NewCustomer, NewInvoice, NewTimeEntry, TimeEntry,
};
use crate::DbConn;
use diesel::prelude::*;
use juniper::{object, Context, FieldResult, RootNode};
impl Context for DbConn {}
/// The GraphQL schema.
pub type Schema = RootNode<'static, Query, Mutation>;
/// The GraphQL query root.
pub struct Query;
#[object(Context = DbConn)]
impl Query {
/// Returns the current API version.
fn api_version() -> &'static str {
"1.0"
}
/// Returns the company info with the given ID.
fn company_info(context: &DbConn, id: i32) -> FieldResult<CompanyInfo> {
retrieve!(CompanyInfo, id, **context).map_err(Into::into)
}
/// Returns all known customers.
fn customers(context: &DbConn) -> FieldResult<Vec<Customer>> {
all!(Customer, **context).map_err(Into::into)
}
/// Returns the customer with the given ID.
fn customer(context: &DbConn, id: i32) -> FieldResult<Customer> {
retrieve!(Customer, id, **context).map_err(Into::into)
}
/// Returns all known customers.
fn customers(context: &DbConn) -> FieldResult<Vec<Customer>> {
all!(Customer, **context).map_err(Into::into)
}
/// Returns the customer with the given ID.
fn invoice(context: &DbConn, id: i32) -> FieldResult<Invoice> {
retrieve!(Invoice, id, **context).map_err(Into::into)
}
/// Returns all known invoices.
fn invoices(context: &DbConn) -> FieldResult<Vec<Invoice>> {
all!(Invoice, **context).map_err(Into::into)
}
/// Returns the time entry with the given ID.
fn time_entry(context: &DbConn, id: i32) -> FieldResult<TimeEntry> {
retrieve!(TimeEntry, id, **context).map_err(Into::into)
}
/// Returns all known invoices.
fn time_entries(context: &DbConn) -> FieldResult<Vec<TimeEntry>> {
all!(TimeEntry, **context).map_err(Into::into)
}
}
/// The GraphQL mutation root.
pub struct Mutation;
#[object(Context = DbConn)]
impl Mutation {
/// Returns the current API version.
fn api_version() -> &'static str {
"1.0"
}
/// Creates a new customer.
fn create_customer(context: &DbConn, new_customer: NewCustomer) -> FieldResult<Customer> {
create!(Customer, new_customer, **context).map_err(Into::into)
}
/// Creates a new invoice.
fn create_invoice(context: &DbConn, new_invoice: NewInvoice) -> FieldResult<Invoice> {
create!(Invoice, new_invoice, **context).map_err(Into::into)
}
/// Creates a new time entry.
fn create_time_entry(context: &DbConn, new_time_entry: NewTimeEntry) -> FieldResult<TimeEntry> {
create!(TimeEntry, new_time_entry, **context).map_err(Into::into)
}
}

View File

@ -1,16 +1,44 @@
//! The root handlers
//! The request handlers.
use rocket::get;
use crate::graphql::Schema;
use crate::DbConn;
pub mod company;
pub mod customers;
pub mod invoices;
pub mod timeline;
use juniper_rocket::{playground_source, GraphQLRequest, GraphQLResponse};
use rocket::response::content::Html;
use rocket::{get, post, State};
/// Presents the dashboard/overview as start/home view
/// Presents the main web application.
///
/// It lists the running tasks and projects per customer and shows a global summary.
/// FIXME: Not implemented yet.
#[get("/")]
pub fn index() {
unimplemented!()
}
/// Presents a playground GraphQL web application.
///
/// This can be used to test the GraphQL backend.
#[get("/graphql/playground")]
pub fn graphql_playground() -> Html<String> {
playground_source("/graphql")
}
/// Handles a GraphQL GET request.
#[get("/graphql?<request>")]
pub fn graphql_get(
request: GraphQLRequest,
conn: DbConn,
schema: State<Schema>,
) -> GraphQLResponse {
request.execute(&schema, &conn)
}
/// Handles a GraphQL POST request.
#[post("/graphql", data = "<request>")]
pub fn graphql_post(
request: GraphQLRequest,
conn: DbConn,
schema: State<Schema>,
) -> GraphQLResponse {
request.execute(&schema, &conn)
}

View File

@ -1,20 +0,0 @@
//! The company (information) handlers
//!
//! These handlers are for showing and updating information of the company of the user.
use rocket::{get, post};
/// Shows a form with the company information that allows for updating it.
///
/// When updating, it will create a new revision. The handler allows showing other revisions.
// FIXME: Implement revisions!
#[get("/")]
pub fn index() {
unimplemented!()
}
/// Updates the company information by creating a new revision.
#[post("/")]
pub fn create() {
unimplemented!()
}

View File

@ -1,44 +0,0 @@
//! The customer handlers
//!
//! Handlers for viewing a list of existing customers or creating a new one.
use rocket::{delete, get, post, put};
pub mod invoices;
pub mod tasks;
/// Shows the list of customers.
#[get("/")]
pub fn index() {
unimplemented!()
}
/// Creates a new customer and redirects to the show handler.
#[post("/")]
pub fn create() {
unimplemented!()
}
/// Provides the form for the data required to create a new customer.
#[get("/new")]
pub fn new() {
unimplemented!()
}
/// Shows a form for viewing and updating information of the customer with the given ID.
#[get("/<_id>")]
pub fn show(_id: u32) {
unimplemented!()
}
/// Updates the customer with the given ID.
#[put("/<_id>")]
pub fn update(_id: u32) {
unimplemented!()
}
/// Destroys the customer with the given ID and redirects to the index handler.
#[delete("/<_id>")]
pub fn destroy(_id: u32) {
unimplemented!()
}

View File

@ -1,41 +0,0 @@
//! The invoice handlers
//!
//! These handlers are for creating and viewing invoices for a specific customer.
use rocket::{get, post, put};
/// Creates a new invoice object for the customer the given ID.
///
/// A unique number is generated for the invoice by taking the year and a sequence number.
///
/// Fixed cost tasks are directly tied to the invoice.
///
/// For a task with an hourly rate, a task copy is created with the select time entries that need
/// to be billed and put in the invoice; the remaining unbilled time entries are left in the
/// original task.
#[post("/<_customer_id>/invoices")]
pub fn create(_customer_id: u32) {
unimplemented!()
}
/// Generates the form to create a new invoice object by listing unbulled fixed cost tasks and
/// unbilled registered time (for tasks with an hourly rate) of the customer with the given ID so
/// that a selection can be made.
#[get("/<_customer_id>/invoices/new")]
pub fn new(_customer_id: u32) {
unimplemented!()
}
/// Shows a form for the invoice with the given number for the customer with the given ID
/// and shows a firm for updating it.
// FIXME: Handle PDF and LaTex generation here too!
#[get("/<_customer_id>/invoices/<_number>", rank = 2)] // Number could be "new"
pub fn show(_customer_id: u32, _number: String) {
unimplemented!()
}
/// Updates the invoices with the given number for the customer with the given ID.
#[put("/<_customer_id>/invoices/<_number>")]
pub fn update(_customer_id: u32, _number: String) {
unimplemented!()
}

View File

@ -1,36 +0,0 @@
//! The tasks handlers
//!
//! These handlers are for creating, editing and deleting a task for a specific customer.
use rocket::{delete, get, post, put};
/// Creates a new task oject for a customer with the given ID.
#[post("/<_customer_id>/tasks")]
pub fn create(_customer_id: u32) {
unimplemented!()
}
/// Provides the form for the data required to create a new task for a customer with the given ID.
#[get("/<_customer_id>/tasks/new")]
pub fn new(_customer_id: u32) {
unimplemented!()
}
/// Shows a form for viewing and updating information of the task with the given ID for
/// a customer with the given ID.
#[get("/<_customer_id>/tasks/<_id>", rank = 2)] // FIXME: Why is rank 2 necessary?
pub fn show(_customer_id: u32, _id: u32) {
unimplemented!()
}
/// Updates the task with the given ID for a customer with the given ID.
#[put("/<_customer_id>/tasks/<_id>")]
pub fn update(_customer_id: u32, _id: u32) {
unimplemented!()
}
/// Destroys the task with the given ID of a customer with the given ID.
#[delete("/<_customer_id>/tasks/<_id>")]
pub fn destroy(_customer_id: u32, _id: u32) {
unimplemented!()
}

View File

@ -1,11 +0,0 @@
//! The invoices handlers
//!
//! The handler is used for showing a list of all invoices.
use rocket::get;
/// Shows a list of invoices, sorted per customer.
#[get("/")]
pub fn index() {
unimplemented!()
}

View File

@ -1,43 +0,0 @@
//! The timeline handlers
//!
//! These handlers are used for presenting a timeline of registered time and also for quickly
//! registering new time entries.
use rocket::{delete, get, post, put};
/// Retrieves all registered time entries in descending order to present the timeline.
#[get("/")]
pub fn index() {
unimplemented!()
}
/// Registers a time entry and redirects back.
#[post("/")]
pub fn create() {
unimplemented!()
}
/// Shows a form for quickly registering time using a list of customers and tasks and the current
/// date and time.
#[get("/new")]
pub fn new() {
unimplemented!()
}
/// Show a form of the time entry with the given ID for updating it.
#[get("/<_id>")]
pub fn show(_id: u32) {
unimplemented!()
}
/// Update the time entry with the given ID.
#[put("/<_id>")]
pub fn update(_id: u32) {
unimplemented!()
}
/// Destroys the time entry with the given ID.
#[delete("/<_id>")]
pub fn destroy(_id: u32) {
unimplemented!()
}

74
src/helpers.rs Normal file
View File

@ -0,0 +1,74 @@
//! Some ORM helpers.
//!
//! These macros can be used to shorten repetative ORM queries.
#![allow(unused_macros)]
macro_rules! all {
($model_name:ident, $conn:expr) => {{
use diesel::associations::HasTable;
let table = $crate::models::$model_name::table();
table.load(&$conn)
}};
}
macro_rules! retrieve {
($model_name:ident, $primary_key:expr, $conn:expr) => {{
use diesel::associations::HasTable;
let table = $crate::models::$model_name::table();
table.find($primary_key).first(&$conn)
}};
}
macro_rules! create {
($model_name:ident, $object:expr, $conn:expr) => {{
use diesel::associations::HasTable;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, Table};
let table = $crate::models::$model_name::table();
let primary_key = table.primary_key();
$conn.transaction::<$model_name, diesel::result::Error, _>(|| {
diesel::insert_into(table).values($object).execute(&$conn)?;
table.order(primary_key.desc()).first(&$conn)
})
}};
}
macro_rules! update {
($model_name:ident, $primary_key:expr, $object:expr, $conn:expr) => {{
use diesel::associations::HasTable;
let table = $crate::models::$model_name::table();
$conn.transaction::<_, diesel::result::Error, _>(|| {
diesel::update(table.find($primary_key))
.set($object)
.execute(&$conn)?;
table.find($primary_key).first(&$conn)
})
}};
}
macro_rules! destroy {
($model_name:ident, $primary_key:expr, $conn:expr) => {{
use diesel::associations::HasTable;
let table = $crate::models::$model_name::table();
let result: Result<$model_name, _> = get!($model_name, $primary_key, $conn);
match result {
Ok(_) => diesel::delete(table.find($primary_key)).execute(&$conn),
Err(e) => Err(e),
}
}};
}
macro_rules! destroy_all {
($model_name:ident, $conn:expr) => {{
use diesel::associations::HasTable;
use diesel::RunQueryDsl;
let table = $crate::models::$model_name::table();
diesel::delete(table).execute(&$conn)
}};
}

View File

@ -3,62 +3,67 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
use rocket::fairing::AdHoc;
use rocket::{catchers, routes, Rocket};
use rocket_contrib::database;
use rocket_contrib::serve::StaticFiles;
pub mod catchers;
pub mod handlers;
#[macro_use]
pub mod helpers;
pub mod catchers;
pub mod graphql;
pub mod handlers;
pub mod models;
/// The application database schema.
pub mod schema;
// This macro from `diesel_migrations` defines an `embedded_migrations` module containing a
// function named `run`. This allows the example to be run and tested without any outside setup
// of the database.
embed_migrations!();
#[database("stoptime_db")]
pub struct DbConn(diesel::SqliteConnection);
/// Runs the database migrations.
fn run_db_migrations(rocket: Rocket) -> Result<Rocket, Rocket> {
let conn = DbConn::get_one(&rocket).expect("Failed to get a database connection");
match embedded_migrations::run(&*conn) {
Ok(()) => Ok(rocket),
Err(e) => {
eprintln!("Failed to run database migrations: {:?}", e);
Err(rocket)
}
}
}
/// Sets up the Rocket application.
fn rocket() -> Rocket {
let static_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/static");
let static_files = StaticFiles::from(static_dir);
rocket::ignite()
.mount("/", routes![handlers::index])
.attach(DbConn::fairing())
.attach(AdHoc::on_attach("Database Migrations", run_db_migrations))
.manage(graphql::Schema::new(graphql::Query, graphql::Mutation))
.mount(
"/company",
routes![handlers::company::index, handlers::company::create],
)
.mount(
"/customers",
"/",
routes![
handlers::customers::index,
handlers::customers::create,
handlers::customers::new,
handlers::customers::show,
handlers::customers::update,
handlers::customers::destroy,
handlers::customers::invoices::create,
handlers::customers::invoices::new,
handlers::customers::invoices::show,
handlers::customers::invoices::update,
handlers::customers::tasks::create,
handlers::customers::tasks::new,
handlers::customers::tasks::show,
handlers::customers::tasks::update,
handlers::customers::tasks::destroy,
handlers::index,
handlers::graphql_playground,
handlers::graphql_get,
handlers::graphql_post,
],
)
.mount("/invoices", routes![handlers::invoices::index])
.mount("/static", static_files)
.mount(
"/timeline",
routes![
handlers::timeline::index,
handlers::timeline::create,
handlers::timeline::new,
handlers::timeline::show,
handlers::timeline::update,
handlers::timeline::destroy,
],
)
.register(catchers![
catchers::not_found,
catchers::unprocessable_entity
])
.register(catchers![catchers::not_found])
}
/// Runs the Rocket application.
fn main() {
rocket().launch();
}

View File

@ -1,4 +1,4 @@
//! The application models
//! The application models.
mod company_info;
mod customer;

View File

@ -1,13 +1,15 @@
use chrono::NaiveDateTime;
use serde_derive::{Deserialize, Serialize};
use diesel::prelude::*;
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
use crate::schema::company_infos;
use crate::DbConn;
/// The company (information) model.
///
/// This model represents information about the company or sole proprietorship of the user of
/// StopTime.
#[derive(Associations, Debug, Identifiable, Deserialize, Queryable, Serialize)]
#[derive(Associations, Debug, GraphQLObject, Identifiable, Queryable)]
#[belongs_to(CompanyInfo, foreign_key = "original_id")]
#[table_name = "company_infos"]
pub struct CompanyInfo {
@ -57,10 +59,25 @@ pub struct CompanyInfo {
pub updated_at: NaiveDateTime,
}
impl CompanyInfo {
/// Returns the original/previous company information model (if any).
pub fn original(&self, conn: &DbConn) -> QueryResult<Option<CompanyInfo>> {
use diesel::associations::HasTable;
match self.original_id {
Some(original_id) => CompanyInfo::table()
.find(original_id)
.first(&**conn)
.optional(),
None => Ok(None),
}
}
}
/// The new company (information) model.
///
/// This model represents new company information that can be inserted into the database.
#[derive(Debug, Deserialize, Insertable, Serialize)]
#[derive(Debug, GraphQLInputObject, Insertable)]
#[table_name = "company_infos"]
pub struct NewCompanyInfo {
/// The international bank account number

View File

@ -1,13 +1,16 @@
use chrono::NaiveDateTime;
use serde_derive::{Deserialize, Serialize};
use diesel::prelude::*;
use juniper_codegen::GraphQLInputObject;
use crate::models::{Invoice, Task};
use crate::schema::customers;
use crate::DbConn;
/// The customer model.
///
/// This model represents a customer that has projects/tasks for which invoices need to be
/// generated.
#[derive(AsChangeset, Debug, Deserialize, Identifiable, Queryable, Serialize)]
#[derive(AsChangeset, Associations, Debug, Identifiable, Queryable)]
#[table_name = "customers"]
pub struct Customer {
/// The unique identification number
@ -23,7 +26,7 @@ pub struct Customer {
/// The name of the financial contact person/department
pub financial_contact: String,
/// The default hourly rate (if applicable)
pub hourly_rate: Option<f32>,
pub hourly_rate: Option<f64>,
/// The official (long) name
pub name: String,
/// The phone number
@ -38,10 +41,22 @@ pub struct Customer {
pub updated_at: NaiveDateTime,
}
impl Customer {
/// Returns the invoices billed to the customer.
pub fn invoices(&self, conn: &DbConn) -> QueryResult<Vec<Invoice>> {
Invoice::belonging_to(self).load(&**conn)
}
/// Returns the project/tasks associated with the customer.
pub fn tasks(&self, conn: &DbConn) -> QueryResult<Vec<Task>> {
Task::belonging_to(self).load(&**conn)
}
}
/// The new customer model
///
/// This model represents a new customer that can be inserted into the database.
#[derive(Default, Deserialize, Insertable, Serialize)]
#[derive(Default, GraphQLInputObject, Insertable)]
#[table_name = "customers"]
pub struct NewCustomer {
/// The city part of the address
@ -55,7 +70,7 @@ pub struct NewCustomer {
/// The name of the financial contact person/department
pub financial_contact: String,
/// The default hourly rate (if applicable)
pub hourly_rate: Option<f32>,
pub hourly_rate: Option<f64>,
/// The official (long) name
pub name: String,
/// The phone number
@ -65,3 +80,22 @@ pub struct NewCustomer {
/// Flag whether the customer requires time specificaions
pub time_specification: bool,
}
mod graphql {
use super::{Customer, DbConn, Invoice, Task};
use juniper::{object, FieldResult};
#[object(Context = DbConn)]
impl Customer {
/// Returns the invoices billed to the customer.
fn invoices(&self, context: &DbConn) -> FieldResult<Vec<Invoice>> {
self.invoices(context).map_err(Into::into)
}
/// Returns the project/tasks associated with the customer.
fn tasks(&self, context: &DbConn) -> FieldResult<Vec<Task>> {
self.tasks(context).map_err(Into::into)
}
}
}

View File

@ -1,14 +1,16 @@
use chrono::NaiveDateTime;
use serde_derive::{Deserialize, Serialize};
use diesel::prelude::*;
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
use crate::models::{CompanyInfo, Customer};
use crate::models::{CompanyInfo, Customer, Task};
use crate::schema::invoices;
use crate::DbConn;
/// The invoice model.
///
/// This model represents an invoice for a customer that contains billed tasks and through the
/// tasks the registered time.
#[derive(AsChangeset, Associations, Debug, Deserialize, Identifiable, Queryable, Serialize)]
#[derive(AsChangeset, Associations, Debug, GraphQLObject, Identifiable, Queryable)]
#[belongs_to(CompanyInfo)]
#[belongs_to(Customer)]
#[table_name = "invoices"]
@ -31,10 +33,33 @@ pub struct Invoice {
pub updated_at: NaiveDateTime,
}
impl Invoice {
/// Returns the associated company info at the time of billing.
pub fn company_info(&self, conn: &DbConn) -> QueryResult<CompanyInfo> {
use diesel::associations::HasTable;
CompanyInfo::table()
.find(self.company_info_id)
.first(&**conn)
}
/// Returns the associated customer.
pub fn customer(&self, conn: &DbConn) -> QueryResult<Customer> {
use diesel::associations::HasTable;
Customer::table().find(self.customer_id).first(&**conn)
}
/// Returns the billed tasks included in the invoice.
pub fn tasks(&self, conn: &DbConn) -> QueryResult<Vec<Task>> {
Task::belonging_to(self).load(&**conn)
}
}
/// The new invoice model.
///
/// This model represents an new invoice for a customer that can be inserted into the database.
#[derive(Debug, Deserialize, Insertable, Serialize)]
#[derive(Debug, GraphQLInputObject, Insertable)]
#[table_name = "invoices"]
pub struct NewInvoice {
/// The ID of the company info at the time of billing

View File

@ -1,14 +1,16 @@
use chrono::NaiveDateTime;
use serde_derive::{Deserialize, Serialize};
use diesel::prelude::*;
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
use crate::models::{Customer, Invoice};
use crate::models::{Customer, Invoice, TimeEntry};
use crate::schema::tasks;
use crate::DbConn;
/// The task (or project) model.
///
/// This model represents a task (or project) of a customer on which time can be registered.
/// generated.
#[derive(AsChangeset, Associations, Debug, Deserialize, Identifiable, Queryable, Serialize)]
#[derive(AsChangeset, Associations, Debug, GraphQLObject, Identifiable, Queryable)]
#[belongs_to(Customer)]
#[belongs_to(Invoice)]
#[table_name = "tasks"]
@ -18,9 +20,9 @@ pub struct Task {
/// The ID of the associated customer
pub customer_id: i32,
/// The fixed cost of the task (if applicable)
pub fixed_cost: Option<f32>,
pub fixed_cost: Option<f64>,
/// The hourly rate of the task (if applicable)
pub hourly_rate: Option<f32>,
pub hourly_rate: Option<f64>,
/// An extra comment for on the invoice
pub invoice_comment: String,
/// The associated invoice (if billed)
@ -28,26 +30,50 @@ pub struct Task {
/// The name/description
pub name: String,
/// The VAT rate (at time of billing)
pub vat_rate: f32,
pub vat_rate: f64,
/// The time of creation
pub created_at: NaiveDateTime,
/// The time of last update
pub updated_at: NaiveDateTime,
}
impl Task {
/// Returns the associated customer.
pub fn customer(&self, conn: &DbConn) -> QueryResult<Customer> {
use diesel::associations::HasTable;
Customer::table().find(self.customer_id).first(&**conn)
}
/// Returns the associated invoice (if billed)
pub fn invoice(&self, conn: &DbConn) -> QueryResult<Option<Invoice>> {
use diesel::associations::HasTable;
match self.invoice_id {
Some(invoice_id) => Invoice::table().find(invoice_id).first(&**conn).optional(),
None => Ok(None),
}
}
/// Returns the registered time entries.
pub fn time_entries(&self, conn: &DbConn) -> QueryResult<Vec<TimeEntry>> {
TimeEntry::belonging_to(self).load(&**conn)
}
}
/// The new task model.
///
/// This model represents a new task (or project) of a customer that can be inserted into the
/// database.
#[derive(Debug, Deserialize, Insertable, Serialize)]
#[derive(Debug, GraphQLInputObject, Insertable)]
#[table_name = "tasks"]
pub struct NewTask {
/// The ID of the associated customer
pub customer_id: i32,
/// The fixed cost of the task (if applicable)
pub fixed_cost: Option<f32>,
pub fixed_cost: Option<f64>,
/// The hourly rate of the task (if applicable)
pub hourly_rate: Option<f32>,
pub hourly_rate: Option<f64>,
/// An extra comment for on the invoice
pub invoice_comment: String,
/// The associated invoice (if billed)
@ -55,5 +81,5 @@ pub struct NewTask {
/// The name/description
pub name: String,
/// The VAT rate (at time of billing)
pub vat_rate: f32,
pub vat_rate: f64,
}

View File

@ -1,13 +1,15 @@
use chrono::NaiveDateTime;
use serde_derive::{Deserialize, Serialize};
use diesel::prelude::*;
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
use crate::models::Task;
use crate::schema::time_entries;
use crate::DbConn;
/// The time entry model.
///
/// This model represents an amount of time that is registered for a certain task.
#[derive(AsChangeset, Associations, Debug, Deserialize, Identifiable, Queryable, Serialize)]
#[derive(AsChangeset, Associations, Debug, GraphQLObject, Identifiable, Queryable)]
#[belongs_to(Task)]
#[table_name = "time_entries"]
pub struct TimeEntry {
@ -29,10 +31,19 @@ pub struct TimeEntry {
pub updated_at: NaiveDateTime,
}
impl TimeEntry {
/// Returns the task the entry is registered for.
pub fn customer(&self, conn: &DbConn) -> QueryResult<Task> {
use diesel::associations::HasTable;
Task::table().find(self.task_id).first(&**conn)
}
}
/// The new time entry model.
///
/// This model represents a new registered amount of time that can be inserted into the database.
#[derive(Debug, Deserialize, Insertable, Serialize)]
#[derive(Debug, GraphQLInputObject, Insertable)]
#[table_name = "time_entries"]
pub struct NewTimeEntry {
/// Flag whether to bill or not

View File

@ -181,10 +181,10 @@ table! {
financial_contact -> Text,
/// The `hourly_rate` column of the `customers` table.
///
/// Its SQL type is `Nullable<Float>`.
/// Its SQL type is `Nullable<Double>`.
///
/// (Automatically generated by Diesel.)
hourly_rate -> Nullable<Float>,
hourly_rate -> Nullable<Double>,
/// The `name` column of the `customers` table.
///
/// Its SQL type is `Text`.
@ -299,16 +299,16 @@ table! {
customer_id -> Integer,
/// The `fixed_cost` column of the `tasks` table.
///
/// Its SQL type is `Nullable<Float>`.
/// Its SQL type is `Nullable<Double>`.
///
/// (Automatically generated by Diesel.)
fixed_cost -> Nullable<Float>,
fixed_cost -> Nullable<Double>,
/// The `hourly_rate` column of the `tasks` table.
///
/// Its SQL type is `Nullable<Float>`.
/// Its SQL type is `Nullable<Double>`.
///
/// (Automatically generated by Diesel.)
hourly_rate -> Nullable<Float>,
hourly_rate -> Nullable<Double>,
/// The `invoice_comment` column of the `tasks` table.
///
/// Its SQL type is `Text`.
@ -329,10 +329,10 @@ table! {
name -> Text,
/// The `vat_rate` column of the `tasks` table.
///
/// Its SQL type is `Float`.
/// Its SQL type is `Double`.
///
/// (Automatically generated by Diesel.)
vat_rate -> Float,
vat_rate -> Double,
/// The `created_at` column of the `tasks` table.
///
/// Its SQL type is `Timestamp`.