Initial version

This commit is contained in:
Jani Pulkkinen
2025-04-16 15:03:30 +03:00
commit 27a51916a3
24 changed files with 2551 additions and 0 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL=postgres://postgres:postgres@localhost:6666/lootakalenteri

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="lootakalenteri@localhost" uuid="2867adad-51b1-40f6-8817-ced1e914c0e3">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:6666/lootakalenteri</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

11
.idea/lootakalenteri-backend.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/lootakalenteri-backend.iml" filepath="$PROJECT_DIR$/.idea/lootakalenteri-backend.iml" />
</modules>
</component>
</project>

7
.idea/sqldialects.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/migrations/2025-04-10-064257_create_lootakalenteri/up.sql" dialect="PostgreSQL" />
<file url="PROJECT" dialect="PostgreSQL" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

2041
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "lootakalenteri-backend"
version = "0.1.0"
edition = "2024"
[dependencies]
diesel = { version = "2.2.9", features = ["postgres", "uuid", "r2d2", "chrono"] }
uuid = "1.8.0"
dotenvy = "0.15.7"
actix-web = "4.10.2"
env_logger = "0.11.8"
serde = { version = "1.0.219", features = ["derive"] }
chrono = { version = "0.4.40", features = ["serde"] }
serde_json = "1.0.51"
actix-rt = "2.10.0"

9
diesel.toml Normal file
View File

@@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory]
dir = "/home/jani-pulkkinen/RustroverProjects/lootakalenteri-backend/migrations"

0
migrations/.keep Normal file
View File

View File

@@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,4 @@
-- This file should undo anything in `up.sql`
drop table loota_customer;
drop table loota_order;
drop table loota_box;

View File

@@ -0,0 +1,34 @@
-- Lootakalenteri migration tables
create table loota_customer
(
id uuid primary key not null default gen_random_uuid(),
identifier varchar not null,
created_time timestamp with time zone not null default now()
);
create table loota_order
(
id uuid primary key not null default gen_random_uuid(),
customer_id uuid not null,
location varchar not null,
created_time timestamp with time zone not null default now()
);
alter table loota_order
add constraint fk_customer_id foreign key (customer_id)
references loota_customer (id) on delete cascade;
create table loota_box
(
id uuid primary key not null default gen_random_uuid(),
order_id uuid not null,
delivery_date timestamp with time zone,
pickup_date timestamp with time zone,
created_time timestamp with time zone not null default now()
);
alter table loota_box
add constraint fk_order_id foreign key (order_id)
references loota_order (id) on delete cascade;

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
delete from loota_customer;

View File

@@ -0,0 +1,17 @@
-- Test data
insert into loota_customer (identifier) values ( 'customer1');
insert into loota_customer (identifier) values ( 'customer2');
insert into loota_order (customer_id, location) values ( (select id from loota_customer where identifier = 'customer1'), 'location1');
insert into loota_order (customer_id, location) values ( (select id from loota_customer where identifier = 'customer2'), 'location1');
insert into loota_box (order_id, delivery_date) values ( (select id from loota_order where customer_id = (select id from loota_customer where identifier = 'customer1')), '2025-04-15 10:00:00+00');
insert into loota_box (order_id, delivery_date) values ( (select id from loota_order where customer_id = (select id from loota_customer where identifier = 'customer1')), '2025-04-16 10:00:00+00');
insert into loota_box (order_id, delivery_date) values ( (select id from loota_order where customer_id = (select id from loota_customer where identifier = 'customer1')), '2025-04-17 10:00:00+00');
insert into loota_box (order_id, delivery_date) values ( (select id from loota_order where customer_id = (select id from loota_customer where identifier = 'customer1')), '2025-04-18 10:00:00+00');
insert into loota_box (order_id, delivery_date) values ( (select id from loota_order where customer_id = (select id from loota_customer where identifier = 'customer2')), '2025-04-15 10:00:00+00');
insert into loota_box (order_id, delivery_date) values ( (select id from loota_order where customer_id = (select id from loota_customer where identifier = 'customer2')), '2025-05-16 10:00:00+00');
insert into loota_box (order_id, delivery_date) values ( (select id from loota_order where customer_id = (select id from loota_customer where identifier = 'customer2')), '2025-06-17 10:00:00+00');
insert into loota_box (order_id, delivery_date) values ( (select id from loota_order where customer_id = (select id from loota_customer where identifier = 'customer2')), '2025-07-18 10:00:00+00');

