Compare commits
2 commits
527ae6433c
...
0f432d3e7e
Author | SHA1 | Date | |
---|---|---|---|
Gandalf | 0f432d3e7e | ||
Gandalf | 51a4c78bd5 |
1997
Cargo.lock
generated
Normal file
1997
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[workspace]
|
||||
|
||||
members = [
|
||||
"backend",
|
||||
"frontend",
|
||||
|
||||
#Internal
|
||||
"common"
|
||||
]
|
|
@ -1,3 +1,11 @@
|
|||
# HambiMap
|
||||
|
||||
Code for a Website that collects stories and photos from 11 years of Hambach Forest occupation, with the goal of producing info tables to put up in the forest
|
||||
|
||||
## Tutorial branch
|
||||
I'll use this branch to follow [this tutorial](https://blog.logrocket.com/full-stack-rust-a-complete-tutorial-with-examples/)
|
||||
|
||||
This uses postgresql as database, my original plan was sqlite. Let's see.
|
||||
Also I'm not sure if wasm is the frontend I want.
|
||||
|
||||
There are crates used that require a newer rust than debian stable can provide.
|
||||
|
|
17
backend/Cargo.toml
Normal file
17
backend/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
authors = ["Gandalf <gandalfderbunte@riseup.net>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "=1.6.1", features = ["macros", "rt-multi-thread"] }
|
||||
warp = "=0.3.1"
|
||||
mobc = "=0.7.2"
|
||||
mobc-postgres = { version = "=0.7.0", features = ["with-chrono-0_4", "with-serde_json-1"] }
|
||||
# tokio-postgres = "=0.7.0"
|
||||
hyper = "=0.14.0"
|
||||
serde = {version = "=1.0.126", features = ["derive"] }
|
||||
serde_json = "=1.0.64"
|
||||
thiserror = "=1.0.24"
|
||||
common = { version = "0.1.0", path = "../common" }
|
16
backend/src/db/db.sql
Normal file
16
backend/src/db/db.sql
Normal file
|
@ -0,0 +1,16 @@
|
|||
CREATE TABLE IF NOT EXISTS owner
|
||||
(
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pet
|
||||
(
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
owner_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
animal_type VARCHAR(255) NOT NULL,
|
||||
color VARCHAR(255),
|
||||
|
||||
CONSTRAINT fk_pet_owner_id FOREIGN KEY (owner_id) REFERENCES pet(id)
|
||||
);
|
30
backend/src/db/mod.rs
Normal file
30
backend/src/db/mod.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
type Result<T> = std::result::Result<T, error::Error>;
|
||||
|
||||
const DB_POOL_MAX_OPEN: u64 = 32;
|
||||
const DB_POOL_MAX_IDLE: u64 = 8;
|
||||
const DB_POOL_TIMEOUT_SECONDS: u64 = 15;
|
||||
const INIT_SQL: &str = "./db.sql";
|
||||
|
||||
pub async fn init_db(db_pool: &DBPool) -> Result<()> {
|
||||
let init_file = fs::read_to_string(INIT_SQL)?;
|
||||
let con = get_db_con(db_pool).await?;
|
||||
con.batch_execute(init_file.as_str())
|
||||
.await
|
||||
.map_err(DBInitError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_db_con(db_pool: &DBPool) -> Result<DBCon> {
|
||||
db_pool.get().await.map_err(DBPoolError)
|
||||
}
|
||||
|
||||
pub fn create_pool() -> std::result::Result<DBPool, mobc::Error<Error>> {
|
||||
let config = Config::from_str("postgres://postgres@127.0.0.1:7878/postgres")?;
|
||||
|
||||
let manager = PgConnectionManager::new(config, NoTls);
|
||||
Ok(Pool::builder()
|
||||
.max_open(DB_POOL_MAX_OPEN)
|
||||
.max_idle(DB_POOL_MAX_IDLE)
|
||||
.get_timeout(Some(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS)))
|
||||
.build(manager))
|
||||
}
|
37
backend/src/db/owner.rs
Normal file
37
backend/src/db/owner.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
pub const TABLE: &str = "owner";
|
||||
const SELECT_FIELDS: &str = "id, name";
|
||||
|
||||
pub async fn fetch(db_pool: &DBPool) -> Result<Vec<Owner>> {
|
||||
let con = get_db_con(db_pool).await?;
|
||||
let query = format!("SELECT {} FROM {}", SELECT_FIELDS, TABLE);
|
||||
let rows = con.query(query.as_str(), &[]).await.map_err(DBQueryError)?;
|
||||
|
||||
Ok(rows.iter().map(|r| row_to_owner(&r)).collect())
|
||||
}
|
||||
|
||||
pub async fn fetch_one(db_pool: &DBPool, id: i32) -> Result<Owner> {
|
||||
let con = get_db_con(db_pool).await?;
|
||||
let query = format!("SELECT {} FROM {} WHERE id = $1", SELECT_FIELDS, TABLE);
|
||||
|
||||
let row = con
|
||||
.query_one(query.as_str(), &[&id])
|
||||
.await
|
||||
.map_err(DBQueryError)?;
|
||||
Ok(row_to_owner(&row))
|
||||
}
|
||||
|
||||
pub async fn create(db_pool: &DBPool, body: OwnerRequest) -> Result<Owner> {
|
||||
let con = get_db_con(db_pool).await?;
|
||||
let query = format!("INSERT INTO {} (name) VALUES ($1) RETURNING *", TABLE);
|
||||
let row = con
|
||||
.query_one(query.as_str(), &[&body.name])
|
||||
.await
|
||||
.map_err(DBQueryError)?;
|
||||
Ok(row_to_owner(&row))
|
||||
}
|
||||
|
||||
fn row_to_owner(row: &Row) -> Owner {
|
||||
let id: i32 = row.get(0);
|
||||
let name: String = row.get(1);
|
||||
Owner { id, name }
|
||||
}
|
55
backend/src/db/pet.rs
Normal file
55
backend/src/db/pet.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
pub const TABLE: &str = "pet";
|
||||
const SELECT_FIELDS: &str = "id, owner_id, name, animal_type, color";
|
||||
|
||||
pub async fn fetch(db_pool: &DBPool, owner_id: i32) -> Result<Vec<Pet>> {
|
||||
let con = get_db_con(db_pool).await?;
|
||||
let query = format!(
|
||||
"SELECT {} FROM {} WHERE owner_id = $1",
|
||||
SELECT_FIELDS, TABLE
|
||||
);
|
||||
let rows = con
|
||||
.query(query.as_str(), &[&owner_id])
|
||||
.await
|
||||
.map_err(DBQueryError)?;
|
||||
|
||||
Ok(rows.iter().map(|r| row_to_pet(&r)).collect())
|
||||
}
|
||||
|
||||
pub async fn create(db_pool: &DBPool, owner_id: i32, body: PetRequest) -> Result<Pet> {
|
||||
let con = get_db_con(db_pool).await?;
|
||||
let query = format!(
|
||||
"INSERT INTO {} (name, owner_id, animal_type, color) VALUES ($1, $2, $3, $4) RETURNING *",
|
||||
TABLE
|
||||
);
|
||||
let row = con
|
||||
.query_one(
|
||||
query.as_str(),
|
||||
&[&body.name, &owner_id, &body.animal_type, &body.color],
|
||||
)
|
||||
.await
|
||||
.map_err(DBQueryError)?;
|
||||
Ok(row_to_pet(&row))
|
||||
}
|
||||
|
||||
pub async fn delete(db_pool: &DBPool, owner_id: i32, id: i32) -> Result<u64> {
|
||||
let con = get_db_con(db_pool).await?;
|
||||
let query = format!("DELETE FROM {} WHERE id = $1 AND owner_id = $2", TABLE);
|
||||
con.execute(query.as_str(), &[&id, &owner_id])
|
||||
.await
|
||||
.map_err(DBQueryError)
|
||||
}
|
||||
|
||||
fn row_to_pet(row: &Row) -> Pet {
|
||||
let id: i32 = row.get(0);
|
||||
let owner_id: i32 = row.get(1);
|
||||
let name: String = row.get(2);
|
||||
let animal_type: String = row.get(3);
|
||||
let color: Option<String> = row.get(4);
|
||||
Pet {
|
||||
id,
|
||||
name,
|
||||
owner_id,
|
||||
animal_type,
|
||||
color,
|
||||
}
|
||||
}
|
49
backend/src/handler.rs
Normal file
49
backend/src/handler.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
pub async fn list_pets_handler(owner_id: i32, db_pool: DBPool) -> Result<impl Reply> {
|
||||
let pets = db::pet::fetch(&db_pool, owner_id)
|
||||
.await
|
||||
.map_err(reject::custom)?;
|
||||
Ok(json::<Vec<_>>(
|
||||
&pets.into_iter().map(PetResponse::of).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn create_pet_handler(
|
||||
owner_id: i32,
|
||||
body: PetRequest,
|
||||
db_pool: DBPool,
|
||||
) -> Result<impl Reply> {
|
||||
Ok(json(&PetResponse::of(
|
||||
db::pet::create(&db_pool, owner_id, body)
|
||||
.await
|
||||
.map_err(reject::custom)?,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn delete_pet_handler(owner_id: i32, id: i32, db_pool: DBPool) -> Result<impl Reply> {
|
||||
db::pet::delete(&db_pool, owner_id, id)
|
||||
.await
|
||||
.map_err(reject::custom)?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn list_owners_handler(db_pool: DBPool) -> Result<impl Reply> {
|
||||
let owners = db::owner::fetch(&db_pool).await.map_err(reject::custom)?;
|
||||
Ok(json::<Vec<_>>(
|
||||
&owners.into_iter().map(OwnerResponse::of).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn fetch_owner_handler(id: i32, db_pool: DBPool) -> Result<impl Reply> {
|
||||
let owner = db::owner::fetch_one(&db_pool, id)
|
||||
.await
|
||||
.map_err(reject::custom)?;
|
||||
Ok(json(&OwnerResponse::of(owner)))
|
||||
}
|
||||
|
||||
pub async fn create_owner_handler(body: OwnerRequest, db_pool: DBPool) -> Result<impl Reply> {
|
||||
Ok(json(&OwnerResponse::of(
|
||||
db::owner::create(&db_pool, body)
|
||||
.await
|
||||
.map_err(reject::custom)?,
|
||||
)))
|
||||
}
|
74
backend/src/main.rs
Normal file
74
backend/src/main.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
mod db;
|
||||
mod error;
|
||||
mod handler;
|
||||
|
||||
type Result<T> = std::result::Result<T, Rejection>;
|
||||
type DBCon = Connection<PgConnectionManager<NoTls>>;
|
||||
type DBPool = Pool<PgConnectionManager<NoTls>>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let db_pool = db::create_pool().expect("database pool can be created");
|
||||
|
||||
db::init_db(&db_pool)
|
||||
.await
|
||||
.expect("database can be initialized");
|
||||
|
||||
let pet = warp::path!("owner" / i32 / "pet");
|
||||
let pet_param = warp::path!("owner" / i32 / "pet" / i32);
|
||||
let owner = warp::path("owner");
|
||||
|
||||
let pet_routes = pet
|
||||
.and(warp::get())
|
||||
.and(with_db(db_pool.clone()))
|
||||
.and_then(handler::list_pets_handler)
|
||||
.or(pet
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_db(db_pool.clone()))
|
||||
.and_then(handler::create_pet_handler))
|
||||
.or(pet_param
|
||||
.and(warp::delete())
|
||||
.and(with_db(db_pool.clone()))
|
||||
.and_then(handler::delete_pet_handler));
|
||||
|
||||
let owner_routes = owner
|
||||
.and(warp::get())
|
||||
.and(warp::path::param())
|
||||
.and(with_db(db_pool.clone()))
|
||||
.and_then(handler::fetch_owner_handler)
|
||||
.or(owner
|
||||
.and(warp::get())
|
||||
.and(with_db(db_pool.clone()))
|
||||
.and_then(handler::list_owners_handler))
|
||||
.or(owner
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_db(db_pool.clone()))
|
||||
.and_then(handler::create_owner_handler));
|
||||
|
||||
let routes = pet_routes
|
||||
.or(owner_routes)
|
||||
.recover(error::handle_rejection)
|
||||
.with(
|
||||
warp::cors()
|
||||
.allow_credentials(true)
|
||||
.allow_methods(&[
|
||||
Method::OPTIONS,
|
||||
Method::GET,
|
||||
Method::POST,
|
||||
Method::DELETE,
|
||||
Method::PUT,
|
||||
])
|
||||
.allow_headers(vec![header::CONTENT_TYPE, header::ACCEPT])
|
||||
.expose_headers(vec![header::LINK])
|
||||
.max_age(300)
|
||||
.allow_any_origin(),
|
||||
);
|
||||
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 8000)).await;
|
||||
}
|
||||
|
||||
fn with_db(db_pool: DBPool) -> impl Filter<Extract = (DBPool,), Error = Infallible> + Clone {
|
||||
warp::any().map(move || db_pool.clone())
|
||||
}
|
8
common/Cargo.toml
Normal file
8
common/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "common"
|
||||
version = "0.1.0"
|
||||
authors = ["Gandalf <gandalfderbunte@riseup.net>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
serde = {version = "=1.0.126", features = ["derive"] }
|
70
common/src/lib.rs
Normal file
70
common/src/lib.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct Owner {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct OwnerRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct OwnerResponse {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl OwnerResponse {
|
||||
pub fn of(owner: Owner) -> OwnerResponse {
|
||||
OwnerResponse {
|
||||
id: owner.id,
|
||||
name: owner.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct Pet {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub owner_id: i32,
|
||||
pub animal_type: String,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct PetRequest {
|
||||
pub name: String,
|
||||
pub animal_type: String,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct PetResponse {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub animal_type: String,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
impl PetResponse {
|
||||
pub fn of(pet: Pet) -> PetResponse {
|
||||
PetResponse {
|
||||
id: pet.id,
|
||||
name: pet.name,
|
||||
animal_type: pet.animal_type,
|
||||
color: pet.color,
|
||||
}
|
||||
}
|
||||
}
|
16
frontend/Cargo.toml
Normal file
16
frontend/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "frontend"
|
||||
version = "0.1.0"
|
||||
authors = ["Gandalf <gandalfderbunte@riseup.net>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
yew = "0.18"
|
||||
wasm-bindgen = "0.2.67"
|
||||
serde_json = "1"
|
||||
serde = {version = "=1.0.126", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
yew-router = "0.15.0"
|
||||
common = { version = "0.1.0", path = "../common" }
|
7
frontend/src/lib.rs
Normal file
7
frontend/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
Loading…
Reference in a new issue