diff --git a/react-rust-postgres/backend/Cargo.toml b/react-rust-postgres/backend/Cargo.toml index 36d1526..f8a7174 100644 --- a/react-rust-postgres/backend/Cargo.toml +++ b/react-rust-postgres/backend/Cargo.toml @@ -7,17 +7,11 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -diesel_migrations = "1.4.0" -rocket = "0.4.4" +actix-web = "4.0.0-beta.8" +deadpool-postgres = "0.9.0" +env_logger = "^0.8" +log = "^0.4" serde = "1.0" serde_json = "1.0" -serde_derive = "1.0" - -[dependencies.diesel] -version = "1.0.0" -features = ["postgres"] - -[dependencies.rocket_contrib] -version = "0.4.4" -default-features = false -features = ["json", "diesel_postgres_pool"] +tokio-postgres = "^0.7" +tokio-postgres-migration = "^0.1" diff --git a/react-rust-postgres/backend/Dockerfile b/react-rust-postgres/backend/Dockerfile index 2f75c1a..1e0ddf9 100644 --- a/react-rust-postgres/backend/Dockerfile +++ b/react-rust-postgres/backend/Dockerfile @@ -1,8 +1,4 @@ -FROM jdrouet/rust-nightly:buster-slim AS base - -RUN apt-get update \ - && apt-get install -y libpq-dev \ - && rm -rf /var/lib/apt/lists/* +FROM rust:buster AS base ENV USER=root ENV ROCKET_ADDRESS=0.0.0.0 diff --git a/react-rust-postgres/backend/diesel.toml b/react-rust-postgres/backend/diesel.toml deleted file mode 100644 index 92267c8..0000000 --- a/react-rust-postgres/backend/diesel.toml +++ /dev/null @@ -1,5 +0,0 @@ -# For documentation on how to configure this file, -# see diesel.rs/guides/configuring-diesel-cli - -[print_schema] -file = "src/schema.rs" diff --git a/react-rust-postgres/backend/migrations/00000000000000_diesel_initial_setup/down.sql b/react-rust-postgres/backend/migrations/00000000000000_diesel_initial_setup/down.sql deleted file mode 100644 index a9f5260..0000000 --- a/react-rust-postgres/backend/migrations/00000000000000_diesel_initial_setup/down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- 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(); diff --git a/react-rust-postgres/backend/migrations/00000000000000_diesel_initial_setup/up.sql b/react-rust-postgres/backend/migrations/00000000000000_diesel_initial_setup/up.sql deleted file mode 100644 index d68895b..0000000 --- a/react-rust-postgres/backend/migrations/00000000000000_diesel_initial_setup/up.sql +++ /dev/null @@ -1,36 +0,0 @@ --- 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; diff --git a/react-rust-postgres/backend/migrations/2020-03-26-084736_create_people/down.sql b/react-rust-postgres/backend/migrations/0001_create-users_down.sql similarity index 100% rename from react-rust-postgres/backend/migrations/2020-03-26-084736_create_people/down.sql rename to react-rust-postgres/backend/migrations/0001_create-users_down.sql diff --git a/react-rust-postgres/backend/migrations/2020-03-26-084736_create_people/up.sql b/react-rust-postgres/backend/migrations/0001_create-users_up.sql similarity index 100% rename from react-rust-postgres/backend/migrations/2020-03-26-084736_create_people/up.sql rename to react-rust-postgres/backend/migrations/0001_create-users_up.sql diff --git a/react-rust-postgres/backend/readme.md b/react-rust-postgres/backend/readme.md index 7c12c43..ce27319 100644 --- a/react-rust-postgres/backend/readme.md +++ b/react-rust-postgres/backend/readme.md @@ -1,4 +1,4 @@ # Backend -This backend is made with Rust using [Rocket](https://rocket.rs/) as a web server and [Diesel](https://diesel.rs/) as an ORM. +This backend is made with Rust using [actix-web](https://actix.rs/) as a web server and [deadpool-postgres](https://crates.io/crates/deadpool-postgres) as a connection manager. diff --git a/react-rust-postgres/backend/src/main.rs b/react-rust-postgres/backend/src/main.rs index c474595..149e99e 100644 --- a/react-rust-postgres/backend/src/main.rs +++ b/react-rust-postgres/backend/src/main.rs @@ -1,96 +1,45 @@ -#![feature(proc_macro_hygiene, decl_macro)] +use actix_web::{get, web, App, HttpResponse, HttpServer}; +use deadpool_postgres::Pool; -#[macro_use] -extern crate diesel; -#[macro_use] -extern crate diesel_migrations; -#[macro_use] -extern crate rocket; -#[macro_use] -extern crate serde_derive; -#[macro_use] -extern crate rocket_contrib; - -mod schema; +mod postgres; mod user; -use rocket::config::{Config, Environment, Value}; -use rocket::fairing::AdHoc; -use rocket_contrib::json::Json; -use std::collections::HashMap; -use std::env; - -// 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("my_db")] -struct MyDBConn(diesel::PgConnection); - -#[derive(Serialize)] -struct HelloMessage { - message: String, -} - -#[get("/")] -fn index(conn: MyDBConn) -> Json { - let result = match user::User::all(&*conn) { - Ok(res) => res.len(), - Err(_) => 0, +#[get("/users")] +async fn list_users(pool: web::Data) -> HttpResponse { + let client = match pool.get().await { + Ok(client) => client, + Err(err) => { + log::debug!("unable to get postgres client: {:?}", err); + return HttpResponse::InternalServerError().json("unable to get postgres client"); + } }; - - Json(HelloMessage { - message: format!("Hello with {} users", result), - }) -} - -fn get_config() -> Config { - let mut database_config = HashMap::new(); - let mut databases = HashMap::new(); - - let env_address = env::var("ROCKET_ADDRESS") - .or::(Ok(String::from("localhost"))) - .unwrap(); - - let env_mode = env::var("ROCKET_ENV") - .or(Ok(String::from("development"))) - .and_then(|value| value.parse::()) - .unwrap(); - - let database_url = match env::var("DATABASE_URL") { - Ok(value) => value, - Err(_) => String::from("postgres://localhost/postgres"), - }; - - database_config.insert("url", Value::from(database_url)); - databases.insert("my_db", Value::from(database_config)); - - let config = Config::build(env_mode) - .address(env_address) - .extra("databases", databases) - .finalize() - .unwrap(); - - config -} - -fn run_db_migrations(r: rocket::Rocket) -> Result { - let conn = MyDBConn::get_one(&r).expect("database connection"); - match embedded_migrations::run(&*conn) { - Ok(()) => Ok(r), - Err(e) => { - println!("Failed to run database migrations: {:?}", e); - Err(r) + match user::User::all(&**client).await { + Ok(list) => HttpResponse::Ok().json(list), + Err(err) => { + log::debug!("unable to fetch users: {:?}", err); + return HttpResponse::InternalServerError().json("unable to fetch users"); } } } -fn main() { - let config = get_config(); - rocket::custom(config) - .attach(MyDBConn::fairing()) - .attach(AdHoc::on_attach("Database Migrations", run_db_migrations)) - .mount("/", routes![index]) - .launch(); +fn address() -> String { + std::env::var("ADDRESS").unwrap_or_else(|_| "127.0.0.1:8000".into()) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + + let pg_pool = postgres::create_pool(); + postgres::migrate_up(&pg_pool).await; + + let address = address(); + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(pg_pool.clone())) + .service(list_users) + }) + .bind(&address)? + .run() + .await } diff --git a/react-rust-postgres/backend/src/postgres.rs b/react-rust-postgres/backend/src/postgres.rs new file mode 100644 index 0000000..a46f6f8 --- /dev/null +++ b/react-rust-postgres/backend/src/postgres.rs @@ -0,0 +1,40 @@ +use deadpool_postgres::{Config, Pool}; +use tokio_postgres::NoTls; +use tokio_postgres_migration::Migration; + +const SCRIPTS_UP: [(&str, &str); 1] = [( + "0001_create-users", + include_str!("../migrations/0001_create-users_up.sql"), +)]; + +fn create_config() -> Config { + let mut cfg = Config::new(); + if let Ok(host) = std::env::var("PG_HOST") { + cfg.host = Some(host); + } + if let Ok(dbname) = std::env::var("PG_DBNAME") { + cfg.dbname = Some(dbname); + } + if let Ok(user) = std::env::var("PG_USER") { + cfg.user = Some(user); + } + if let Ok(password) = std::env::var("PG_PASSWORD") { + cfg.password = Some(password); + } + cfg +} + +pub fn create_pool() -> Pool { + create_config() + .create_pool(NoTls) + .expect("couldn't create postgres pool") +} + +pub async fn migrate_up(pool: &Pool) { + let mut client = pool.get().await.expect("couldn't get postgres client"); + let migration = Migration::new("migrations".to_string()); + migration + .up(&mut **client, &SCRIPTS_UP) + .await + .expect("couldn't run migrations"); +} diff --git a/react-rust-postgres/backend/src/schema.rs b/react-rust-postgres/backend/src/schema.rs deleted file mode 100644 index 4087ca1..0000000 --- a/react-rust-postgres/backend/src/schema.rs +++ /dev/null @@ -1,6 +0,0 @@ -table! { - users (id) { - id -> Int4, - login -> Text, - } -} diff --git a/react-rust-postgres/backend/src/user.rs b/react-rust-postgres/backend/src/user.rs index 6ea71b9..7e47994 100644 --- a/react-rust-postgres/backend/src/user.rs +++ b/react-rust-postgres/backend/src/user.rs @@ -1,18 +1,25 @@ -#![allow(proc_macro_derive_resolution_fallback)] +use tokio_postgres::{Error, GenericClient, Row}; -use diesel; -use diesel::prelude::*; -use super::schema::users; - -#[derive(Queryable, AsChangeset, Serialize, Deserialize)] -#[table_name = "users"] +#[derive(Debug, serde::Serialize)] pub struct User { pub id: i32, pub login: String, } -impl User { - pub fn all(connection: &PgConnection) -> QueryResult> { - users::table.load::(&*connection) +impl From for User { + fn from(row: Row) -> Self { + Self { + id: row.get(0), + login: row.get(1), + } + } +} + +impl User { + pub async fn all(client: &C) -> Result, Error> { + let stmt = client.prepare("SELECT id, login FROM users").await?; + let rows = client.query(&stmt, &[]).await?; + + Ok(rows.into_iter().map(User::from).collect()) } } diff --git a/react-rust-postgres/docker-compose.yaml b/react-rust-postgres/docker-compose.yaml index 263ed2b..d40cb71 100644 --- a/react-rust-postgres/docker-compose.yaml +++ b/react-rust-postgres/docker-compose.yaml @@ -10,12 +10,18 @@ services: - 3000:3000 volumes: - ./frontend/src:/code/src:ro + backend: build: context: backend target: development environment: - - DATABASE_URL=postgres://postgres:mysecretpassword@db/postgres + - ADDRESS=0.0.0.0:8000 + - RUST_LOG=debug + - PG_DBNAME=postgres + - PG_HOST=db + - PG_USER=postgres + - PG_PASSWORD=mysecretpassword networks: - client-side - server-side @@ -24,6 +30,7 @@ services: - backend-cache:/code/target depends_on: - db + db: image: postgres:12-alpine restart: always @@ -35,9 +42,11 @@ services: - 5432:5432 volumes: - db-data:/var/lib/postgresql/data + networks: client-side: {} server-side: {} + volumes: backend-cache: {} db-data: {} diff --git a/react-rust-postgres/frontend/src/App.js b/react-rust-postgres/frontend/src/App.js index f7b8ec5..af8ad01 100644 --- a/react-rust-postgres/frontend/src/App.js +++ b/react-rust-postgres/frontend/src/App.js @@ -5,9 +5,9 @@ import "./App.css"; function App() { const [message, setMessage] = useState(); useEffect(() => { - fetch("/api/") - .then(res => res.json()) - .then(res => setMessage(res.message)) + fetch("/api/users") + .then((res) => res.json()) + .then((res) => setMessage(res.message)) .catch(console.error); }, [setMessage]); return (