diff --git a/Cargo.toml b/Cargo.toml index 792fd300..0edc9c60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "cors/backend", "data-factory", "databases/diesel", + "databases/diesel-async", "databases/mongodb", "databases/mysql", "databases/postgres", diff --git a/databases/diesel-async/Cargo.toml b/databases/diesel-async/Cargo.toml new file mode 100644 index 00000000..8443b577 --- /dev/null +++ b/databases/diesel-async/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "db-diesel-async" +version = "1.0.0" +edition = "2021" + +[dependencies] +actix-web.workspace = true +diesel = { version = "*", default-features = false, features = ["uuid"] } +diesel-async = { version = "*", features = ["postgres", "bb8", "async-connection-wrapper"] } +serde.workspace = true +uuid.workspace = true +dotenvy.workspace = true diff --git a/databases/diesel-async/README.md b/databases/diesel-async/README.md new file mode 100644 index 00000000..983beede --- /dev/null +++ b/databases/diesel-async/README.md @@ -0,0 +1,108 @@ +# diesel + +Basic integration of [Diesel-async](https://github.com/weiznich/diesel_async) using PostgreSQL for Actix Web. + +## Usage + +### Install PostgreSQL + +```sh +# on any OS +docker run -d --restart unless-stopped --name postgresql -e POSTGRES_USER=test-user -e POSTGRES_PASSWORD=password -p 5432:5432 -v postgres_data:/var/lib/postgresql/data postgres:alpine +``` + +### Initialize PostgreSQL Database + +```sh +cd databases/diesel-async +cargo install diesel_cli --no-default-features --features postgres + +echo DATABASE_URL=postgres://test-user:password@localhost:5432/test_db > .env +diesel setup +diesel migration run +``` + +The database will now be created in your PostgreSQL instance. +```sh +docker exec -i postgresql psql -U test-user -c "\l" +``` + +### Running Server + +```sh +cd databases/diesel-async +cargo run + +# Started http server: 127.0.0.1:8080 +``` + +### Available Routes + +#### `POST /item` + +Inserts a new item into the PostgreSQL DB. + +Provide a JSON payload with a name. Eg: + +```json +{ "name": "bill" } +``` + +On success, a response like the following is returned: + +```json +{ + "id": "01948982-67d0-7a55-b4b1-8b8b962d8c6b", + "name": "bill" +} +``` + +
+ Client Examples + +Using [HTTPie]: + +```sh +http POST localhost:8080/item name=bill +``` + +Using cURL: + +```sh +curl -S -X POST --header "Content-Type: application/json" --data '{"name":"bill"}' http://localhost:8080/item +``` + +
+ +#### `GET /item/{item_uid}` + +Gets an item from the DB using its UID (returned from the insert request or taken from the DB directly). Returns a 404 when no item exists with that UID. + +
+ Client Examples + +Using [HTTPie]: + +```sh +http localhost:8080/item/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 +``` + +Using cURL: + +```sh +curl -S http://localhost:8080/item/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 +``` + +
+ +### Explore The PostgreSQL DB + +```sh +docker exec -i postgresql psql -U test-user -d test_db -c "select * from public.items" +``` + +## Using Other Databases + +You can find a complete example of Diesel + PostgreSQL at: [https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix) + +[httpie]: https://httpie.io/cli diff --git a/databases/diesel-async/diesel.toml b/databases/diesel-async/diesel.toml new file mode 100644 index 00000000..34c0a182 --- /dev/null +++ b/databases/diesel-async/diesel.toml @@ -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/alex/CLionProjects/actix-with-async-diesel/migrations" diff --git a/databases/diesel-async/migrations/.keep b/databases/diesel-async/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/databases/diesel-async/migrations/00000000000000_diesel_initial_setup/down.sql b/databases/diesel-async/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 00000000..a9f52609 --- /dev/null +++ b/databases/diesel-async/migrations/00000000000000_diesel_initial_setup/down.sql @@ -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(); diff --git a/databases/diesel-async/migrations/00000000000000_diesel_initial_setup/up.sql b/databases/diesel-async/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 00000000..8335c232 --- /dev/null +++ b/databases/diesel-async/migrations/00000000000000_diesel_initial_setup/up.sql @@ -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; diff --git a/databases/diesel-async/migrations/2025-01-18-144029_create_items/down.sql b/databases/diesel-async/migrations/2025-01-18-144029_create_items/down.sql new file mode 100644 index 00000000..6839ef8e --- /dev/null +++ b/databases/diesel-async/migrations/2025-01-18-144029_create_items/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS items; diff --git a/databases/diesel-async/migrations/2025-01-18-144029_create_items/up.sql b/databases/diesel-async/migrations/2025-01-18-144029_create_items/up.sql new file mode 100644 index 00000000..7066001b --- /dev/null +++ b/databases/diesel-async/migrations/2025-01-18-144029_create_items/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS items +( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + name VARCHAR NOT NULL +); diff --git a/databases/diesel-async/src/actions.rs b/databases/diesel-async/src/actions.rs new file mode 100644 index 00000000..303d6284 --- /dev/null +++ b/databases/diesel-async/src/actions.rs @@ -0,0 +1,53 @@ +use diesel::prelude::*; +use uuid::{NoContext, Timestamp, Uuid}; + +use crate::models; + +use diesel_async::AsyncPgConnection; +use diesel_async::RunQueryDsl; + +type DbError = Box; + +// /// Run query using Diesel to find item by uid and return it. +pub async fn find_item_by_uid( + conn: &mut AsyncPgConnection, + uid: Uuid, +) -> Result, DbError> { + use super::schema::items::dsl::*; + + let item = items + .filter(id.eq(uid)) + .select(models::Item::as_select()) + // execute the query via the provided + // async `diesel_async::RunQueryDsl` + .first::(conn) + .await + .optional()?; + + Ok(item) +} + + /// Run query using Diesel to insert a new database row and return the result. +pub async fn insert_new_item( + conn: &mut AsyncPgConnection, + nm: &str, // prevent collision with `name` column imported inside the function +) -> Result { + // It is common when using Diesel with Actix Web to import schema-related + // modules inside a function's scope (rather than the normal module's scope) + // to prevent import collisions and namespace pollution. + use crate::schema::items::dsl::*; + + let new_item = models::Item { + id: Uuid::new_v7(Timestamp::now(NoContext)), + name: nm.to_owned(), + }; + + let item = diesel::insert_into(items) + .values(&new_item) + .returning(models::Item::as_returning()) + .get_result(conn) + .await + .expect("Error inserting person"); + + Ok(item) +} \ No newline at end of file diff --git a/databases/diesel-async/src/main.rs b/databases/diesel-async/src/main.rs new file mode 100644 index 00000000..cd69324b --- /dev/null +++ b/databases/diesel-async/src/main.rs @@ -0,0 +1,94 @@ +#[macro_use] +extern crate diesel; + +use std::env::VarError; + +use actix_web::{error, get, post, web, App, HttpResponse, HttpServer, Responder}; +use diesel_async::pooled_connection::{bb8::Pool, AsyncDieselConnectionManager, PoolError}; +use diesel_async::AsyncPgConnection; +use dotenvy::dotenv; +use std::{env, io}; +use thiserror::Error as ThisError; +use uuid::Uuid; + +pub mod actions; +pub mod models; +pub mod schema; + +type DbPool = Pool; + +/// Finds item by UID. +/// +/// Extracts: +/// - the database pool handle from application data +/// - an item UID from the request path +#[get("/items/{item_id}")] +async fn get_item( + pool: web::Data, + item_uid: web::Path, +) -> actix_web::Result { + let item_uid = item_uid.into_inner(); + + let mut conn = pool + .get() + .await + .expect("Couldn't get db connection from the pool"); + + let item = actions::find_item_by_uid(&mut conn, item_uid) + .await + // map diesel query errors to a 500 error response + .map_err(error::ErrorInternalServerError)?; + + Ok(match item { + // item was found; return 200 response with JSON formatted item object + Some(item) => HttpResponse::Ok().json(item), + + // item was not found; return 404 response with error message + None => HttpResponse::NotFound().body(format!("No item found with UID: {item_uid}")), + }) +} + +/// Creates new item. +/// +/// Extracts: +/// - the database pool handle from application data +/// - a JSON form containing new item info from the request body +#[post("/items")] +async fn add_item( + pool: web::Data, + form: web::Json, +) -> actix_web::Result { + + let mut conn = pool + .get() + .await + .expect("Couldn't get db connection from the pool"); + + let item = actions::insert_new_item(&mut conn, &form.name) + .await + // map diesel query errors to a 500 error response + .map_err(error::ErrorInternalServerError)?; + + // item was added successfully; return 201 response with new item info + Ok(HttpResponse::Created().json(item)) +} + +#[actix_web::main] +async fn main() -> io::Result<()> { + dotenv().ok(); + + let db_url = env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set"); + + let mgr = AsyncDieselConnectionManager::::new(db_url); + let pool = Pool::builder().build(mgr).await.unwrap(); + + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(pool.clone())) + .service(add_item) + .service(get_item) + }) + .bind(("127.0.0.1", 5000))? + .run() + .await +} diff --git a/databases/diesel-async/src/models.rs b/databases/diesel-async/src/models.rs new file mode 100644 index 00000000..e8cc61bf --- /dev/null +++ b/databases/diesel-async/src/models.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use super::schema::items; + +/// Item details. +#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable, Insertable)] +#[diesel(table_name = items)] +pub struct Item { + pub id: Uuid, + pub name: String, +} + +/// New item details. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct NewItem { + pub name: String, +} + +impl NewItem { + /// Constructs new item details from name. + #[cfg(test)] // only needed in tests + pub fn new(name: impl Into) -> Self { + Self { name: name.into(), ..Default::default() } + } +} diff --git a/databases/diesel-async/src/schema.rs b/databases/diesel-async/src/schema.rs new file mode 100644 index 00000000..a9038e33 --- /dev/null +++ b/databases/diesel-async/src/schema.rs @@ -0,0 +1,8 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + items (id) { + id -> Uuid, + name -> Varchar, + } +}