diff --git a/examples/http-axum-diesel/Cargo.toml b/examples/http-axum-diesel/Cargo.toml new file mode 100644 index 00000000..dd37346f --- /dev/null +++ b/examples/http-axum-diesel/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "http-axum-diesel" +version = "0.1.0" +edition = "2021" + + +# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) +# to manage dependencies. +# Running `cargo add DEPENDENCY_NAME` will +# add the latest version of a dependency to the list, +# and it will keep the alphabetic ordering for you. + +[dependencies] +axum = "0.6.4" +bb8 = "0.8.0" +diesel = "2.0.3" +diesel-async = { version = "0.2.1", features = ["postgres", "bb8"] } +lambda_http = { path = "../../lambda-http" } +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1.0.159" +tokio = { version = "1", features = ["macros"] } +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "fmt"] } diff --git a/examples/http-axum-diesel/README.md b/examples/http-axum-diesel/README.md new file mode 100644 index 00000000..8b2330f5 --- /dev/null +++ b/examples/http-axum-diesel/README.md @@ -0,0 +1,13 @@ +# AWS Lambda Function example + +This example shows how to develop a REST API with Axum and Diesel that connects to a Postgres database. + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/down.sql b/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/down.sql new file mode 100644 index 00000000..e00da655 --- /dev/null +++ b/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE posts \ No newline at end of file diff --git a/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/up.sql b/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/up.sql new file mode 100644 index 00000000..aa684de6 --- /dev/null +++ b/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + title VARCHAR NOT NULL, + content TEXT NOT NULL, + published BOOLEAN NOT NULL DEFAULT FALSE +) \ No newline at end of file diff --git a/examples/http-axum-diesel/src/main.rs b/examples/http-axum-diesel/src/main.rs new file mode 100644 index 00000000..227e23dd --- /dev/null +++ b/examples/http-axum-diesel/src/main.rs @@ -0,0 +1,122 @@ +use axum::{ + extract::{Path, State}, + response::Json, + routing::get, + Router, +}; +use bb8::Pool; +use diesel::prelude::*; +use diesel_async::{pooled_connection::AsyncDieselConnectionManager, AsyncPgConnection, RunQueryDsl}; +use lambda_http::{http::StatusCode, run, Error}; +use serde::{Deserialize, Serialize}; + +table! { + posts (id) { + id -> Integer, + title -> Text, + content -> Text, + published -> Bool, + } +} + +#[derive(Default, Queryable, Selectable, Serialize)] +struct Post { + id: i32, + title: String, + content: String, + published: bool, +} + +#[derive(Deserialize, Insertable)] +#[diesel(table_name = posts)] +struct NewPost { + title: String, + content: String, + published: bool, +} + +type AsyncPool = Pool>; +type ServerError = (StatusCode, String); + +async fn create_post(State(pool): State, Json(post): Json) -> Result, ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + let post = diesel::insert_into(posts::table) + .values(post) + .returning(Post::as_returning()) + .get_result(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(Json(post)) +} + +async fn list_posts(State(pool): State) -> Result>, ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + let posts = posts::table + .filter(posts::dsl::published.eq(true)) + .load(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(Json(posts)) +} + +async fn get_post(State(pool): State, Path(post_id): Path) -> Result, ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + let post = posts::table + .find(post_id) + .first(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(Json(post)) +} + +async fn delete_post(State(pool): State, Path(post_id): Path) -> Result<(), ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + diesel::delete(posts::table.find(post_id)) + .execute(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(()) +} + +fn internal_server_error(err: E) -> ServerError { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + // disable printing the name of the module in every log line. + .with_target(false) + // this needs to be set to false, otherwise ANSI color codes will + // show up in a confusing manner in CloudWatch logs. + .with_ansi(false) + // disabling time is handy because CloudWatch will add the ingestion time. + .without_time() + .init(); + + // Set up the database connection + let db_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL environment variable"); + let config = AsyncDieselConnectionManager::::new(db_url); + let connection = Pool::builder() + .build(config) + .await + .expect("unable to establish the database connection"); + + // Set up the API routes + let posts_api = Router::new() + .route("/", get(list_posts).post(create_post)) + .route("/:id", get(get_post).delete(delete_post)); + let app = Router::new().nest("/posts", posts_api).with_state(connection); + + run(app).await +}