From 631c5f94720764a3e041fe735e569bb7893cd1a5 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 21 Jan 2025 20:27:58 +0300 Subject: [PATCH 01/13] feat: diesel-async usage example --- Cargo.toml | 1 + databases/diesel-async/Cargo.toml | 12 ++ databases/diesel-async/README.md | 108 ++++++++++++++++++ databases/diesel-async/diesel.toml | 9 ++ databases/diesel-async/migrations/.keep | 0 .../down.sql | 6 + .../up.sql | 36 ++++++ .../2025-01-18-144029_create_items/down.sql | 2 + .../2025-01-18-144029_create_items/up.sql | 6 + databases/diesel-async/src/actions.rs | 53 +++++++++ databases/diesel-async/src/main.rs | 94 +++++++++++++++ databases/diesel-async/src/models.rs | 25 ++++ databases/diesel-async/src/schema.rs | 8 ++ 13 files changed, 360 insertions(+) create mode 100644 databases/diesel-async/Cargo.toml create mode 100644 databases/diesel-async/README.md create mode 100644 databases/diesel-async/diesel.toml create mode 100644 databases/diesel-async/migrations/.keep create mode 100644 databases/diesel-async/migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 databases/diesel-async/migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 databases/diesel-async/migrations/2025-01-18-144029_create_items/down.sql create mode 100644 databases/diesel-async/migrations/2025-01-18-144029_create_items/up.sql create mode 100644 databases/diesel-async/src/actions.rs create mode 100644 databases/diesel-async/src/main.rs create mode 100644 databases/diesel-async/src/models.rs create mode 100644 databases/diesel-async/src/schema.rs 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, + } +} From 2b275e8ed15a2577b5eeea21fb6c863aec86a1e2 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 21 Jan 2025 20:41:27 +0300 Subject: [PATCH 02/13] chore: updated from main --- Cargo.lock | 78 +++++++++++++++++++++++++++ databases/diesel-async/src/actions.rs | 34 ++++++------ databases/diesel-async/src/main.rs | 28 +++++----- databases/diesel-async/src/models.rs | 4 +- 4 files changed, 109 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ced37c0f..6986b512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1692,6 +1692,18 @@ dependencies = [ "log", ] +[[package]] +name = "bb8" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89aabfae550a5c44b43ab941844ffcd2e993cb6900b342debf59e9ea74acdb8" +dependencies = [ + "async-trait", + "futures-util", + "parking_lot", + "tokio", +] + [[package]] name = "bigdecimal" version = "0.3.1" @@ -2584,6 +2596,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "db-diesel-async" +version = "1.0.0" +dependencies = [ + "actix-web", + "diesel", + "diesel-async", + "dotenvy", + "serde", + "uuid", +] + [[package]] name = "db-mongo" version = "1.0.0" @@ -2811,6 +2835,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "diesel-async" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a307ac00f7c23f526a04a77761a0519b9f0eb2838ebf5b905a58580095bdcb" +dependencies = [ + "async-trait", + "bb8", + "diesel", + "futures-util", + "scoped-futures", + "tokio", + "tokio-postgres", +] + [[package]] name = "diesel_derives" version = "2.2.3" @@ -4340,6 +4379,17 @@ dependencies = [ "libc", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + [[package]] name = "inotify" version = "0.11.0" @@ -5439,6 +5489,25 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.6.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify 0.9.6", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "notify" version = "6.1.1" @@ -7090,6 +7159,15 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "scoped-futures" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b24aae2d0636530f359e9d5ef0c04669d11c5e756699b27a6a6d845d8329091" +dependencies = [ + "pin-project-lite", +] + [[package]] name = "scopeguard" version = "1.2.0" diff --git a/databases/diesel-async/src/actions.rs b/databases/diesel-async/src/actions.rs index 303d6284..17ecb31d 100644 --- a/databases/diesel-async/src/actions.rs +++ b/databases/diesel-async/src/actions.rs @@ -27,27 +27,27 @@ pub async fn find_item_by_uid( Ok(item) } - /// Run query using Diesel to insert a new database row and return the result. +/// 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(), + // 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"); - + .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 index cd69324b..84d36691 100644 --- a/databases/diesel-async/src/main.rs +++ b/databases/diesel-async/src/main.rs @@ -1,19 +1,16 @@ #[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::pooled_connection::{bb8::Pool, AsyncDieselConnectionManager}; 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; +mod actions; +mod models; +mod schema; type DbPool = Pool; @@ -35,9 +32,9 @@ async fn get_item( .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)?; + .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 @@ -58,16 +55,15 @@ 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)?; + .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)) @@ -76,7 +72,7 @@ async fn add_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); @@ -88,7 +84,7 @@ async fn main() -> io::Result<()> { .service(add_item) .service(get_item) }) - .bind(("127.0.0.1", 5000))? + .bind(("127.0.0.1", 8080))? .run() .await } diff --git a/databases/diesel-async/src/models.rs b/databases/diesel-async/src/models.rs index e8cc61bf..6d08565f 100644 --- a/databases/diesel-async/src/models.rs +++ b/databases/diesel-async/src/models.rs @@ -1,6 +1,6 @@ +use super::schema::items; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::schema::items; /// Item details. #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Selectable, Insertable)] @@ -20,6 +20,6 @@ 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() } + Self { name: name.into() } } } From 1b14e6e98b25465cd7213c303e988674c1ddbee5 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 21 Jan 2025 21:14:29 +0300 Subject: [PATCH 03/13] feat: unit-tests added --- Cargo.lock | 2 + databases/diesel-async/Cargo.toml | 6 +- databases/diesel-async/src/actions.rs | 3 +- databases/diesel-async/src/main.rs | 104 ++++++++++++++++++++++++-- 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6986b512..f558be26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2604,6 +2604,8 @@ dependencies = [ "diesel", "diesel-async", "dotenvy", + "env_logger", + "log", "serde", "uuid", ] diff --git a/databases/diesel-async/Cargo.toml b/databases/diesel-async/Cargo.toml index 8443b577..fbd7827b 100644 --- a/databases/diesel-async/Cargo.toml +++ b/databases/diesel-async/Cargo.toml @@ -5,8 +5,10 @@ edition = "2021" [dependencies] actix-web.workspace = true -diesel = { version = "*", default-features = false, features = ["uuid"] } -diesel-async = { version = "*", features = ["postgres", "bb8", "async-connection-wrapper"] } +diesel = { version = "2", default-features = false, features = ["uuid"] } +diesel-async = { version = "0.5", features = ["postgres", "bb8", "async-connection-wrapper"] } serde.workspace = true uuid.workspace = true dotenvy.workspace = true +env_logger.workspace = true +log.workspace = true diff --git a/databases/diesel-async/src/actions.rs b/databases/diesel-async/src/actions.rs index 17ecb31d..2bbb2c94 100644 --- a/databases/diesel-async/src/actions.rs +++ b/databases/diesel-async/src/actions.rs @@ -18,8 +18,7 @@ pub async fn find_item_by_uid( let item = items .filter(id.eq(uid)) .select(models::Item::as_select()) - // execute the query via the provided - // async `diesel_async::RunQueryDsl` + // execute the query via the provided async `diesel_async::RunQueryDsl` .first::(conn) .await .optional()?; diff --git a/databases/diesel-async/src/main.rs b/databases/diesel-async/src/main.rs index 84d36691..b6cb829f 100644 --- a/databases/diesel-async/src/main.rs +++ b/databases/diesel-async/src/main.rs @@ -1,9 +1,11 @@ #[macro_use] extern crate diesel; -use actix_web::{error, get, post, web, App, HttpResponse, HttpServer, Responder}; -use diesel_async::pooled_connection::{bb8::Pool, AsyncDieselConnectionManager}; -use diesel_async::AsyncPgConnection; +use actix_web::{error, get, middleware, post, web, App, HttpResponse, HttpServer, Responder}; +use diesel_async::{ + pooled_connection::{bb8::Pool, AsyncDieselConnectionManager}, + AsyncPgConnection, +}; use dotenvy::dotenv; use std::{env, io}; use uuid::Uuid; @@ -72,15 +74,20 @@ async fn add_item( #[actix_web::main] async fn main() -> io::Result<()> { dotenv().ok(); + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); - let db_url = env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set"); + // initialize DB pool outside `HttpServer::new` so that it is shared across all workers + let pool = initialize_db_pool().await; - let mgr = AsyncDieselConnectionManager::::new(db_url); - let pool = Pool::builder().build(mgr).await.unwrap(); + log::info!("starting HTTP server at http://localhost:8080"); HttpServer::new(move || { App::new() + // add DB pool handle to app data; enables use of `web::Data` extractor .app_data(web::Data::new(pool.clone())) + // add request logger middleware + .wrap(middleware::Logger::default()) + // add route handlers .service(add_item) .service(get_item) }) @@ -88,3 +95,88 @@ async fn main() -> io::Result<()> { .run() .await } + +/// Initialize database connection pool based on `DATABASE_URL` environment variable. +/// +/// See more: . +async fn initialize_db_pool() -> DbPool { + let db_url = env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set"); + + let connection_manager = AsyncDieselConnectionManager::::new(db_url); + Pool::builder().build(connection_manager).await.unwrap() +} + +#[cfg(test)] +mod tests { + use actix_web::{http::StatusCode, test}; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + use super::*; + + #[actix_web::test] + async fn item_routes() { + dotenv().ok(); + env_logger::try_init_from_env(env_logger::Env::new().default_filter_or("info")).ok(); + + let pool = initialize_db_pool().await; + + let app = test::init_service( + App::new() + .app_data(web::Data::new(pool.clone())) + .wrap(middleware::Logger::default()) + .service(get_item) + .service(add_item), + ) + .await; + + // send something that isn't a UUID to `get_item` + let req = test::TestRequest::get().uri("/item/123").to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + let body = test::read_body(res).await; + assert!( + body.starts_with(b"UUID parsing failed"), + "unexpected body: {body:?}", + ); + + // try to find a non-existent item + let req = test::TestRequest::get() + .uri(&format!("/item/{}", Uuid::nil())) + .to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + let body = test::read_body(res).await; + assert!( + body.starts_with(b"No item found"), + "unexpected body: {body:?}", + ); + + // create new item + let req = test::TestRequest::post() + .uri("/item") + .set_json(models::NewItem::new("Test item")) + .to_request(); + let res: models::Item = test::call_and_read_body_json(&app, req).await; + assert_eq!(res.name, "Test item"); + + // get an item + let req = test::TestRequest::get() + .uri(&format!("/item/{}", res.id)) + .to_request(); + let res: models::Item = test::call_and_read_body_json(&app, req).await; + assert_eq!(res.name, "Test item"); + + // delete new item from table + use crate::schema::items::dsl::*; + diesel::delete(items.filter(id.eq(res.id))) + .execute( + &mut pool + .get() + .await + .expect("couldn't get db connection from pool"), + ) + .await + .expect("couldn't delete test item from table"); + } +} From e2a957eadd9f48f2c9fac0771ef8eb025dc4ffaf Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 21 Jan 2025 21:20:22 +0300 Subject: [PATCH 04/13] feat: fixed endpoints path prefix --- databases/diesel-async/README.md | 12 ++++++------ databases/diesel-async/src/main.rs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/databases/diesel-async/README.md b/databases/diesel-async/README.md index 983beede..d773cf9f 100644 --- a/databases/diesel-async/README.md +++ b/databases/diesel-async/README.md @@ -38,7 +38,7 @@ cargo run ### Available Routes -#### `POST /item` +#### `POST /items` Inserts a new item into the PostgreSQL DB. @@ -63,18 +63,18 @@ On success, a response like the following is returned: Using [HTTPie]: ```sh -http POST localhost:8080/item name=bill +http POST localhost:8080/items name=bill ``` Using cURL: ```sh -curl -S -X POST --header "Content-Type: application/json" --data '{"name":"bill"}' http://localhost:8080/item +curl -S -X POST --header "Content-Type: application/json" --data '{"name":"bill"}' http://localhost:8080/items ``` -#### `GET /item/{item_uid}` +#### `GET /items/{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. @@ -84,13 +84,13 @@ Gets an item from the DB using its UID (returned from the insert request or take Using [HTTPie]: ```sh -http localhost:8080/item/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 +http localhost:8080/items/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 ``` Using cURL: ```sh -curl -S http://localhost:8080/item/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 +curl -S http://localhost:8080/items/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 ``` diff --git a/databases/diesel-async/src/main.rs b/databases/diesel-async/src/main.rs index b6cb829f..cc64f54d 100644 --- a/databases/diesel-async/src/main.rs +++ b/databases/diesel-async/src/main.rs @@ -131,7 +131,7 @@ mod tests { .await; // send something that isn't a UUID to `get_item` - let req = test::TestRequest::get().uri("/item/123").to_request(); + let req = test::TestRequest::get().uri("/items/123").to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); let body = test::read_body(res).await; @@ -142,7 +142,7 @@ mod tests { // try to find a non-existent item let req = test::TestRequest::get() - .uri(&format!("/item/{}", Uuid::nil())) + .uri(&format!("/items/{}", Uuid::nil())) .to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); @@ -154,7 +154,7 @@ mod tests { // create new item let req = test::TestRequest::post() - .uri("/item") + .uri("/items") .set_json(models::NewItem::new("Test item")) .to_request(); let res: models::Item = test::call_and_read_body_json(&app, req).await; @@ -162,7 +162,7 @@ mod tests { // get an item let req = test::TestRequest::get() - .uri(&format!("/item/{}", res.id)) + .uri(&format!("/items/{}", res.id)) .to_request(); let res: models::Item = test::call_and_read_body_json(&app, req).await; assert_eq!(res.name, "Test item"); From f53394c132a54dc5d66e00c10cfdf86687a7a58b Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Wed, 22 Jan 2025 07:55:45 +0300 Subject: [PATCH 05/13] feat: added postgresql feature to the ci diesel_cli installation --- .github/workflows/ci-nightly.yml | 2 +- .github/workflows/ci.yml | 2 +- databases/diesel-async/README.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 46949b54..105704a3 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -24,7 +24,7 @@ jobs: - name: Install DB CLI tools run: | cargo install --force sqlx-cli --no-default-features --features=sqlite,rustls - cargo install --force diesel_cli --no-default-features --features sqlite + cargo install --force diesel_cli --no-default-features --features=sqlite,postgres - name: Create Test DBs env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5eeb7da..1b4925e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Install DB CLI tools run: | cargo install --force sqlx-cli --no-default-features --features=sqlite,rustls - cargo install --force diesel_cli --no-default-features --features sqlite + cargo install --force diesel_cli --no-default-features --features=sqlite,postgres - name: Create Test DBs env: diff --git a/databases/diesel-async/README.md b/databases/diesel-async/README.md index d773cf9f..8eae6998 100644 --- a/databases/diesel-async/README.md +++ b/databases/diesel-async/README.md @@ -45,7 +45,7 @@ Inserts a new item into the PostgreSQL DB. Provide a JSON payload with a name. Eg: ```json -{ "name": "bill" } +{ "name": "thingamajig" } ``` On success, a response like the following is returned: @@ -53,7 +53,7 @@ On success, a response like the following is returned: ```json { "id": "01948982-67d0-7a55-b4b1-8b8b962d8c6b", - "name": "bill" + "name": "thingamajig" } ``` @@ -63,13 +63,13 @@ On success, a response like the following is returned: Using [HTTPie]: ```sh -http POST localhost:8080/items name=bill +http POST localhost:8080/items name=thingamajig ``` Using cURL: ```sh -curl -S -X POST --header "Content-Type: application/json" --data '{"name":"bill"}' http://localhost:8080/items +curl -S -X POST --header "Content-Type: application/json" --data '{"name":"thingamajig"}' http://localhost:8080/items ``` From 14fbe6d334d72e50cdcd8afd9d11ea881ac34fda Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Wed, 22 Jan 2025 08:26:15 +0300 Subject: [PATCH 06/13] feat: fixed link to details about pooled connection initialization --- databases/diesel-async/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/databases/diesel-async/src/main.rs b/databases/diesel-async/src/main.rs index cc64f54d..c4ea5ad6 100644 --- a/databases/diesel-async/src/main.rs +++ b/databases/diesel-async/src/main.rs @@ -98,7 +98,7 @@ async fn main() -> io::Result<()> { /// Initialize database connection pool based on `DATABASE_URL` environment variable. /// -/// See more: . +/// See more: . async fn initialize_db_pool() -> DbPool { let db_url = env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set"); From 661438af4ae23f4d69ac9a595b12c4c1456076cb Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Wed, 22 Jan 2025 11:02:14 +0300 Subject: [PATCH 07/13] feat: added docker list containers command to the diesel-async README.md to check if postgres is up and running --- databases/diesel-async/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/databases/diesel-async/README.md b/databases/diesel-async/README.md index 8eae6998..891e55b5 100644 --- a/databases/diesel-async/README.md +++ b/databases/diesel-async/README.md @@ -10,6 +10,11 @@ Basic integration of [Diesel-async](https://github.com/weiznich/diesel_async) us # 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 ``` +make sure it has successfully started up and is running +```sh +# on any OS +docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}" +``` ### Initialize PostgreSQL Database From dbea1221d82e0ef775844ba79120f554bb21bd1b Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Sat, 8 Feb 2025 20:49:01 +0300 Subject: [PATCH 08/13] build: updated from main --- Cargo.lock | 36 ++++++++++++++++++++++++++- Cargo.toml | 2 +- databases/diesel-async/src/actions.rs | 2 +- databases/diesel-async/src/main.rs | 8 +++--- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f558be26..32ae66c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2640,7 +2640,7 @@ version = "1.0.0" dependencies = [ "actix-web", "env_logger", - "redis 0.27.6", + "redis 0.28.2", "serde", "tracing", ] @@ -6578,6 +6578,29 @@ dependencies = [ "pin-project-lite", "ryu", "sha1_smol", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redis" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37ec3fd44bea2ec947ba6cc7634d7999a6590aca7c35827c250bc0de502bda6" +dependencies = [ + "arc-swap", + "backon", + "bytes", + "combine", + "futures-channel", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", "socket2", "tokio", "tokio-util", @@ -10056,6 +10079,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index 0edc9c60..ff4f9586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,7 +110,7 @@ openssl = { version = "0.10.60", features = ["v110"] } parking_lot = "0.12" pin-project-lite = "0.2" rand = "0.9" -redis = { version = "0.27" } +redis = { version = "0.28" } reqwest = { version = "0.12", features = ["json", "stream"] } rustls = "0.23" rustls-pemfile = "2" diff --git a/databases/diesel-async/src/actions.rs b/databases/diesel-async/src/actions.rs index 2bbb2c94..542f5662 100644 --- a/databases/diesel-async/src/actions.rs +++ b/databases/diesel-async/src/actions.rs @@ -9,7 +9,7 @@ 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( +pub async fn find_item_by_id( conn: &mut AsyncPgConnection, uid: Uuid, ) -> Result, DbError> { diff --git a/databases/diesel-async/src/main.rs b/databases/diesel-async/src/main.rs index c4ea5ad6..b499a5cd 100644 --- a/databases/diesel-async/src/main.rs +++ b/databases/diesel-async/src/main.rs @@ -24,16 +24,16 @@ type DbPool = Pool; #[get("/items/{item_id}")] async fn get_item( pool: web::Data, - item_uid: web::Path, + item_id: web::Path, ) -> actix_web::Result { - let item_uid = item_uid.into_inner(); + let item_id = item_id.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) + let item = actions::find_item_by_id(&mut conn, item_id) .await // map diesel query errors to a 500 error response .map_err(error::ErrorInternalServerError)?; @@ -43,7 +43,7 @@ async fn get_item( 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}")), + None => HttpResponse::NotFound().body(format!("No item found with UID: {item_id}")), }) } From 8681b2a1563aa8896e8f429c9899481faeda04db Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 25 Feb 2025 07:23:07 +0000 Subject: [PATCH 09/13] ci: install libpq-dev --- .github/workflows/ci-nightly.yml | 7 ++++++- .github/workflows/ci.yml | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 105704a3..dd6f7445 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -21,6 +21,12 @@ jobs: with: toolchain: ${{ matrix.version }} + - name: Install system packages + run: | + sudo apt-get update + sudo apt-get -y install sqlite3 + sudo apt-get -y install libpq-dev + - name: Install DB CLI tools run: | cargo install --force sqlx-cli --no-default-features --features=sqlite,rustls @@ -30,7 +36,6 @@ jobs: env: DATABASE_URL: sqlite://./todo.db run: | - sudo apt-get update && sudo apt-get install sqlite3 sqlx database create chmod a+rwx ./todo.db sqlx migrate run --source=./basics/todo/migrations diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b4925e6..11a0495c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,12 @@ jobs: with: toolchain: ${{ matrix.version }} + - name: Install system packages + run: | + sudo apt-get update + sudo apt-get -y install sqlite3 + sudo apt-get -y install libpq-dev + - name: Install DB CLI tools run: | cargo install --force sqlx-cli --no-default-features --features=sqlite,rustls @@ -35,7 +41,6 @@ jobs: env: DATABASE_URL: sqlite://./todo.db run: | - sudo apt-get update && sudo apt-get install sqlite3 sqlx database create chmod a+rwx ./todo.db sqlx migrate run --source=./basics/todo/migrations From c8da70bb0a0916f0f72b85c55189a802fdb06012 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 25 Feb 2025 10:56:47 +0300 Subject: [PATCH 10/13] fix(build): fix duplicate conflicts in Cargo.lock --- Cargo.lock | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32ae66c5..d2264778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4381,17 +4381,6 @@ dependencies = [ "libc", ] -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - [[package]] name = "inotify" version = "0.11.0" @@ -5491,25 +5480,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.6.0", - "crossbeam-channel", - "filetime", - "fsevent-sys", - "inotify 0.9.6", - "kqueue", - "libc", - "log", - "mio 0.8.11", - "walkdir", - "windows-sys 0.48.0", -] - [[package]] name = "notify" version = "6.1.1" @@ -10079,17 +10049,6 @@ dependencies = [ "syn 2.0.98", ] -[[package]] -name = "zerocopy-derive" -version = "0.8.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.93", -] - [[package]] name = "zerofrom" version = "0.1.5" From 9a25c3b2339cd857edcd531f0340a5da989a264e Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Fri, 28 Feb 2025 12:17:26 +0300 Subject: [PATCH 11/13] fix(tests): ignore tests if postgres isn't up --- databases/diesel-async/Cargo.toml | 3 +++ databases/diesel-async/src/main.rs | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/databases/diesel-async/Cargo.toml b/databases/diesel-async/Cargo.toml index fbd7827b..fbf35f2f 100644 --- a/databases/diesel-async/Cargo.toml +++ b/databases/diesel-async/Cargo.toml @@ -3,6 +3,9 @@ name = "db-diesel-async" version = "1.0.0" edition = "2021" +[features] +postgres_tests = [] + [dependencies] actix-web.workspace = true diesel = { version = "2", default-features = false, features = ["uuid"] } diff --git a/databases/diesel-async/src/main.rs b/databases/diesel-async/src/main.rs index b499a5cd..5d315c01 100644 --- a/databases/diesel-async/src/main.rs +++ b/databases/diesel-async/src/main.rs @@ -106,7 +106,8 @@ async fn initialize_db_pool() -> DbPool { Pool::builder().build(connection_manager).await.unwrap() } -#[cfg(test)] +#[cfg(not(feature = "postgres_tests"))] +#[allow(unused_imports)] mod tests { use actix_web::{http::StatusCode, test}; use diesel::prelude::*; From 94f20bb48180d84175224d1941aae2041197d060 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Fri, 28 Feb 2025 12:26:21 +0300 Subject: [PATCH 12/13] fix(tests): ignore tests if postgres isn't up --- databases/diesel-async/src/models.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/databases/diesel-async/src/models.rs b/databases/diesel-async/src/models.rs index 6d08565f..610839ba 100644 --- a/databases/diesel-async/src/models.rs +++ b/databases/diesel-async/src/models.rs @@ -16,6 +16,7 @@ pub struct NewItem { pub name: String, } +#[cfg(not(feature = "postgres_tests"))] impl NewItem { /// Constructs new item details from name. #[cfg(test)] // only needed in tests From f7f01a179d412f84c5970cbeebd2b15cd337b90c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 9 Apr 2025 22:40:38 +0100 Subject: [PATCH 13/13] Update Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ff4f9586..0edc9c60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,7 +110,7 @@ openssl = { version = "0.10.60", features = ["v110"] } parking_lot = "0.12" pin-project-lite = "0.2" rand = "0.9" -redis = { version = "0.28" } +redis = { version = "0.27" } reqwest = { version = "0.12", features = ["json", "stream"] } rustls = "0.23" rustls-pemfile = "2"