12
src/connection.rs Normal file
View File

@@ -0,0 +1,12 @@
use diesel::prelude::*;
use dotenvy::dotenv;
use std::env;
pub fn establish_connection() -> PgConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

1
src/constants.rs Normal file
View File

@@ -0,0 +1 @@
pub const APPLICATION_JSON: &str = "application/json";

193
src/loota.rs Normal file
View File

@@ -0,0 +1,193 @@
use actix_web::{get, post, web, HttpResponse, Responder};
use chrono::{NaiveDateTime};
use diesel::prelude::*;
use diesel::result::Error;
use uuid::Uuid;
use crate::constants::APPLICATION_JSON;
use crate::connection::establish_connection;
use crate::models::{Customer, Box, Order, LootaResponse, LootaBoxResponse, LootaRequest};
use crate::schema::loota_box::dsl::loota_box;
use crate::schema::loota_box::{delivery_date, id, pickup_date};
use crate::schema::loota_customer::dsl::loota_customer;
use crate::schema::loota_customer::identifier;
fn find_boxes(_identifier: String, _order: &Order, conn: &mut PgConnection) -> LootaResponse {
let boxes = Box::belonging_to(_order)
.select(Box::as_select())
.order_by(delivery_date.asc())
.load(conn);
match boxes {
Ok(boxes) => {
let _box_responses = boxes.iter().map(|b| {
LootaBoxResponse {
id: b.id.to_string(),
delivery_date: b.delivery_date,
pickup_date: b.pickup_date,
}
});
LootaResponse {
identifier: _identifier.clone(),
location: _order.location.clone(),
boxes: _box_responses.collect(),
}
}
Err(err) => {
println!("Error: {:?}", err);
LootaResponse {
identifier: "".to_string(),
location: "".to_string(),
boxes: vec![],
}
}
}
}
fn find_order(_customer: &Customer, conn: &mut PgConnection) -> Result<LootaResponse, Error> {
let _identifier = _customer.identifier.clone();
let order = Order::belonging_to(_customer)
.select(Order::as_select())
.limit(1)
.load(conn);
match order {
Ok(order) => match order.first() {
Some(order) => Ok(find_boxes(_identifier, order, conn)),
_ => Err(Error::NotFound)
}
Err(err) => {
println!("Error: {:?}", err);
Err(err)
}
}
}
fn find_customer(_identifier: String) -> Result<LootaResponse, Error> {
let conn = &mut establish_connection();
let customer = loota_customer
.filter(identifier.eq(_identifier))
.select(Customer::as_select())
.load(conn);
match customer {
Ok(customer) => match customer.first() {
Some(customer) => Ok(find_order(customer, conn)?),
_ => Err(Error::NotFound)
}
Err(err) => {
println!("Error: {:?}", err);
Err(err)
}
}
}
fn update_box(_id: Uuid, _pickup_date: NaiveDateTime) -> Result<LootaBoxResponse, Error> {
let conn = &mut establish_connection();
let loota = diesel::update(loota_box.filter(id.eq(&_id)))
.set(pickup_date.eq(_pickup_date))
.returning(Box::as_returning())
.get_result(conn);
match loota {
Ok(loota) => {
Ok(LootaBoxResponse {
id: loota.id.to_string(),
delivery_date: loota.delivery_date,
pickup_date: loota.pickup_date,
})
}
Err(err) => {
println!("Error: {:?}", err);
Err(err)
}
}
}
fn parse_uuid(_id: String) -> Result<Uuid, uuid::Error> {
Uuid::parse_str(&_id)
}
fn parse_date(_date: String) -> Result<NaiveDateTime, chrono::ParseError> {
NaiveDateTime::parse_from_str(&_date, "%Y-%m-%dT%H:%M:%S")
}
#[get("/loota/{id}")]
async fn get(path: web::Path<String>) -> impl Responder {
let identifer = path.into_inner();
println!("id: {:?}", identifer);
let response = web::block(move || find_customer(identifer)).await.unwrap();
match response {
Ok(response) => {
HttpResponse::Ok()
.content_type(APPLICATION_JSON).json(response)
}
_ => HttpResponse::NotFound()
.content_type(APPLICATION_JSON)
.await
.unwrap(),
}
}
#[post("/loota/")]
async fn update(req: web::Json<LootaRequest>) -> impl Responder {
let box_id = parse_uuid(req.id.clone());
match box_id {
Ok(box_id) => {
let date = parse_date(req.pickup_date.clone());
match date {
Ok(date) => {
let updated_box = update_box(box_id, date);
match updated_box {
Ok(updated_box) => {
HttpResponse::Ok()
.content_type(APPLICATION_JSON)
.json(updated_box)
}
Err(err) => {
println!("Error: {:?}", err);
HttpResponse::BadRequest()
.content_type(APPLICATION_JSON)
.finish()
}
}
}
Err(err) => {
println!("Error: {:?}", err);
HttpResponse::BadRequest()
.content_type(APPLICATION_JSON)
.finish()
}
}
}
Err(err) => {
println!("Error: {:?}", err);
HttpResponse::BadRequest()
.content_type(APPLICATION_JSON)
.finish()
}
}
}

35
src/main.rs Normal file
View File

@@ -0,0 +1,35 @@
extern crate actix_web;
extern crate diesel;
use std::{io};
use actix_web::{middleware, App, HttpServer};
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use diesel::PgConnection;
mod constants;
mod loota;
mod models;
mod schema;
mod connection;
pub type DBPool = Pool<ConnectionManager<PgConnection>>;
pub type DBPooledConnection = PooledConnection<ConnectionManager<PgConnection>>;
#[actix_rt::main]
async fn main() -> io::Result<()> {
env_logger::init();
HttpServer::new(|| {
App::new()
// enable logger - always register actix-web Logger middleware last
.wrap(middleware::Logger::default())
// register HTTP requests handlers
.service(loota::get)
.service(loota::update)
})
.bind("0.0.0.0:9090")?
.run()
.await
}

55
src/models.rs Normal file
View File

@@ -0,0 +1,55 @@
use uuid::Uuid;
use diesel::prelude::*;
use chrono::{NaiveDateTime};
use serde::{Serialize, Deserialize};
use crate::schema::{loota_customer, loota_order, loota_box};
#[derive(Queryable, Identifiable, Selectable, Debug, PartialEq)]
#[diesel(table_name = loota_customer)]
pub struct Customer {
pub id: Uuid,
pub identifier: String
}
#[derive(Queryable, Selectable, Identifiable, Associations, Debug, PartialEq)]
#[diesel(belongs_to(Customer))]
#[diesel(table_name = loota_order)]
pub struct Order {
pub id: Uuid,
pub location: String,
pub customer_id: Uuid,
}
#[derive(Queryable, Selectable, Identifiable, Associations, Debug, PartialEq)]
#[diesel(belongs_to(Order))]
#[diesel(table_name = loota_box)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Box {
pub id: Uuid,
pub delivery_date: Option<NaiveDateTime>,
pub pickup_date: Option<NaiveDateTime>,
pub order_id: Uuid
}
#[derive(Debug, Serialize)]
pub struct LootaResponse {
pub identifier: String,
pub location: String,
pub boxes: Vec<LootaBoxResponse>
}
#[derive(Debug, Serialize)]
pub struct LootaBoxResponse {
pub id: String,
pub delivery_date: Option<NaiveDateTime>,
pub pickup_date: Option<NaiveDateTime>
}
#[derive(Debug, Deserialize)]
pub struct LootaRequest {
pub id: String,
pub pickup_date: String
}

37
src/schema.rs Normal file
View File

@@ -0,0 +1,37 @@
// @generated automatically by Diesel CLI.
diesel::table! {
loota_box (id) {
id -> Uuid,
order_id -> Uuid,
delivery_date -> Nullable<Timestamp>,
pickup_date -> Nullable<Timestamp>,
created_time -> Timestamp,
}
}
diesel::table! {
loota_customer (id) {
id -> Uuid,
identifier -> Varchar,
created_time -> Timestamp,
}
}
diesel::table! {
loota_order (id) {
id -> Uuid,
customer_id -> Uuid,
location -> Varchar,
created_time -> Timestamp,
}
}
diesel::joinable!(loota_box -> loota_order (order_id));
diesel::joinable!(loota_order -> loota_customer (customer_id));
diesel::allow_tables_to_appear_in_same_query!(
loota_box,
loota_customer,
loota_order,
);