From 8884bacc18094ae0131a851347d1cb525e49ca9f Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Mon, 27 Jan 2025 15:35:15 +0300 Subject: [PATCH 01/12] feat: base sqlx integration example --- sqlx/.gitignore | 2 + sqlx/Cargo.toml | 16 + sqlx/README.md | 49 ++ ...20220205163207_create_tasks_table.down.sql | 1 + .../20220205163207_create_tasks_table.up.sql | 5 + sqlx/src/api.rs | 148 ++++++ sqlx/src/db.rs | 36 ++ sqlx/src/main.rs | 68 +++ sqlx/src/model.rs | 73 +++ sqlx/src/session.rs | 39 ++ sqlx/static/css/normalize.css | 427 ++++++++++++++++++ sqlx/static/css/skeleton.css | 421 +++++++++++++++++ sqlx/static/css/style.css | 58 +++ sqlx/static/errors/400.html | 21 + sqlx/static/errors/404.html | 22 + sqlx/static/errors/500.html | 24 + sqlx/templates/index.html.tera | 65 +++ 17 files changed, 1475 insertions(+) create mode 100644 sqlx/.gitignore create mode 100644 sqlx/Cargo.toml create mode 100644 sqlx/README.md create mode 100644 sqlx/migrations/20220205163207_create_tasks_table.down.sql create mode 100644 sqlx/migrations/20220205163207_create_tasks_table.up.sql create mode 100644 sqlx/src/api.rs create mode 100644 sqlx/src/db.rs create mode 100644 sqlx/src/main.rs create mode 100644 sqlx/src/model.rs create mode 100644 sqlx/src/session.rs create mode 100644 sqlx/static/css/normalize.css create mode 100644 sqlx/static/css/skeleton.css create mode 100644 sqlx/static/css/style.css create mode 100644 sqlx/static/errors/400.html create mode 100644 sqlx/static/errors/404.html create mode 100644 sqlx/static/errors/500.html create mode 100644 sqlx/templates/index.html.tera diff --git a/sqlx/.gitignore b/sqlx/.gitignore new file mode 100644 index 0000000..e2f287b --- /dev/null +++ b/sqlx/.gitignore @@ -0,0 +1,2 @@ +todo.db +todo.db-* diff --git a/sqlx/Cargo.toml b/sqlx/Cargo.toml new file mode 100644 index 0000000..20a9e29 --- /dev/null +++ b/sqlx/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "todo" +version = "1.0.0" +edition = "2021" + +[dependencies] +actix-files.workspace = true +actix-session = { workspace = true, features = ["cookie-session"] } +actix-web.workspace = true + +dotenvy.workspace = true +env_logger.workspace = true +log.workspace = true +serde.workspace = true +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] } +tera = "1.5" diff --git a/sqlx/README.md b/sqlx/README.md new file mode 100644 index 0000000..2b8df3c --- /dev/null +++ b/sqlx/README.md @@ -0,0 +1,49 @@ +# Todo + +A simple Todo project using a SQLite database. + +## Prerequisites + +- SQLite 3 + +## Change Into The Examples Workspace Root Directory + +All instructions assume you have changed into the examples workspace root: + +```console +$ pwd +.../examples +``` + +## Set Up The Database + +Install the [sqlx](https://github.com/launchbadge/sqlx/tree/HEAD/sqlx-cli) command-line tool with the required features: + +```sh +cargo install sqlx-cli --no-default-features --features=rustls,sqlite +``` + +Then to create and set-up the database run: + +```sh +sqlx database create --database-url=sqlite://./todo.db +sqlx migrate run --database-url=sqlite://./todo.db +``` + +## Run The Application + +Start the application with: + +```sh +cargo run --bin=todo +``` + +The app will be viewable in the browser at . + +## Modifying The Example Database + +For simplicity, this example uses SQLx's offline mode. If you make any changes to the database schema, this must be turned off or the `sqlx-data.json` file regenerated using the following command: + +```sh +cargo sqlx prepare --database-url=sqlite://./todo.db +``` diff --git a/sqlx/migrations/20220205163207_create_tasks_table.down.sql b/sqlx/migrations/20220205163207_create_tasks_table.down.sql new file mode 100644 index 0000000..87f1ed5 --- /dev/null +++ b/sqlx/migrations/20220205163207_create_tasks_table.down.sql @@ -0,0 +1 @@ +DROP TABLE tasks; diff --git a/sqlx/migrations/20220205163207_create_tasks_table.up.sql b/sqlx/migrations/20220205163207_create_tasks_table.up.sql new file mode 100644 index 0000000..20d5bd2 --- /dev/null +++ b/sqlx/migrations/20220205163207_create_tasks_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE tasks ( + id INTEGER PRIMARY KEY, + description VARCHAR NOT NULL, + completed BOOLEAN NOT NULL DEFAULT 'f' +); diff --git a/sqlx/src/api.rs b/sqlx/src/api.rs new file mode 100644 index 0000000..caaefd6 --- /dev/null +++ b/sqlx/src/api.rs @@ -0,0 +1,148 @@ +use actix_files::NamedFile; +use actix_session::Session; +use actix_web::{ + dev, error, http::StatusCode, middleware::ErrorHandlerResponse, web, Error, HttpResponse, + Responder, Result, +}; +use serde::Deserialize; +use sqlx::SqlitePool; +use tera::{Context, Tera}; + +use crate::{ + db, + session::{self, FlashMessage}, +}; + +pub async fn index( + pool: web::Data, + tmpl: web::Data, + session: Session, +) -> Result { + let tasks = db::get_all_tasks(&pool) + .await + .map_err(error::ErrorInternalServerError)?; + + let mut context = Context::new(); + context.insert("tasks", &tasks); + + // Session is set during operations on other endpoints that can redirect to index + if let Some(flash) = session::get_flash(&session)? { + context.insert("msg", &(flash.kind, flash.message)); + session::clear_flash(&session); + } + + let rendered = tmpl + .render("index.html.tera", &context) + .map_err(error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().body(rendered)) +} + +#[derive(Debug, Deserialize)] +pub struct CreateForm { + description: String, +} + +pub async fn create( + params: web::Form, + pool: web::Data, + session: Session, +) -> Result { + if params.description.is_empty() { + session::set_flash(&session, FlashMessage::error("Description cannot be empty"))?; + Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND)) + } else { + db::create_task(params.into_inner().description, &pool) + .await + .map_err(error::ErrorInternalServerError)?; + + session::set_flash(&session, FlashMessage::success("Task successfully added"))?; + + Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND)) + } +} + +#[derive(Debug, Deserialize)] +pub struct UpdateParams { + id: i32, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateForm { + _method: String, +} + +pub async fn update( + db: web::Data, + params: web::Path, + form: web::Form, + session: Session, +) -> Result { + Ok(web::Redirect::to(match form._method.as_ref() { + "put" => toggle(db, params).await?, + "delete" => delete(db, params, session).await?, + unsupported_method => { + let msg = format!("Unsupported HTTP method: {unsupported_method}"); + return Err(error::ErrorBadRequest(msg)); + } + }) + .using_status_code(StatusCode::FOUND)) +} + +async fn toggle( + pool: web::Data, + params: web::Path, +) -> Result<&'static str, Error> { + db::toggle_task(params.id, &pool) + .await + .map_err(error::ErrorInternalServerError)?; + + Ok("/") +} + +async fn delete( + pool: web::Data, + params: web::Path, + session: Session, +) -> Result<&'static str, Error> { + db::delete_task(params.id, &pool) + .await + .map_err(error::ErrorInternalServerError)?; + + session::set_flash(&session, FlashMessage::success("Task was deleted."))?; + + Ok("/") +} + +pub fn bad_request(res: dev::ServiceResponse) -> Result> { + let new_resp = NamedFile::open("static/errors/400.html")? + .customize() + .with_status(res.status()) + .respond_to(res.request()) + .map_into_boxed_body() + .map_into_right_body(); + + Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) +} + +pub fn not_found(res: dev::ServiceResponse) -> Result> { + let new_resp = NamedFile::open("static/errors/404.html")? + .customize() + .with_status(res.status()) + .respond_to(res.request()) + .map_into_boxed_body() + .map_into_right_body(); + + Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) +} + +pub fn internal_server_error(res: dev::ServiceResponse) -> Result> { + let new_resp = NamedFile::open("static/errors/500.html")? + .customize() + .with_status(res.status()) + .respond_to(res.request()) + .map_into_boxed_body() + .map_into_right_body(); + + Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) +} diff --git a/sqlx/src/db.rs b/sqlx/src/db.rs new file mode 100644 index 0000000..6791bff --- /dev/null +++ b/sqlx/src/db.rs @@ -0,0 +1,36 @@ +use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; + +use crate::model::{NewTask, Task}; + +pub async fn init_pool(database_url: &str) -> Result { + SqlitePoolOptions::new() + .acquire_timeout(std::time::Duration::from_secs(1)) + .connect(database_url) + .await +} + +pub async fn get_all_tasks(pool: &SqlitePool) -> Result, &'static str> { + Task::all(pool).await.map_err(|_| "Error retrieving tasks") +} + +pub async fn create_task(todo: String, pool: &SqlitePool) -> Result<(), &'static str> { + let new_task = NewTask { description: todo }; + Task::insert(new_task, pool) + .await + .map(|_| ()) + .map_err(|_| "Error inserting task") +} + +pub async fn toggle_task(id: i32, pool: &SqlitePool) -> Result<(), &'static str> { + Task::toggle_with_id(id, pool) + .await + .map(|_| ()) + .map_err(|_| "Error toggling task completion") +} + +pub async fn delete_task(id: i32, pool: &SqlitePool) -> Result<(), &'static str> { + Task::delete_with_id(id, pool) + .await + .map(|_| ()) + .map_err(|_| "Error deleting task") +} diff --git a/sqlx/src/main.rs b/sqlx/src/main.rs new file mode 100644 index 0000000..8965b46 --- /dev/null +++ b/sqlx/src/main.rs @@ -0,0 +1,68 @@ +use std::{env, io}; + +use actix_files::Files; +use actix_session::{storage::CookieSessionStore, SessionMiddleware}; +use actix_web::{ + http, + middleware::{ErrorHandlers, Logger}, + web, App, HttpServer, +}; +use dotenvy::dotenv; +use tera::Tera; + +mod api; +mod db; +mod model; +mod session; + +// NOTE: Not a suitable session key for production. +static SESSION_SIGNING_KEY: &[u8] = &[0; 64]; + +#[actix_web::main] +async fn main() -> io::Result<()> { + dotenv().ok(); + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let key = actix_web::cookie::Key::from(SESSION_SIGNING_KEY); + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let pool = db::init_pool(&database_url) + .await + .expect("Failed to create pool"); + + log::info!("starting HTTP server at http://localhost:8080"); + + HttpServer::new(move || { + log::debug!("Constructing the App"); + + let mut templates = Tera::new("templates/**/*").expect("errors in tera templates"); + templates.autoescape_on(vec!["tera"]); + + let session_store = SessionMiddleware::builder(CookieSessionStore::default(), key.clone()) + .cookie_secure(false) + .build(); + + let error_handlers = ErrorHandlers::new() + .handler( + http::StatusCode::INTERNAL_SERVER_ERROR, + api::internal_server_error, + ) + .handler(http::StatusCode::BAD_REQUEST, api::bad_request) + .handler(http::StatusCode::NOT_FOUND, api::not_found); + + App::new() + .app_data(web::Data::new(templates)) + .app_data(web::Data::new(pool.clone())) + .wrap(Logger::default()) + .wrap(session_store) + .wrap(error_handlers) + .service(web::resource("/").route(web::get().to(api::index))) + .service(web::resource("/todo").route(web::post().to(api::create))) + .service(web::resource("/todo/{id}").route(web::post().to(api::update))) + .service(Files::new("/static", "./static")) + }) + .bind(("127.0.0.1", 8080))? + .workers(2) + .run() + .await +} diff --git a/sqlx/src/model.rs b/sqlx/src/model.rs new file mode 100644 index 0000000..4166233 --- /dev/null +++ b/sqlx/src/model.rs @@ -0,0 +1,73 @@ +use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; + +#[derive(Debug)] +pub struct NewTask { + pub description: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Task { + pub id: i64, + pub description: String, + pub completed: bool, +} + +impl Task { + pub async fn all(connection: &SqlitePool) -> Result, sqlx::Error> { + let tasks = sqlx::query_as!( + Task, + r#" + SELECT * + FROM tasks + "# + ) + .fetch_all(connection) + .await?; + + Ok(tasks) + } + + pub async fn insert(todo: NewTask, connection: &SqlitePool) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO tasks (description) + VALUES ($1) + "#, + todo.description, + ) + .execute(connection) + .await?; + + Ok(()) + } + + pub async fn toggle_with_id(id: i32, connection: &SqlitePool) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE tasks + SET completed = NOT completed + WHERE id = $1 + "#, + id + ) + .execute(connection) + .await?; + + Ok(()) + } + + pub async fn delete_with_id(id: i32, connection: &SqlitePool) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM tasks + WHERE id = $1 + "#, + id + ) + .execute(connection) + .await?; + + Ok(()) + } +} diff --git a/sqlx/src/session.rs b/sqlx/src/session.rs new file mode 100644 index 0000000..5230549 --- /dev/null +++ b/sqlx/src/session.rs @@ -0,0 +1,39 @@ +use actix_session::Session; +use actix_web::error::Result; +use serde::{Deserialize, Serialize}; + +const FLASH_KEY: &str = "flash"; + +pub fn set_flash(session: &Session, flash: FlashMessage) -> Result<()> { + Ok(session.insert(FLASH_KEY, flash)?) +} + +pub fn get_flash(session: &Session) -> Result> { + Ok(session.get::(FLASH_KEY)?) +} + +pub fn clear_flash(session: &Session) { + session.remove(FLASH_KEY); +} + +#[derive(Deserialize, Serialize)] +pub struct FlashMessage { + pub kind: String, + pub message: String, +} + +impl FlashMessage { + pub fn success(message: &str) -> Self { + Self { + kind: "success".to_owned(), + message: message.to_owned(), + } + } + + pub fn error(message: &str) -> Self { + Self { + kind: "error".to_owned(), + message: message.to_owned(), + } + } +} diff --git a/sqlx/static/css/normalize.css b/sqlx/static/css/normalize.css new file mode 100644 index 0000000..458eea1 --- /dev/null +++ b/sqlx/static/css/normalize.css @@ -0,0 +1,427 @@ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/sqlx/static/css/skeleton.css b/sqlx/static/css/skeleton.css new file mode 100644 index 0000000..4855df4 --- /dev/null +++ b/sqlx/static/css/skeleton.css @@ -0,0 +1,421 @@ +/* +* Skeleton V2.0.4 +* Copyright 2014, Dave Gamache +* www.getskeleton.com +* Free to use under the MIT license. +* http://www.opensource.org/licenses/mit-license.php +* 12/29/2014 +*/ + + +/* Table of contents +–––––––––––––––––––––––––––––––––––––––––––––––––– +- Grid +- Base Styles +- Typography +- Links +- Buttons +- Forms +- Lists +- Code +- Tables +- Spacing +- Utilities +- Clearing +- Media Queries +*/ + + +/* Grid +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.container { + position: relative; + width: 100%; + max-width: 960px; + margin: 0 auto; + padding: 0 20px; + box-sizing: border-box; } +.column, +.columns { + width: 100%; + float: left; + box-sizing: border-box; } + +/* For devices larger than 400px */ +@media (min-width: 400px) { + .container { + width: 85%; + padding: 0; } +} + +/* For devices larger than 550px */ +@media (min-width: 550px) { + .container { + width: 80%; } + .column, + .columns { + margin-left: 4%; } + .column:first-child, + .columns:first-child { + margin-left: 0; } + + .one.column, + .one.columns { width: 4.66666666667%; } + .two.columns { width: 13.3333333333%; } + .three.columns { width: 22%; } + .four.columns { width: 30.6666666667%; } + .five.columns { width: 39.3333333333%; } + .six.columns { width: 48%; } + .seven.columns { width: 56.6666666667%; } + .eight.columns { width: 65.3333333333%; } + .nine.columns { width: 74.0%; } + .ten.columns { width: 82.6666666667%; } + .eleven.columns { width: 91.3333333333%; } + .twelve.columns { width: 100%; margin-left: 0; } + + .one-third.column { width: 30.6666666667%; } + .two-thirds.column { width: 65.3333333333%; } + + .one-half.column { width: 48%; } + + /* Offsets */ + .offset-by-one.column, + .offset-by-one.columns { margin-left: 8.66666666667%; } + .offset-by-two.column, + .offset-by-two.columns { margin-left: 17.3333333333%; } + .offset-by-three.column, + .offset-by-three.columns { margin-left: 26%; } + .offset-by-four.column, + .offset-by-four.columns { margin-left: 34.6666666667%; } + .offset-by-five.column, + .offset-by-five.columns { margin-left: 43.3333333333%; } + .offset-by-six.column, + .offset-by-six.columns { margin-left: 52%; } + .offset-by-seven.column, + .offset-by-seven.columns { margin-left: 60.6666666667%; } + .offset-by-eight.column, + .offset-by-eight.columns { margin-left: 69.3333333333%; } + .offset-by-nine.column, + .offset-by-nine.columns { margin-left: 78.0%; } + .offset-by-ten.column, + .offset-by-ten.columns { margin-left: 86.6666666667%; } + .offset-by-eleven.column, + .offset-by-eleven.columns { margin-left: 95.3333333333%; } + + .offset-by-one-third.column, + .offset-by-one-third.columns { margin-left: 34.6666666667%; } + .offset-by-two-thirds.column, + .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } + + .offset-by-one-half.column, + .offset-by-one-half.columns { margin-left: 52%; } + +} + + +/* Base Styles +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +/* NOTE +html is set to 62.5% so that all the REM measurements throughout Skeleton +are based on 10px sizing. So basically 1.5rem = 15px :) */ +html { + font-size: 62.5%; } +body { + font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ + line-height: 1.6; + font-weight: 400; + font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #222; } + + +/* Typography +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 2rem; + font-weight: 300; } +h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} +h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } +h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } +h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } +h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } +h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } + +/* Larger than phablet */ +@media (min-width: 550px) { + h1 { font-size: 5.0rem; } + h2 { font-size: 4.2rem; } + h3 { font-size: 3.6rem; } + h4 { font-size: 3.0rem; } + h5 { font-size: 2.4rem; } + h6 { font-size: 1.5rem; } +} + +p { + margin-top: 0; } + + +/* Links +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +a { + color: #1EAEDB; } +a:hover { + color: #0FA0CE; } + + +/* Buttons +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.button, +button, +input[type="submit"], +input[type="reset"], +input[type="button"] { + display: inline-block; + height: 38px; + padding: 0 30px; + color: #555; + text-align: center; + font-size: 11px; + font-weight: 600; + line-height: 38px; + letter-spacing: .1rem; + text-transform: uppercase; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border-radius: 4px; + border: 1px solid #bbb; + cursor: pointer; + box-sizing: border-box; } +.button:hover, +button:hover, +input[type="submit"]:hover, +input[type="reset"]:hover, +input[type="button"]:hover, +.button:focus, +button:focus, +input[type="submit"]:focus, +input[type="reset"]:focus, +input[type="button"]:focus { + color: #333; + border-color: #888; + outline: 0; } +.button.button-primary, +button.button-primary, +button.primary, +input[type="submit"].button-primary, +input[type="reset"].button-primary, +input[type="button"].button-primary { + color: #FFF; + background-color: #33C3F0; + border-color: #33C3F0; } +.button.button-primary:hover, +button.button-primary:hover, +button.primary:hover, +input[type="submit"].button-primary:hover, +input[type="reset"].button-primary:hover, +input[type="button"].button-primary:hover, +.button.button-primary:focus, +button.button-primary:focus, +button.primary:focus, +input[type="submit"].button-primary:focus, +input[type="reset"].button-primary:focus, +input[type="button"].button-primary:focus { + color: #FFF; + background-color: #1EAEDB; + border-color: #1EAEDB; } + + +/* Forms +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +input[type="email"], +input[type="number"], +input[type="search"], +input[type="text"], +input[type="tel"], +input[type="url"], +input[type="password"], +textarea, +select { + height: 38px; + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + background-color: #fff; + border: 1px solid #D1D1D1; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; } +/* Removes awkward default styles on some inputs for iOS */ +input[type="email"], +input[type="number"], +input[type="search"], +input[type="text"], +input[type="tel"], +input[type="url"], +input[type="password"], +textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } +textarea { + min-height: 65px; + padding-top: 6px; + padding-bottom: 6px; } +input[type="email"]:focus, +input[type="number"]:focus, +input[type="search"]:focus, +input[type="text"]:focus, +input[type="tel"]:focus, +input[type="url"]:focus, +input[type="password"]:focus, +textarea:focus, +select:focus { + border: 1px solid #33C3F0; + outline: 0; } +label, +legend { + display: block; + margin-bottom: .5rem; + font-weight: 600; } +fieldset { + padding: 0; + border-width: 0; } +input[type="checkbox"], +input[type="radio"] { + display: inline; } +label > .label-body { + display: inline-block; + margin-left: .5rem; + font-weight: normal; } + + +/* Lists +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +ul { + list-style: circle inside; } +ol { + list-style: decimal inside; } +ol, ul { + padding-left: 0; + margin-top: 0; } +ul ul, +ul ol, +ol ol, +ol ul { + margin: 1.5rem 0 1.5rem 3rem; + font-size: 90%; } +li { + margin-bottom: 1rem; } + + +/* Code +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +code { + padding: .2rem .5rem; + margin: 0 .2rem; + font-size: 90%; + white-space: nowrap; + background: #F1F1F1; + border: 1px solid #E1E1E1; + border-radius: 4px; } +pre > code { + display: block; + padding: 1rem 1.5rem; + white-space: pre; } + + +/* Tables +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +th, +td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #E1E1E1; } +th:first-child, +td:first-child { + padding-left: 0; } +th:last-child, +td:last-child { + padding-right: 0; } + + +/* Spacing +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +button, +.button { + margin-bottom: 1rem; } +input, +textarea, +select, +fieldset { + margin-bottom: 1.5rem; } +pre, +blockquote, +dl, +figure, +table, +p, +ul, +ol, +form { + margin-bottom: 2.5rem; } + + +/* Utilities +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.u-full-width { + width: 100%; + box-sizing: border-box; } +.u-max-full-width { + max-width: 100%; + box-sizing: border-box; } +.u-pull-right { + float: right; } +.u-pull-left { + float: left; } + + +/* Misc +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +hr { + margin-top: 3rem; + margin-bottom: 3.5rem; + border-width: 0; + border-top: 1px solid #E1E1E1; } + + +/* Clearing +–––––––––––––––––––––––––––––––––––––––––––––––––– */ + +/* Self Clearing Goodness */ +.container:after, +.row:after, +.u-cf { + content: ""; + display: table; + clear: both; } + + +/* Media Queries +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +/* +Note: The best way to structure the use of media queries is to create the queries +near the relevant code. For example, if you wanted to change the styles for buttons +on small devices, paste the mobile query code up in the buttons section and style it +there. +*/ + + +/* Larger than mobile */ +@media (min-width: 400px) {} + +/* Larger than phablet (also point when grid becomes active) */ +@media (min-width: 550px) {} + +/* Larger than tablet */ +@media (min-width: 750px) {} + +/* Larger than desktop */ +@media (min-width: 1000px) {} + +/* Larger than Desktop HD */ +@media (min-width: 1200px) {} diff --git a/sqlx/static/css/style.css b/sqlx/static/css/style.css new file mode 100644 index 0000000..cdf155e --- /dev/null +++ b/sqlx/static/css/style.css @@ -0,0 +1,58 @@ +.field-error { + border: 1px solid #ff0000 !important; +} + +.field-error-msg { + color: #ff0000; + display: block; + margin: -10px 0 10px 0; +} + +.field-success { + border: 1px solid #5AB953 !important; +} + +.field-success-msg { + color: #5AB953; + display: block; + margin: -10px 0 10px 0; +} + +span.completed { + text-decoration: line-through; +} + +form.inline { + display: inline; +} + +form.link, +button.link { + display: inline; + color: #1EAEDB; + border: none; + outline: none; + background: none; + cursor: pointer; + padding: 0; + margin: 0 0 0 0; + height: inherit; + text-decoration: underline; + font-size: inherit; + text-transform: none; + font-weight: normal; + line-height: inherit; + letter-spacing: inherit; +} + +form.link:hover, button.link:hover { + color: #0FA0CE; +} + +button.small { + height: 20px; + padding: 0 10px; + font-size: 10px; + line-height: 20px; + margin: 0 2.5px; +} diff --git a/sqlx/static/errors/400.html b/sqlx/static/errors/400.html new file mode 100644 index 0000000..be66536 --- /dev/null +++ b/sqlx/static/errors/400.html @@ -0,0 +1,21 @@ + + + + + + + The server could not understand the request (400) + + + + + + + +
+
+

The server could not understand the request

+
+
+ + diff --git a/sqlx/static/errors/404.html b/sqlx/static/errors/404.html new file mode 100644 index 0000000..2205a07 --- /dev/null +++ b/sqlx/static/errors/404.html @@ -0,0 +1,22 @@ + + + + + + + The page you were looking for doesn't exist (404) + + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+
+ + diff --git a/sqlx/static/errors/500.html b/sqlx/static/errors/500.html new file mode 100644 index 0000000..c2710c2 --- /dev/null +++ b/sqlx/static/errors/500.html @@ -0,0 +1,24 @@ + + + + + + + Ooops (500) + + + + + + + +
+
+

Ooops ...

+

How embarrassing!

+

Looks like something weird happened while processing your request.

+

Please try again in a few moments.

+
+
+ + diff --git a/sqlx/templates/index.html.tera b/sqlx/templates/index.html.tera new file mode 100644 index 0000000..64cbf85 --- /dev/null +++ b/sqlx/templates/index.html.tera @@ -0,0 +1,65 @@ + + + + + + + Actix Todo Example + + + + + + + +
+

+ +
+

Actix Todo

+
+
+ + {% if msg %} + + {{msg.1}} + + {% endif %} +
+
+ +
+
+
+ +
+
+
    + {% for task in tasks %} +
  • + {% if task.completed %} + {{task.description}} +
    + + +
    +
    + + +
    + {% else %} + + {% endif %} +
  • + {% endfor %} +
+
+
+
+ + From 6f2bd83cab555cf23545ba01a186cea757a4dfcf Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Sat, 8 Feb 2025 20:50:22 +0300 Subject: [PATCH 02/12] sea-orm integration example (#4) * feat: added base sea-orm integration example * feat: restructure sqlx integration example * feat: redo example app * feat: redo example app v2 * feat: pk changed from sequence to uuid --- Cargo.toml | 19 +- seaorm/Cargo.toml | 18 ++ seaorm/README.md | 25 +++ seaorm/migration/Cargo.toml | 21 +++ seaorm/migration/README.md | 37 ++++ seaorm/migration/src/lib.rs | 12 ++ .../src/m20220120_000001_create_item_table.rs | 35 ++++ seaorm/migration/src/main.rs | 6 + seaorm/src/actions.rs | 79 ++++++++ seaorm/src/main.rs | 171 ++++++++++++++++++ seaorm/src/models.rs | 18 ++ seaorm/src/tests/mock.rs | 79 ++++++++ seaorm/src/tests/mod.rs | 0 seaorm/src/tests/prepare.rs | 50 +++++ 14 files changed, 561 insertions(+), 9 deletions(-) create mode 100644 seaorm/Cargo.toml create mode 100644 seaorm/README.md create mode 100644 seaorm/migration/Cargo.toml create mode 100644 seaorm/migration/README.md create mode 100644 seaorm/migration/src/lib.rs create mode 100644 seaorm/migration/src/m20220120_000001_create_item_table.rs create mode 100644 seaorm/migration/src/main.rs create mode 100644 seaorm/src/actions.rs create mode 100644 seaorm/src/main.rs create mode 100644 seaorm/src/models.rs create mode 100644 seaorm/src/tests/mock.rs create mode 100644 seaorm/src/tests/mod.rs create mode 100644 seaorm/src/tests/prepare.rs diff --git a/Cargo.toml b/Cargo.toml index 0319e76..6d93b7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,6 @@ resolver = "2" members = [ "application", - "database_async", - "database_sync", "errors", "extractors", "getting_started", @@ -17,6 +15,9 @@ members = [ "testing", "url_dispatch", "websockets", + "diesel", + "diesel_async", + "seaorm" ] [workspace.package] @@ -25,12 +26,12 @@ edition = "2021" rust-version = "1.84" [workspace.dependencies] -actix-web = { version = "*", features = ["openssl", "rustls"] } +actix-web = { version = "4.9", features = ["openssl", "rustls"] } actix-files = "0.6" -serde = { version = "*", features = ["derive"] } -serde_json = "*" -futures = "*" -env_logger = "*" -log = "*" -futures-util = "*" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +futures = "0.3" +futures-util = "0.3" +env_logger = "0.11" +log = "0.4" uuid = { version = "1.11.0", features = ["v7", "serde"] } diff --git a/seaorm/Cargo.toml b/seaorm/Cargo.toml new file mode 100644 index 0000000..a35e5d3 --- /dev/null +++ b/seaorm/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "seaorm" +version = "1.0.0" +edition = "2021" + +[dependencies] +actix-files.workspace = true +actix-http = "3.9" +actix-rt = "2.10" +actix-service = "2" +actix-web.workspace = true +#tera = "1.20" +dotenvy = "0.15" +listenfd = "1" +serde.workspace = true +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +sea-orm = { version = "1.1", features = [ "sqlx-postgres", "runtime-tokio", "macros", "with-uuid" ] } +migration = { path = "./migration" } diff --git a/seaorm/README.md b/seaorm/README.md new file mode 100644 index 0000000..88fab09 --- /dev/null +++ b/seaorm/README.md @@ -0,0 +1,25 @@ +![screenshot](Screenshot.png) + +# Actix 4 with SeaORM example app + +1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database + +1. Turn on the appropriate database feature for your chosen db in `service/Cargo.toml` (the `"sqlx-mysql",` line) + +1. Execute `cargo run` to start the server + +1. Visit [localhost:8000](http://localhost:8000) in browser + +Run server with auto-reloading: + +```bash +cargo install systemfd cargo-watch +systemfd --no-pid -s http::8000 -- cargo watch -x run +``` + +Run mock test on the service logic crate: + +```bash +cd service +cargo test --features mock +``` diff --git a/seaorm/migration/Cargo.toml b/seaorm/migration/Cargo.toml new file mode 100644 index 0000000..9c48ad9 --- /dev/null +++ b/seaorm/migration/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "migration" +version = "1.0.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +async-std = { version = "1.13", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +#path = "../../../sea-orm-migration" # remove this line in your own project +version = "~1.1.4" # sea-orm-migration version +features = [ + # Enable following runtime and db backend features if you want to run migration via CLI + # "runtime-actix-native-tls", + # "sqlx-mysql", +] diff --git a/seaorm/migration/README.md b/seaorm/migration/README.md new file mode 100644 index 0000000..963caae --- /dev/null +++ b/seaorm/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/seaorm/migration/src/lib.rs b/seaorm/migration/src/lib.rs new file mode 100644 index 0000000..4cc94e3 --- /dev/null +++ b/seaorm/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_orm_migration::prelude::*; + +mod m20220120_000001_create_item_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220120_000001_create_item_table::Migration)] + } +} diff --git a/seaorm/migration/src/m20220120_000001_create_item_table.rs b/seaorm/migration/src/m20220120_000001_create_item_table.rs new file mode 100644 index 0000000..cf5bfe1 --- /dev/null +++ b/seaorm/migration/src/m20220120_000001_create_item_table.rs @@ -0,0 +1,35 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Items::Table) + .if_not_exists() + .col(pk_uuid(Items::Id)) + .col(string(Items::Title)) + .col(string(Items::Description)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Items::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum Items { + Table, + Id, + Title, + Description, +} diff --git a/seaorm/migration/src/main.rs b/seaorm/migration/src/main.rs new file mode 100644 index 0000000..c6b6e48 --- /dev/null +++ b/seaorm/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/seaorm/src/actions.rs b/seaorm/src/actions.rs new file mode 100644 index 0000000..351fe3b --- /dev/null +++ b/seaorm/src/actions.rs @@ -0,0 +1,79 @@ +// use ::entity::{item, Entity as Item}; +use crate::models::{ActiveModel, Column, Entity as Item, Model}; +use sea_orm::prelude::Uuid; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_item(db: &DbConn, form_data: Model) -> Result { + ActiveModel { + name: Set(form_data.name.to_owned()), + description: Set(form_data.description.to_owned()), + ..Default::default() + } + .insert(db) + .await + } + + pub async fn update_item_by_id( + db: &DbConn, + id: Uuid, + form_data: Model, + ) -> Result { + let item: ActiveModel = Item::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find item.".to_owned())) + .map(Into::into)?; + + ActiveModel { + id: item.id, + name: Set(form_data.name.to_owned()), + description: Set(form_data.description.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_item(db: &DbConn, id: Uuid) -> Result { + let item: ActiveModel = Item::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find item.".to_owned())) + .map(Into::into)?; + + item.delete(db).await + } + + pub async fn delete_all_items(db: &DbConn) -> Result { + Item::delete_many().exec(db).await + } +} + +// use ::entity::{item, Entity as Item}; +// use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_item_by_id(db: &DbConn, id: Uuid) -> Result, DbErr> { + Item::find_by_id(id).one(db).await + } + + /// If ok, returns (item models, num pages). + pub async fn find_items_in_page( + db: &DbConn, + page: u64, + items_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { + // Setup paginator + let paginator = Item::find() + .order_by_asc(Column::Id) + .paginate(db, items_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated items + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/seaorm/src/main.rs b/seaorm/src/main.rs new file mode 100644 index 0000000..853189d --- /dev/null +++ b/seaorm/src/main.rs @@ -0,0 +1,171 @@ +mod actions; +mod models; + +use actix_web::{ + delete, get, middleware, post, put, web, App, HttpRequest, HttpResponse, HttpServer, Responder, +}; + +use crate::actions::{Mutation, Query}; +use crate::models::Model; +use listenfd::ListenFd; +use migration::{Migrator, MigratorTrait}; +use sea_orm::prelude::Uuid; +use sea_orm::{Database, DatabaseConnection}; +use serde::{Deserialize, Serialize}; +use std::env; + +const DEFAULT_POSTS_PER_PAGE: u64 = 5; + +#[derive(Debug, Clone)] +struct AppState { + conn: DatabaseConnection, +} + +#[derive(Debug, Deserialize)] +pub struct Params { + page: Option, + items_per_page: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +struct FlashData { + kind: String, + message: String, +} + +#[get("/items")] +async fn list_items( + req: HttpRequest, + data: web::Data, +) -> actix_web::Result { + let conn = &data.conn; + + // get params + let params = web::Query::::from_query(req.query_string())?; + + let page = params.page.unwrap_or(1); + let items_per_page = params.items_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + + let (items, num_pages) = Query::find_items_in_page(conn, page, items_per_page) + .await + .expect("Cannot find items in page"); + + Ok(HttpResponse::Ok().json(items)) +} + +#[post("/items")] +async fn create_item( + data: web::Data, + item_data: web::Json, +) -> actix_web::Result { + let conn = &data.conn; + + let item = Mutation::create_item(conn, item_data.into_inner()) + .await + .expect("could not insert item"); + + Ok(HttpResponse::Created().json(item)) +} + +#[get("/items/{id}")] +async fn get_item( + data: web::Data, + id: web::Path, +) -> actix_web::Result { + let conn = &data.conn; + + let id = id.into_inner(); + + let item: Option = Query::find_item_by_id(conn, id) + .await + .expect("could not find item"); + + Ok(HttpResponse::Ok().json(item)) +} + +#[put("/{id}")] +async fn update_item( + data: web::Data, + id: web::Path, + item_form: web::Form, +) -> actix_web::Result { + let conn = &data.conn; + let form = item_form.into_inner(); + let id = id.into_inner(); + + let item = Mutation::update_item_by_id(conn, id, form) + .await + .expect("could not edit item"); + + Ok(HttpResponse::Accepted().json(item)) +} + +#[delete("/items/{id}")] +async fn delete_item( + data: web::Data, + id: web::Path, +) -> actix_web::Result { + let conn = &data.conn; + let id = id.into_inner(); + + Mutation::delete_item(conn, id) + .await + .expect("could not delete item"); + + Ok(HttpResponse::NoContent()) +} + +#[actix_web::main] +async fn start() -> std::io::Result<()> { + std::env::set_var("RUST_LOG", "debug"); + tracing_subscriber::fmt::init(); + + // get env vars + dotenvy::dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = env::var("HOST").expect("HOST is not set in .env file"); + let port = env::var("PORT").expect("PORT is not set in .env file"); + let server_url = format!("{host}:{port}"); + + // establish connection to database and apply migrations + // -> create table `item` if it not exists + let conn = Database::connect(&db_url).await.unwrap(); + Migrator::up(&conn, None).await.unwrap(); + + let state = AppState { conn }; //, templates }; + + // create server and try to serve over socket if possible + let mut listenfd = ListenFd::from_env(); + let mut server = HttpServer::new(move || { + App::new() + .app_data(web::Data::new(state.clone())) + .wrap(middleware::Logger::default()) // enable logger + .configure(init) + }); + + server = match listenfd.take_tcp_listener(0)? { + Some(listener) => server.listen(listener)?, + None => server.bind(&server_url)?, + }; + + println!("Starting server at {server_url}"); + server.run().await?; + + Ok(()) +} + +fn init(cfg: &mut web::ServiceConfig) { + cfg.service(list_items); + cfg.service(create_item); + cfg.service(get_item); + cfg.service(update_item); + cfg.service(delete_item); +} + +pub fn main() { + let result = start(); + + if let Some(err) = result.err() { + println!("Error: {err}"); + } +} diff --git a/seaorm/src/models.rs b/seaorm/src/models.rs new file mode 100644 index 0000000..80e223c --- /dev/null +++ b/seaorm/src/models.rs @@ -0,0 +1,18 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "items")] +pub struct Model { + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + pub id: Uuid, + pub name: String, + #[sea_orm(column_type = "Text")] + pub description: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/seaorm/src/tests/mock.rs b/seaorm/src/tests/mock.rs new file mode 100644 index 0000000..9da3da2 --- /dev/null +++ b/seaorm/src/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use actix_example_service::{Mutation, Query}; +use entity::item; +use prepare::prepare_mock_db; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let item = Query::find_item_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(item.id, 1); + } + + { + let item = Query::find_item_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(item.id, 5); + } + + { + let item = Mutation::create_item( + db, + item::Model { + id: 0, + name: "Title D".to_owned(), + description: "Description D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + item, + item::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + name: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + description: sea_orm::ActiveValue::Unchanged("Description D".to_owned()) + } + ); + } + + { + let item = Mutation::update_item_by_id( + db, + 1, + item::Model { + id: 1, + name: "New Title A".to_owned(), + description: "New Description A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + item, + item::Model { + id: 1, + name: "New Title A".to_owned(), + description: "New Description A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_item(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_items(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/seaorm/src/tests/mod.rs b/seaorm/src/tests/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/seaorm/src/tests/prepare.rs b/seaorm/src/tests/prepare.rs new file mode 100644 index 0000000..5906e81 --- /dev/null +++ b/seaorm/src/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::item; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Itemgres) + .append_query_results([ + [item::Model { + id: 1, + name: "Title A".to_owned(), + description: "Description A".to_owned(), + }], + [item::Model { + id: 5, + name: "Title C".to_owned(), + description: "Description C".to_owned(), + }], + [item::Model { + id: 6, + name: "Title D".to_owned(), + description: "Description D".to_owned(), + }], + [item::Model { + id: 1, + name: "Title A".to_owned(), + description: "Description A".to_owned(), + }], + [item::Model { + id: 1, + name: "New Title A".to_owned(), + description: "New Description A".to_owned(), + }], + [item::Model { + id: 5, + name: "Title C".to_owned(), + description: "Description C".to_owned(), + }], + ]) + .append_exec_results([ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} From e6d96ef56f789d008aa4ffd2fa1de1fcb8d8e94d Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Sun, 9 Feb 2025 23:50:47 +0300 Subject: [PATCH 03/12] refactor: refactor endpoints, business logic, and db operations --- Cargo.toml | 6 +- diesel_async/Cargo.toml | 2 +- seaorm/Cargo.toml | 5 - seaorm/src/main.rs | 2 +- ...fb199e7017d876a6d7c0e8720d8ec4561dcaa.json | 32 ++ ...5584579a09bad66b28e1390533fc5811ac924.json | 14 + sqlx/Cargo.toml | 11 +- ...20220205163207_create_items_table.down.sql | 1 + .../20220205163207_create_items_table.up.sql | 6 + ...20220205163207_create_tasks_table.down.sql | 1 - .../20220205163207_create_tasks_table.up.sql | 5 - sqlx/src/api.rs | 205 +++++---- sqlx/src/db.rs | 34 +- sqlx/src/main.rs | 48 +- sqlx/src/model.rs | 73 --- sqlx/src/models.rs | 131 ++++++ sqlx/static/css/normalize.css | 427 ------------------ sqlx/static/css/skeleton.css | 421 ----------------- sqlx/static/css/style.css | 58 --- sqlx/static/errors/400.html | 21 - sqlx/static/errors/404.html | 22 - sqlx/static/errors/500.html | 24 - sqlx/templates/index.html.tera | 65 --- 23 files changed, 339 insertions(+), 1275 deletions(-) create mode 100644 sqlx/.sqlx/query-56fe0b1d42e9e3999405f165f97fb199e7017d876a6d7c0e8720d8ec4561dcaa.json create mode 100644 sqlx/.sqlx/query-f93cdfd729c79b6e83806dc5c005584579a09bad66b28e1390533fc5811ac924.json create mode 100644 sqlx/migrations/20220205163207_create_items_table.down.sql create mode 100644 sqlx/migrations/20220205163207_create_items_table.up.sql delete mode 100644 sqlx/migrations/20220205163207_create_tasks_table.down.sql delete mode 100644 sqlx/migrations/20220205163207_create_tasks_table.up.sql delete mode 100644 sqlx/src/model.rs create mode 100644 sqlx/src/models.rs delete mode 100644 sqlx/static/css/normalize.css delete mode 100644 sqlx/static/css/skeleton.css delete mode 100644 sqlx/static/css/style.css delete mode 100644 sqlx/static/errors/400.html delete mode 100644 sqlx/static/errors/404.html delete mode 100644 sqlx/static/errors/500.html delete mode 100644 sqlx/templates/index.html.tera diff --git a/Cargo.toml b/Cargo.toml index 6d93b7b..b12095a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,8 @@ members = [ "websockets", "diesel", "diesel_async", - "seaorm" + "seaorm", + "sqlx", ] [workspace.package] @@ -32,6 +33,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" futures = "0.3" futures-util = "0.3" +dotenvy = "0.15" env_logger = "0.11" log = "0.4" -uuid = { version = "1.11.0", features = ["v7", "serde"] } +uuid = { version = "1.11", features = ["v7", "serde"] } diff --git a/diesel_async/Cargo.toml b/diesel_async/Cargo.toml index 839aab4..2775ca4 100644 --- a/diesel_async/Cargo.toml +++ b/diesel_async/Cargo.toml @@ -9,6 +9,6 @@ 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 = "0.15" +dotenvy.workspace = true env_logger.workspace = true log.workspace = true diff --git a/seaorm/Cargo.toml b/seaorm/Cargo.toml index a35e5d3..e1e3e8e 100644 --- a/seaorm/Cargo.toml +++ b/seaorm/Cargo.toml @@ -4,12 +4,7 @@ version = "1.0.0" edition = "2021" [dependencies] -actix-files.workspace = true -actix-http = "3.9" -actix-rt = "2.10" -actix-service = "2" actix-web.workspace = true -#tera = "1.20" dotenvy = "0.15" listenfd = "1" serde.workspace = true diff --git a/seaorm/src/main.rs b/seaorm/src/main.rs index 853189d..736acf3 100644 --- a/seaorm/src/main.rs +++ b/seaorm/src/main.rs @@ -87,7 +87,7 @@ async fn get_item( async fn update_item( data: web::Data, id: web::Path, - item_form: web::Form, + item_form: web::Json, ) -> actix_web::Result { let conn = &data.conn; let form = item_form.into_inner(); diff --git a/sqlx/.sqlx/query-56fe0b1d42e9e3999405f165f97fb199e7017d876a6d7c0e8720d8ec4561dcaa.json b/sqlx/.sqlx/query-56fe0b1d42e9e3999405f165f97fb199e7017d876a6d7c0e8720d8ec4561dcaa.json new file mode 100644 index 0000000..9615a6f --- /dev/null +++ b/sqlx/.sqlx/query-56fe0b1d42e9e3999405f165f97fb199e7017d876a6d7c0e8720d8ec4561dcaa.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT *\n FROM items\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "56fe0b1d42e9e3999405f165f97fb199e7017d876a6d7c0e8720d8ec4561dcaa" +} diff --git a/sqlx/.sqlx/query-f93cdfd729c79b6e83806dc5c005584579a09bad66b28e1390533fc5811ac924.json b/sqlx/.sqlx/query-f93cdfd729c79b6e83806dc5c005584579a09bad66b28e1390533fc5811ac924.json new file mode 100644 index 0000000..59fcf04 --- /dev/null +++ b/sqlx/.sqlx/query-f93cdfd729c79b6e83806dc5c005584579a09bad66b28e1390533fc5811ac924.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM items\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "f93cdfd729c79b6e83806dc5c005584579a09bad66b28e1390533fc5811ac924" +} diff --git a/sqlx/Cargo.toml b/sqlx/Cargo.toml index 20a9e29..4aa5f03 100644 --- a/sqlx/Cargo.toml +++ b/sqlx/Cargo.toml @@ -1,16 +1,13 @@ [package] -name = "todo" +name = "sqlx" version = "1.0.0" edition = "2021" [dependencies] -actix-files.workspace = true -actix-session = { workspace = true, features = ["cookie-session"] } actix-web.workspace = true - +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "migrate", "macros"] } +uuid.workspace = true +serde.workspace = true dotenvy.workspace = true env_logger.workspace = true log.workspace = true -serde.workspace = true -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] } -tera = "1.5" diff --git a/sqlx/migrations/20220205163207_create_items_table.down.sql b/sqlx/migrations/20220205163207_create_items_table.down.sql new file mode 100644 index 0000000..f3f9b64 --- /dev/null +++ b/sqlx/migrations/20220205163207_create_items_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS items; diff --git a/sqlx/migrations/20220205163207_create_items_table.up.sql b/sqlx/migrations/20220205163207_create_items_table.up.sql new file mode 100644 index 0000000..e1fc834 --- /dev/null +++ b/sqlx/migrations/20220205163207_create_items_table.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS items +( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + title VARCHAR NOT NULL, + description VARCHAR NOT NULL +); diff --git a/sqlx/migrations/20220205163207_create_tasks_table.down.sql b/sqlx/migrations/20220205163207_create_tasks_table.down.sql deleted file mode 100644 index 87f1ed5..0000000 --- a/sqlx/migrations/20220205163207_create_tasks_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE tasks; diff --git a/sqlx/migrations/20220205163207_create_tasks_table.up.sql b/sqlx/migrations/20220205163207_create_tasks_table.up.sql deleted file mode 100644 index 20d5bd2..0000000 --- a/sqlx/migrations/20220205163207_create_tasks_table.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE tasks ( - id INTEGER PRIMARY KEY, - description VARCHAR NOT NULL, - completed BOOLEAN NOT NULL DEFAULT 'f' -); diff --git a/sqlx/src/api.rs b/sqlx/src/api.rs index caaefd6..607ebfc 100644 --- a/sqlx/src/api.rs +++ b/sqlx/src/api.rs @@ -1,70 +1,74 @@ -use actix_files::NamedFile; -use actix_session::Session; +// use actix_files::NamedFile; +// use actix_session::Session; use actix_web::{ dev, error, http::StatusCode, middleware::ErrorHandlerResponse, web, Error, HttpResponse, Responder, Result, }; use serde::Deserialize; -use sqlx::SqlitePool; -use tera::{Context, Tera}; +use sqlx::PgPool; +use uuid::Uuid; +// use sqlx::types::Uuid; +// use tera::{Context, Tera}; use crate::{ db, - session::{self, FlashMessage}, + models + // session::{self, FlashMessage}, }; - -pub async fn index( - pool: web::Data, - tmpl: web::Data, - session: Session, -) -> Result { - let tasks = db::get_all_tasks(&pool) +use crate::models::UpdateItem; + +pub async fn get_items( + pool: web::Data, + // tmpl: web::Data, + // session: Session, +) -> Result { + let items = db::get_all_items(&pool) .await .map_err(error::ErrorInternalServerError)?; - let mut context = Context::new(); - context.insert("tasks", &tasks); + // let mut context = Context::new(); + // context.insert("items", &items); // Session is set during operations on other endpoints that can redirect to index - if let Some(flash) = session::get_flash(&session)? { - context.insert("msg", &(flash.kind, flash.message)); - session::clear_flash(&session); - } + // if let Some(flash) = session::get_flash(&session)? { + // context.insert("msg", &(flash.kind, flash.message)); + // session::clear_flash(&session); + // } - let rendered = tmpl - .render("index.html.tera", &context) - .map_err(error::ErrorInternalServerError)?; + // let rendered = tmpl + // .render("index.html.tera", &context) + // .map_err(error::ErrorInternalServerError)?; - Ok(HttpResponse::Ok().body(rendered)) + Ok(HttpResponse::Ok().json(items)) } -#[derive(Debug, Deserialize)] -pub struct CreateForm { - description: String, -} +// #[derive(Debug, Deserialize)] +// pub struct CreateForm { +// description: String, +// } -pub async fn create( - params: web::Form, - pool: web::Data, - session: Session, +pub async fn create_item( + params: web::Json, + pool: web::Data, + // session: Session, ) -> Result { - if params.description.is_empty() { - session::set_flash(&session, FlashMessage::error("Description cannot be empty"))?; - Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND)) - } else { - db::create_task(params.into_inner().description, &pool) - .await - .map_err(error::ErrorInternalServerError)?; - - session::set_flash(&session, FlashMessage::success("Task successfully added"))?; - - Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND)) - } + // if params.description.is_empty() { + // session::set_flash(&session, FlashMessage::error("Description cannot be empty"))?; + // Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND)) + // } else { + db::create_item(params.into_inner(), &pool) + .await + .map_err(error::ErrorInternalServerError)?; + + // session::set_flash(&session, FlashMessage::success("Task successfully added"))?; + + Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND)) + // } } #[derive(Debug, Deserialize)] pub struct UpdateParams { - id: i32, + id: Uuid, } #[derive(Debug, Deserialize)] @@ -72,77 +76,78 @@ pub struct UpdateForm { _method: String, } -pub async fn update( - db: web::Data, - params: web::Path, - form: web::Form, - session: Session, -) -> Result { - Ok(web::Redirect::to(match form._method.as_ref() { - "put" => toggle(db, params).await?, - "delete" => delete(db, params, session).await?, - unsupported_method => { - let msg = format!("Unsupported HTTP method: {unsupported_method}"); - return Err(error::ErrorBadRequest(msg)); - } - }) - .using_status_code(StatusCode::FOUND)) -} +// pub async fn update( +// db: web::Data, +// params: web::Path, +// form: web::Json, +// // session: Session, +// ) -> Result { +// Ok(web::Redirect::to(match form._method.as_ref() { +// "put" => toggle(db, params).await?, +// // "delete" => delete(db, params, session).await?, +// unsupported_method => { +// let msg = format!("Unsupported HTTP method: {unsupported_method}"); +// return Err(error::ErrorBadRequest(msg)); +// } +// }) +// .using_status_code(StatusCode::FOUND)) +// } -async fn toggle( - pool: web::Data, - params: web::Path, +pub async fn update( + pool: web::Data, + id: web::Path, + data: web::Json, ) -> Result<&'static str, Error> { - db::toggle_task(params.id, &pool) + db::update_item(id.into_inner(), data.into_inner(), &pool) .await .map_err(error::ErrorInternalServerError)?; Ok("/") } -async fn delete( - pool: web::Data, +pub async fn delete( + pool: web::Data, params: web::Path, - session: Session, + // session: Session, ) -> Result<&'static str, Error> { - db::delete_task(params.id, &pool) + db::delete_item(params.id, &pool) .await .map_err(error::ErrorInternalServerError)?; - session::set_flash(&session, FlashMessage::success("Task was deleted."))?; + // session::set_flash(&session, FlashMessage::success("Task was deleted."))?; Ok("/") } -pub fn bad_request(res: dev::ServiceResponse) -> Result> { - let new_resp = NamedFile::open("static/errors/400.html")? - .customize() - .with_status(res.status()) - .respond_to(res.request()) - .map_into_boxed_body() - .map_into_right_body(); - - Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) -} - -pub fn not_found(res: dev::ServiceResponse) -> Result> { - let new_resp = NamedFile::open("static/errors/404.html")? - .customize() - .with_status(res.status()) - .respond_to(res.request()) - .map_into_boxed_body() - .map_into_right_body(); - - Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) -} - -pub fn internal_server_error(res: dev::ServiceResponse) -> Result> { - let new_resp = NamedFile::open("static/errors/500.html")? - .customize() - .with_status(res.status()) - .respond_to(res.request()) - .map_into_boxed_body() - .map_into_right_body(); - - Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) -} +// pub fn bad_request(res: dev::ServiceResponse) -> Result> { +// let new_resp = NamedFile::open("static/errors/400.html")? +// .customize() +// .with_status(res.status()) +// .respond_to(res.request()) +// .map_into_boxed_body() +// .map_into_right_body(); +// +// Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) +// } +// +// pub fn not_found(res: dev::ServiceResponse) -> Result> { +// let new_resp = NamedFile::open("static/errors/404.html")? +// .customize() +// .with_status(res.status()) +// .respond_to(res.request()) +// .map_into_boxed_body() +// .map_into_right_body(); +// +// Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) +// } + +// pub fn internal_server_error(res: dev::ServiceResponse) -> Result> { +// let new_resp = NamedFile::open("static/errors/500.html")? +// .customize() +// .with_status(res.status()) +// .respond_to(res.request()) +// .map_into_boxed_body() +// .map_into_right_body(); +// +// Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) +// } diff --git a/sqlx/src/db.rs b/sqlx/src/db.rs index 6791bff..97220be 100644 --- a/sqlx/src/db.rs +++ b/sqlx/src/db.rs @@ -1,36 +1,36 @@ -use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; +use sqlx::postgres::{PgPool, PgPoolOptions}; +use sqlx::types::Uuid; +use crate::models::{NewItem, Item, UpdateItem}; -use crate::model::{NewTask, Task}; - -pub async fn init_pool(database_url: &str) -> Result { - SqlitePoolOptions::new() +pub async fn init_pool(database_url: &str) -> Result { + PgPoolOptions::new() .acquire_timeout(std::time::Duration::from_secs(1)) .connect(database_url) .await } -pub async fn get_all_tasks(pool: &SqlitePool) -> Result, &'static str> { - Task::all(pool).await.map_err(|_| "Error retrieving tasks") +pub async fn get_all_items(pool: &PgPool) -> Result, &'static str> { + Item::all(pool).await.map_err(|_| "Error retrieving items") } -pub async fn create_task(todo: String, pool: &SqlitePool) -> Result<(), &'static str> { - let new_task = NewTask { description: todo }; - Task::insert(new_task, pool) +pub async fn create_item(item_data: NewItem, pool: &PgPool) -> Result<(), &'static str> { + let new_item = NewItem { title: item_data.title, description: item_data.description }; + Item::insert(new_item, pool) .await .map(|_| ()) - .map_err(|_| "Error inserting task") + .map_err(|_| "Error inserting item") } -pub async fn toggle_task(id: i32, pool: &SqlitePool) -> Result<(), &'static str> { - Task::toggle_with_id(id, pool) +pub async fn update_item(id: Uuid, data: UpdateItem, pool: &PgPool) -> Result<(), &'static str> { + Item::update(id, data, pool) .await .map(|_| ()) - .map_err(|_| "Error toggling task completion") + .map_err(|_| "Error toggling item completion") } -pub async fn delete_task(id: i32, pool: &SqlitePool) -> Result<(), &'static str> { - Task::delete_with_id(id, pool) +pub async fn delete_item(id: Uuid, pool: &PgPool) -> Result<(), &'static str> { + Item::delete_with_id(id, pool) .await .map(|_| ()) - .map_err(|_| "Error deleting task") + .map_err(|_| "Error deleting item") } diff --git a/sqlx/src/main.rs b/sqlx/src/main.rs index 8965b46..f68b867 100644 --- a/sqlx/src/main.rs +++ b/sqlx/src/main.rs @@ -1,19 +1,16 @@ use std::{env, io}; -use actix_files::Files; -use actix_session::{storage::CookieSessionStore, SessionMiddleware}; use actix_web::{ http, middleware::{ErrorHandlers, Logger}, web, App, HttpServer, }; use dotenvy::dotenv; -use tera::Tera; mod api; mod db; -mod model; -mod session; +mod models; +// mod session; // NOTE: Not a suitable session key for production. static SESSION_SIGNING_KEY: &[u8] = &[0; 64]; @@ -23,7 +20,7 @@ async fn main() -> io::Result<()> { dotenv().ok(); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); - let key = actix_web::cookie::Key::from(SESSION_SIGNING_KEY); + // let key = actix_web::cookie::Key::from(SESSION_SIGNING_KEY); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let pool = db::init_pool(&database_url) @@ -35,31 +32,32 @@ async fn main() -> io::Result<()> { HttpServer::new(move || { log::debug!("Constructing the App"); - let mut templates = Tera::new("templates/**/*").expect("errors in tera templates"); - templates.autoescape_on(vec!["tera"]); + // let mut templates = Tera::new("templates/**/*").expect("errors in tera templates"); + // templates.autoescape_on(vec!["tera"]); - let session_store = SessionMiddleware::builder(CookieSessionStore::default(), key.clone()) - .cookie_secure(false) - .build(); + // let session_store = SessionMiddleware::builder(CookieSessionStore::default(), key.clone()) + // .cookie_secure(false) + // .build(); - let error_handlers = ErrorHandlers::new() - .handler( - http::StatusCode::INTERNAL_SERVER_ERROR, - api::internal_server_error, - ) - .handler(http::StatusCode::BAD_REQUEST, api::bad_request) - .handler(http::StatusCode::NOT_FOUND, api::not_found); + // let error_handlers = ErrorHandlers::new() + // .handler( + // http::StatusCode::INTERNAL_SERVER_ERROR, + // api::internal_server_error, + // ) + // .handler(http::StatusCode::BAD_REQUEST, api::bad_request) + // .handler(http::StatusCode::NOT_FOUND, api::not_found); App::new() - .app_data(web::Data::new(templates)) + // .app_data(web::Data::new(templates)) .app_data(web::Data::new(pool.clone())) .wrap(Logger::default()) - .wrap(session_store) - .wrap(error_handlers) - .service(web::resource("/").route(web::get().to(api::index))) - .service(web::resource("/todo").route(web::post().to(api::create))) - .service(web::resource("/todo/{id}").route(web::post().to(api::update))) - .service(Files::new("/static", "./static")) + // .wrap(session_store) + // .wrap(error_handlers) + .service(web::resource("/items").route(web::get().to(api::get_items))) + .service(web::resource("/items").route(web::post().to(api::create_item))) + .service(web::resource("/items/{id}").route(web::patch().to(api::update))) + .service(web::resource("/items/{id}").route(web::delete().to(api::delete))) + // .service(Files::new("/static", "./static")) }) .bind(("127.0.0.1", 8080))? .workers(2) diff --git a/sqlx/src/model.rs b/sqlx/src/model.rs deleted file mode 100644 index 4166233..0000000 --- a/sqlx/src/model.rs +++ /dev/null @@ -1,73 +0,0 @@ -use serde::{Deserialize, Serialize}; -use sqlx::SqlitePool; - -#[derive(Debug)] -pub struct NewTask { - pub description: String, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct Task { - pub id: i64, - pub description: String, - pub completed: bool, -} - -impl Task { - pub async fn all(connection: &SqlitePool) -> Result, sqlx::Error> { - let tasks = sqlx::query_as!( - Task, - r#" - SELECT * - FROM tasks - "# - ) - .fetch_all(connection) - .await?; - - Ok(tasks) - } - - pub async fn insert(todo: NewTask, connection: &SqlitePool) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - INSERT INTO tasks (description) - VALUES ($1) - "#, - todo.description, - ) - .execute(connection) - .await?; - - Ok(()) - } - - pub async fn toggle_with_id(id: i32, connection: &SqlitePool) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - UPDATE tasks - SET completed = NOT completed - WHERE id = $1 - "#, - id - ) - .execute(connection) - .await?; - - Ok(()) - } - - pub async fn delete_with_id(id: i32, connection: &SqlitePool) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - DELETE FROM tasks - WHERE id = $1 - "#, - id - ) - .execute(connection) - .await?; - - Ok(()) - } -} diff --git a/sqlx/src/models.rs b/sqlx/src/models.rs new file mode 100644 index 0000000..f6dc391 --- /dev/null +++ b/sqlx/src/models.rs @@ -0,0 +1,131 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use sqlx::types::Uuid; + +#[derive(Debug, Deserialize)] +pub struct NewItem { + pub title: String, + pub description: Option, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateItem { + pub title: Option, + pub description: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Item { + pub id: Uuid, + pub title: String, + pub description: Option, +} + +impl Item { + pub async fn all(connection: &PgPool) -> Result, sqlx::Error> { + let items = sqlx::query_as!( + Item, + r#" + SELECT * + FROM items + "# + ) + .fetch_all(connection) + .await?; + + Ok(items) + } + + pub async fn insert(item_data: NewItem, connection: &PgPool) -> Result<(), sqlx::Error> { + let mut updates = Vec::new(); + let mut params = HashMap::new(); + + // if let Some(title) = item_data.title { + // updates.push("title = $1"); + // params.insert(1, title); + // } + if let Some(description) = item_data.description { + updates.push("description = $2"); + params.insert(2, description); + } + + // if updates.is_empty() { + // return Ok(()); // Нет данных для обновления + // } + + let query = format!( + "INSERT INTO items VALUES ({}, {})", + item_data.title, + updates.join("") + ); + + let mut query_builder = sqlx::query(&query); + for (i, value) in params { + query_builder = query_builder.bind(value); + } + // query_builder = query_builder.bind(id); + + query_builder.execute(connection).await?; + + // sqlx::query!( + // r#" + // INSERT INTO items (title, description) + // VALUES ($1, $2) + // "#, + // item_data.title, + // item_data.description, + // ) + // .execute(connection) + // .await?; + + Ok(()) + } + + pub async fn update(id: Uuid, item_data: UpdateItem, connection: &PgPool) -> Result<(), sqlx::Error> { + let mut updates = Vec::new(); + let mut params = HashMap::new(); + + if let Some(title) = item_data.title { + updates.push("title = $1"); + params.insert(1, title); + } + if let Some(description) = item_data.description { + updates.push("description = $2"); + params.insert(2, description); + } + + if updates.is_empty() { + return Ok(()); // Нет данных для обновления + } + + let query = format!( + "UPDATE items SET {} WHERE id = $3", + updates.join(", ") + ); + + let mut query_builder = sqlx::query(&query); + for (i, value) in params { + query_builder = query_builder.bind(value); + } + query_builder = query_builder.bind(id); + + query_builder.execute(connection).await?; + + Ok(()) + } + + pub async fn delete_with_id(id: Uuid, connection: &PgPool) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM items + WHERE id = $1 + "#, + id + ) + .execute(connection) + .await?; + + Ok(()) + } +} diff --git a/sqlx/static/css/normalize.css b/sqlx/static/css/normalize.css deleted file mode 100644 index 458eea1..0000000 --- a/sqlx/static/css/normalize.css +++ /dev/null @@ -1,427 +0,0 @@ -/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ - -/** - * 1. Set default font family to sans-serif. - * 2. Prevent iOS text size adjust after orientation change, without disabling - * user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove default margin. - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Correct `block` display not defined for any HTML5 element in IE 8/9. - * Correct `block` display not defined for `details` or `summary` in IE 10/11 - * and Firefox. - * Correct `block` display not defined for `main` in IE 11. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -/** - * 1. Correct `inline-block` display not defined in IE 8/9. - * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. - */ - -audio, -canvas, -progress, -video { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Prevent modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Address `[hidden]` styling not present in IE 8/9/10. - * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. - */ - -[hidden], -template { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * Remove the gray background color from active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * Improve readability when focused and also mouse hovered in all browsers. - */ - -a:active, -a:hover { - outline: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Address styling not present in IE 8/9/10/11, Safari, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/** - * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/** - * Address styling not present in Safari and Chrome. - */ - -dfn { - font-style: italic; -} - -/** - * Address variable `h1` font-size and margin within `section` and `article` - * contexts in Firefox 4+, Safari, and Chrome. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Address styling not present in IE 8/9. - */ - -mark { - background: #ff0; - color: #000; -} - -/** - * Address inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove border when inside `a` element in IE 8/9/10. - */ - -img { - border: 0; -} - -/** - * Correct overflow not hidden in IE 9/10/11. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * Address margin not present in IE 8/9 and Safari. - */ - -figure { - margin: 1em 40px; -} - -/** - * Address differences between Firefox and other browsers. - */ - -hr { - -moz-box-sizing: content-box; - box-sizing: content-box; - height: 0; -} - -/** - * Contain overflow in all browsers. - */ - -pre { - overflow: auto; -} - -/** - * Address odd `em`-unit font size rendering in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -/* Forms - ========================================================================== */ - -/** - * Known limitation: by default, Chrome and Safari on OS X allow very limited - * styling of `select`, unless a `border` property is set. - */ - -/** - * 1. Correct color not being inherited. - * Known issue: affects color of disabled elements. - * 2. Correct font properties not being inherited. - * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. - */ - -button, -input, -optgroup, -select, -textarea { - color: inherit; /* 1 */ - font: inherit; /* 2 */ - margin: 0; /* 3 */ -} - -/** - * Address `overflow` set to `hidden` in IE 8/9/10/11. - */ - -button { - overflow: visible; -} - -/** - * Address inconsistent `text-transform` inheritance for `button` and `select`. - * All other form control elements do not inherit `text-transform` values. - * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. - * Correct `select` style inheritance in Firefox. - */ - -button, -select { - text-transform: none; -} - -/** - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Correct inability to style clickable `input` types in iOS. - * 3. Improve usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/** - * Re-set default cursor for disabled elements. - */ - -button[disabled], -html input[disabled] { - cursor: default; -} - -/** - * Remove inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/** - * Address Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -input { - line-height: normal; -} - -/** - * It's recommended that you don't attempt to style these elements. - * Firefox's implementation doesn't respect box-sizing, padding, or width. - * - * 1. Address box sizing set to `content-box` in IE 8/9/10. - * 2. Remove excess padding in IE 8/9/10. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Fix the cursor style for Chrome's increment/decrement buttons. For certain - * `font-size` values of the `input`, it causes the cursor style of the - * decrement button to change from `default` to `text`. - */ - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome - * (include `-moz` to future-proof). - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/** - * Remove inner padding and search cancel button in Safari and Chrome on OS X. - * Safari (but not Chrome) clips the cancel button when the search input has - * padding (and `textfield` appearance). - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct `color` not being inherited in IE 8/9/10/11. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Remove default vertical scrollbar in IE 8/9/10/11. - */ - -textarea { - overflow: auto; -} - -/** - * Don't inherit the `font-weight` (applied by a rule above). - * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. - */ - -optgroup { - font-weight: bold; -} - -/* Tables - ========================================================================== */ - -/** - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -} diff --git a/sqlx/static/css/skeleton.css b/sqlx/static/css/skeleton.css deleted file mode 100644 index 4855df4..0000000 --- a/sqlx/static/css/skeleton.css +++ /dev/null @@ -1,421 +0,0 @@ -/* -* Skeleton V2.0.4 -* Copyright 2014, Dave Gamache -* www.getskeleton.com -* Free to use under the MIT license. -* http://www.opensource.org/licenses/mit-license.php -* 12/29/2014 -*/ - - -/* Table of contents -–––––––––––––––––––––––––––––––––––––––––––––––––– -- Grid -- Base Styles -- Typography -- Links -- Buttons -- Forms -- Lists -- Code -- Tables -- Spacing -- Utilities -- Clearing -- Media Queries -*/ - - -/* Grid -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.container { - position: relative; - width: 100%; - max-width: 960px; - margin: 0 auto; - padding: 0 20px; - box-sizing: border-box; } -.column, -.columns { - width: 100%; - float: left; - box-sizing: border-box; } - -/* For devices larger than 400px */ -@media (min-width: 400px) { - .container { - width: 85%; - padding: 0; } -} - -/* For devices larger than 550px */ -@media (min-width: 550px) { - .container { - width: 80%; } - .column, - .columns { - margin-left: 4%; } - .column:first-child, - .columns:first-child { - margin-left: 0; } - - .one.column, - .one.columns { width: 4.66666666667%; } - .two.columns { width: 13.3333333333%; } - .three.columns { width: 22%; } - .four.columns { width: 30.6666666667%; } - .five.columns { width: 39.3333333333%; } - .six.columns { width: 48%; } - .seven.columns { width: 56.6666666667%; } - .eight.columns { width: 65.3333333333%; } - .nine.columns { width: 74.0%; } - .ten.columns { width: 82.6666666667%; } - .eleven.columns { width: 91.3333333333%; } - .twelve.columns { width: 100%; margin-left: 0; } - - .one-third.column { width: 30.6666666667%; } - .two-thirds.column { width: 65.3333333333%; } - - .one-half.column { width: 48%; } - - /* Offsets */ - .offset-by-one.column, - .offset-by-one.columns { margin-left: 8.66666666667%; } - .offset-by-two.column, - .offset-by-two.columns { margin-left: 17.3333333333%; } - .offset-by-three.column, - .offset-by-three.columns { margin-left: 26%; } - .offset-by-four.column, - .offset-by-four.columns { margin-left: 34.6666666667%; } - .offset-by-five.column, - .offset-by-five.columns { margin-left: 43.3333333333%; } - .offset-by-six.column, - .offset-by-six.columns { margin-left: 52%; } - .offset-by-seven.column, - .offset-by-seven.columns { margin-left: 60.6666666667%; } - .offset-by-eight.column, - .offset-by-eight.columns { margin-left: 69.3333333333%; } - .offset-by-nine.column, - .offset-by-nine.columns { margin-left: 78.0%; } - .offset-by-ten.column, - .offset-by-ten.columns { margin-left: 86.6666666667%; } - .offset-by-eleven.column, - .offset-by-eleven.columns { margin-left: 95.3333333333%; } - - .offset-by-one-third.column, - .offset-by-one-third.columns { margin-left: 34.6666666667%; } - .offset-by-two-thirds.column, - .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } - - .offset-by-one-half.column, - .offset-by-one-half.columns { margin-left: 52%; } - -} - - -/* Base Styles -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* NOTE -html is set to 62.5% so that all the REM measurements throughout Skeleton -are based on 10px sizing. So basically 1.5rem = 15px :) */ -html { - font-size: 62.5%; } -body { - font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ - line-height: 1.6; - font-weight: 400; - font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #222; } - - -/* Typography -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -h1, h2, h3, h4, h5, h6 { - margin-top: 0; - margin-bottom: 2rem; - font-weight: 300; } -h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} -h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } -h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } -h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } -h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } -h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } - -/* Larger than phablet */ -@media (min-width: 550px) { - h1 { font-size: 5.0rem; } - h2 { font-size: 4.2rem; } - h3 { font-size: 3.6rem; } - h4 { font-size: 3.0rem; } - h5 { font-size: 2.4rem; } - h6 { font-size: 1.5rem; } -} - -p { - margin-top: 0; } - - -/* Links -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -a { - color: #1EAEDB; } -a:hover { - color: #0FA0CE; } - - -/* Buttons -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.button, -button, -input[type="submit"], -input[type="reset"], -input[type="button"] { - display: inline-block; - height: 38px; - padding: 0 30px; - color: #555; - text-align: center; - font-size: 11px; - font-weight: 600; - line-height: 38px; - letter-spacing: .1rem; - text-transform: uppercase; - text-decoration: none; - white-space: nowrap; - background-color: transparent; - border-radius: 4px; - border: 1px solid #bbb; - cursor: pointer; - box-sizing: border-box; } -.button:hover, -button:hover, -input[type="submit"]:hover, -input[type="reset"]:hover, -input[type="button"]:hover, -.button:focus, -button:focus, -input[type="submit"]:focus, -input[type="reset"]:focus, -input[type="button"]:focus { - color: #333; - border-color: #888; - outline: 0; } -.button.button-primary, -button.button-primary, -button.primary, -input[type="submit"].button-primary, -input[type="reset"].button-primary, -input[type="button"].button-primary { - color: #FFF; - background-color: #33C3F0; - border-color: #33C3F0; } -.button.button-primary:hover, -button.button-primary:hover, -button.primary:hover, -input[type="submit"].button-primary:hover, -input[type="reset"].button-primary:hover, -input[type="button"].button-primary:hover, -.button.button-primary:focus, -button.button-primary:focus, -button.primary:focus, -input[type="submit"].button-primary:focus, -input[type="reset"].button-primary:focus, -input[type="button"].button-primary:focus { - color: #FFF; - background-color: #1EAEDB; - border-color: #1EAEDB; } - - -/* Forms -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea, -select { - height: 38px; - padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ - background-color: #fff; - border: 1px solid #D1D1D1; - border-radius: 4px; - box-shadow: none; - box-sizing: border-box; } -/* Removes awkward default styles on some inputs for iOS */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; } -textarea { - min-height: 65px; - padding-top: 6px; - padding-bottom: 6px; } -input[type="email"]:focus, -input[type="number"]:focus, -input[type="search"]:focus, -input[type="text"]:focus, -input[type="tel"]:focus, -input[type="url"]:focus, -input[type="password"]:focus, -textarea:focus, -select:focus { - border: 1px solid #33C3F0; - outline: 0; } -label, -legend { - display: block; - margin-bottom: .5rem; - font-weight: 600; } -fieldset { - padding: 0; - border-width: 0; } -input[type="checkbox"], -input[type="radio"] { - display: inline; } -label > .label-body { - display: inline-block; - margin-left: .5rem; - font-weight: normal; } - - -/* Lists -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -ul { - list-style: circle inside; } -ol { - list-style: decimal inside; } -ol, ul { - padding-left: 0; - margin-top: 0; } -ul ul, -ul ol, -ol ol, -ol ul { - margin: 1.5rem 0 1.5rem 3rem; - font-size: 90%; } -li { - margin-bottom: 1rem; } - - -/* Code -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -code { - padding: .2rem .5rem; - margin: 0 .2rem; - font-size: 90%; - white-space: nowrap; - background: #F1F1F1; - border: 1px solid #E1E1E1; - border-radius: 4px; } -pre > code { - display: block; - padding: 1rem 1.5rem; - white-space: pre; } - - -/* Tables -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -th, -td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid #E1E1E1; } -th:first-child, -td:first-child { - padding-left: 0; } -th:last-child, -td:last-child { - padding-right: 0; } - - -/* Spacing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -button, -.button { - margin-bottom: 1rem; } -input, -textarea, -select, -fieldset { - margin-bottom: 1.5rem; } -pre, -blockquote, -dl, -figure, -table, -p, -ul, -ol, -form { - margin-bottom: 2.5rem; } - - -/* Utilities -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.u-full-width { - width: 100%; - box-sizing: border-box; } -.u-max-full-width { - max-width: 100%; - box-sizing: border-box; } -.u-pull-right { - float: right; } -.u-pull-left { - float: left; } - - -/* Misc -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -hr { - margin-top: 3rem; - margin-bottom: 3.5rem; - border-width: 0; - border-top: 1px solid #E1E1E1; } - - -/* Clearing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ - -/* Self Clearing Goodness */ -.container:after, -.row:after, -.u-cf { - content: ""; - display: table; - clear: both; } - - -/* Media Queries -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* -Note: The best way to structure the use of media queries is to create the queries -near the relevant code. For example, if you wanted to change the styles for buttons -on small devices, paste the mobile query code up in the buttons section and style it -there. -*/ - - -/* Larger than mobile */ -@media (min-width: 400px) {} - -/* Larger than phablet (also point when grid becomes active) */ -@media (min-width: 550px) {} - -/* Larger than tablet */ -@media (min-width: 750px) {} - -/* Larger than desktop */ -@media (min-width: 1000px) {} - -/* Larger than Desktop HD */ -@media (min-width: 1200px) {} diff --git a/sqlx/static/css/style.css b/sqlx/static/css/style.css deleted file mode 100644 index cdf155e..0000000 --- a/sqlx/static/css/style.css +++ /dev/null @@ -1,58 +0,0 @@ -.field-error { - border: 1px solid #ff0000 !important; -} - -.field-error-msg { - color: #ff0000; - display: block; - margin: -10px 0 10px 0; -} - -.field-success { - border: 1px solid #5AB953 !important; -} - -.field-success-msg { - color: #5AB953; - display: block; - margin: -10px 0 10px 0; -} - -span.completed { - text-decoration: line-through; -} - -form.inline { - display: inline; -} - -form.link, -button.link { - display: inline; - color: #1EAEDB; - border: none; - outline: none; - background: none; - cursor: pointer; - padding: 0; - margin: 0 0 0 0; - height: inherit; - text-decoration: underline; - font-size: inherit; - text-transform: none; - font-weight: normal; - line-height: inherit; - letter-spacing: inherit; -} - -form.link:hover, button.link:hover { - color: #0FA0CE; -} - -button.small { - height: 20px; - padding: 0 10px; - font-size: 10px; - line-height: 20px; - margin: 0 2.5px; -} diff --git a/sqlx/static/errors/400.html b/sqlx/static/errors/400.html deleted file mode 100644 index be66536..0000000 --- a/sqlx/static/errors/400.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - The server could not understand the request (400) - - - - - - - -
-
-

The server could not understand the request

-
-
- - diff --git a/sqlx/static/errors/404.html b/sqlx/static/errors/404.html deleted file mode 100644 index 2205a07..0000000 --- a/sqlx/static/errors/404.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - The page you were looking for doesn't exist (404) - - - - - - - -
-
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
-
- - diff --git a/sqlx/static/errors/500.html b/sqlx/static/errors/500.html deleted file mode 100644 index c2710c2..0000000 --- a/sqlx/static/errors/500.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - Ooops (500) - - - - - - - -
-
-

Ooops ...

-

How embarrassing!

-

Looks like something weird happened while processing your request.

-

Please try again in a few moments.

-
-
- - diff --git a/sqlx/templates/index.html.tera b/sqlx/templates/index.html.tera deleted file mode 100644 index 64cbf85..0000000 --- a/sqlx/templates/index.html.tera +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - Actix Todo Example - - - - - - - -
-

- -
-

Actix Todo

-
-
- - {% if msg %} - - {{msg.1}} - - {% endif %} -
-
- -
-
-
- -
-
-
    - {% for task in tasks %} -
  • - {% if task.completed %} - {{task.description}} -
    - - -
    -
    - - -
    - {% else %} - - {% endif %} -
  • - {% endfor %} -
-
-
-
- - From e993bda5dfd94a1dd07e28d513a4c2120fa1e330 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Mon, 10 Feb 2025 23:47:55 +0300 Subject: [PATCH 04/12] feat: updated README.md --- sqlx/README.md | 100 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 78 insertions(+), 22 deletions(-) diff --git a/sqlx/README.md b/sqlx/README.md index 2b8df3c..cde2608 100644 --- a/sqlx/README.md +++ b/sqlx/README.md @@ -1,49 +1,105 @@ -# Todo +# SQLx -A simple Todo project using a SQLite database. +Basic integration of [sqlx](https://github.com/launchbadge/sqlx) using PostgreSQL for Actix Web. -## Prerequisites +## Usage -- SQLite 3 +### Prerequisites +Set up PostgreSQL -## Change Into The Examples Workspace Root Directory +```sh +# on any OS +docker run -d --restart unless-stopped --name postgresql -e POSTGRES_USER=sqlx-user -e POSTGRES_PASSWORD=password -p 5432:5432 -v postgres_data:/var/lib/postgresql/data postgres:alpine +``` + +### Initialize PostgreSQL Database + +```sh +cd sqlx +cargo install sqlx-cli --no-default-features --features postgres + +echo DATABASE_URL=postgres://sqlx-user:password@localhost:5432/sqlx-db > .env +sqlx database create +sqlx migrate run +``` + +You can verify that the database has been created in your PostgreSQL instance. +```sh +docker exec -i postgresql psql -U test-user -c "\l" +``` + +### Running Server + +```sh +cargo run + +# Started http server: 127.0.0.1:8080 +``` + +### Available Routes + +#### `POST /items` + +Inserts a new item into the PostgreSQL DB. + +Provide a JSON payload with a name with/out description. Eg: + +```json +{ "name": "thingamajig", "description": "This is awesome thing!" } +``` -All instructions assume you have changed into the examples workspace root: +On success, a response like the following is returned: -```console -$ pwd -.../examples +```json +{ + "id": "01948982-67d0-7a55-b4b1-8b8b962d8c6b", + "name": "thingamajig", + "description": "This is awesome thing!" +} ``` -## Set Up The Database +
+ Client Examples -Install the [sqlx](https://github.com/launchbadge/sqlx/tree/HEAD/sqlx-cli) command-line tool with the required features: +Using [HTTPie]: ```sh -cargo install sqlx-cli --no-default-features --features=rustls,sqlite +http POST localhost:8080/items name=thingamajig ``` -Then to create and set-up the database run: +Using cURL: ```sh -sqlx database create --database-url=sqlite://./todo.db -sqlx migrate run --database-url=sqlite://./todo.db +curl -S -X POST --header "Content-Type: application/json" --data '{"name":"thingamajig"}' http://localhost:8080/items ``` -## Run The Application +
-Start the application with: +#### `GET /items/{item_id}` + +Gets an item from the DB using its ID (UUID) (returned from the insert request or taken from the DB directly). Returns a 404 when no item exists with that ID. + +
+ Client Examples + +Using [HTTPie]: ```sh -cargo run --bin=todo +http localhost:8080/items/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 ``` -The app will be viewable in the browser at . +Using cURL: -## Modifying The Example Database +```sh +curl -S http://localhost:8080/items/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 +``` + +
-For simplicity, this example uses SQLx's offline mode. If you make any changes to the database schema, this must be turned off or the `sqlx-data.json` file regenerated using the following command: +### Explore The PostgreSQL DB ```sh -cargo sqlx prepare --database-url=sqlite://./todo.db +docker exec -i postgresql psql -U sqlx-user -d sqlx-db -c "select * from items" ``` + +[httpie]: https://httpie.io/cli From c60232eea58c5b2c01f9da91c428ab713f7e3176 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Mon, 10 Feb 2025 23:50:12 +0300 Subject: [PATCH 05/12] feat: deleted session.rs and all related stuff --- sqlx/src/main.rs | 15 --------------- sqlx/src/session.rs | 39 --------------------------------------- 2 files changed, 54 deletions(-) delete mode 100644 sqlx/src/session.rs diff --git a/sqlx/src/main.rs b/sqlx/src/main.rs index f68b867..7669bd7 100644 --- a/sqlx/src/main.rs +++ b/sqlx/src/main.rs @@ -10,18 +10,13 @@ use dotenvy::dotenv; mod api; mod db; mod models; -// mod session; -// NOTE: Not a suitable session key for production. -static SESSION_SIGNING_KEY: &[u8] = &[0; 64]; #[actix_web::main] async fn main() -> io::Result<()> { dotenv().ok(); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); - // let key = actix_web::cookie::Key::from(SESSION_SIGNING_KEY); - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let pool = db::init_pool(&database_url) .await @@ -32,13 +27,6 @@ async fn main() -> io::Result<()> { HttpServer::new(move || { log::debug!("Constructing the App"); - // let mut templates = Tera::new("templates/**/*").expect("errors in tera templates"); - // templates.autoescape_on(vec!["tera"]); - - // let session_store = SessionMiddleware::builder(CookieSessionStore::default(), key.clone()) - // .cookie_secure(false) - // .build(); - // let error_handlers = ErrorHandlers::new() // .handler( // http::StatusCode::INTERNAL_SERVER_ERROR, @@ -48,16 +36,13 @@ async fn main() -> io::Result<()> { // .handler(http::StatusCode::NOT_FOUND, api::not_found); App::new() - // .app_data(web::Data::new(templates)) .app_data(web::Data::new(pool.clone())) .wrap(Logger::default()) - // .wrap(session_store) // .wrap(error_handlers) .service(web::resource("/items").route(web::get().to(api::get_items))) .service(web::resource("/items").route(web::post().to(api::create_item))) .service(web::resource("/items/{id}").route(web::patch().to(api::update))) .service(web::resource("/items/{id}").route(web::delete().to(api::delete))) - // .service(Files::new("/static", "./static")) }) .bind(("127.0.0.1", 8080))? .workers(2) diff --git a/sqlx/src/session.rs b/sqlx/src/session.rs deleted file mode 100644 index 5230549..0000000 --- a/sqlx/src/session.rs +++ /dev/null @@ -1,39 +0,0 @@ -use actix_session::Session; -use actix_web::error::Result; -use serde::{Deserialize, Serialize}; - -const FLASH_KEY: &str = "flash"; - -pub fn set_flash(session: &Session, flash: FlashMessage) -> Result<()> { - Ok(session.insert(FLASH_KEY, flash)?) -} - -pub fn get_flash(session: &Session) -> Result> { - Ok(session.get::(FLASH_KEY)?) -} - -pub fn clear_flash(session: &Session) { - session.remove(FLASH_KEY); -} - -#[derive(Deserialize, Serialize)] -pub struct FlashMessage { - pub kind: String, - pub message: String, -} - -impl FlashMessage { - pub fn success(message: &str) -> Self { - Self { - kind: "success".to_owned(), - message: message.to_owned(), - } - } - - pub fn error(message: &str) -> Self { - Self { - kind: "error".to_owned(), - message: message.to_owned(), - } - } -} From 5981638e70ec4e3a8be4e2888390eebe570df2b6 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Mon, 10 Feb 2025 23:53:13 +0300 Subject: [PATCH 06/12] feat: deleted commented code --- sqlx/src/api.rs | 53 +--------------------------------------------- sqlx/src/db.rs | 4 ++-- sqlx/src/models.rs | 4 ++-- 3 files changed, 5 insertions(+), 56 deletions(-) diff --git a/sqlx/src/api.rs b/sqlx/src/api.rs index 607ebfc..02319b9 100644 --- a/sqlx/src/api.rs +++ b/sqlx/src/api.rs @@ -1,67 +1,36 @@ -// use actix_files::NamedFile; -// use actix_session::Session; use actix_web::{ - dev, error, http::StatusCode, middleware::ErrorHandlerResponse, web, Error, HttpResponse, + error, http::StatusCode, web, Error, HttpResponse, Responder, Result, }; use serde::Deserialize; use sqlx::PgPool; use uuid::Uuid; -// use sqlx::types::Uuid; -// use tera::{Context, Tera}; use crate::{ db, models - // session::{self, FlashMessage}, }; use crate::models::UpdateItem; pub async fn get_items( pool: web::Data, - // tmpl: web::Data, - // session: Session, ) -> Result { let items = db::get_all_items(&pool) .await .map_err(error::ErrorInternalServerError)?; - // let mut context = Context::new(); - // context.insert("items", &items); - - // Session is set during operations on other endpoints that can redirect to index - // if let Some(flash) = session::get_flash(&session)? { - // context.insert("msg", &(flash.kind, flash.message)); - // session::clear_flash(&session); - // } - - // let rendered = tmpl - // .render("index.html.tera", &context) - // .map_err(error::ErrorInternalServerError)?; - Ok(HttpResponse::Ok().json(items)) } -// #[derive(Debug, Deserialize)] -// pub struct CreateForm { -// description: String, -// } pub async fn create_item( params: web::Json, pool: web::Data, - // session: Session, ) -> Result { - // if params.description.is_empty() { - // session::set_flash(&session, FlashMessage::error("Description cannot be empty"))?; - // Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND)) - // } else { db::create_item(params.into_inner(), &pool) .await .map_err(error::ErrorInternalServerError)?; - // session::set_flash(&session, FlashMessage::success("Task successfully added"))?; - Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND)) // } } @@ -76,23 +45,6 @@ pub struct UpdateForm { _method: String, } -// pub async fn update( -// db: web::Data, -// params: web::Path, -// form: web::Json, -// // session: Session, -// ) -> Result { -// Ok(web::Redirect::to(match form._method.as_ref() { -// "put" => toggle(db, params).await?, -// // "delete" => delete(db, params, session).await?, -// unsupported_method => { -// let msg = format!("Unsupported HTTP method: {unsupported_method}"); -// return Err(error::ErrorBadRequest(msg)); -// } -// }) -// .using_status_code(StatusCode::FOUND)) -// } - pub async fn update( pool: web::Data, id: web::Path, @@ -108,14 +60,11 @@ pub async fn update( pub async fn delete( pool: web::Data, params: web::Path, - // session: Session, ) -> Result<&'static str, Error> { db::delete_item(params.id, &pool) .await .map_err(error::ErrorInternalServerError)?; - // session::set_flash(&session, FlashMessage::success("Task was deleted."))?; - Ok("/") } diff --git a/sqlx/src/db.rs b/sqlx/src/db.rs index 97220be..042c6f7 100644 --- a/sqlx/src/db.rs +++ b/sqlx/src/db.rs @@ -15,7 +15,7 @@ pub async fn get_all_items(pool: &PgPool) -> Result, &'static str> { pub async fn create_item(item_data: NewItem, pool: &PgPool) -> Result<(), &'static str> { let new_item = NewItem { title: item_data.title, description: item_data.description }; - Item::insert(new_item, pool) + Item::create(new_item, pool) .await .map(|_| ()) .map_err(|_| "Error inserting item") @@ -29,7 +29,7 @@ pub async fn update_item(id: Uuid, data: UpdateItem, pool: &PgPool) -> Result<() } pub async fn delete_item(id: Uuid, pool: &PgPool) -> Result<(), &'static str> { - Item::delete_with_id(id, pool) + Item::delete(id, pool) .await .map(|_| ()) .map_err(|_| "Error deleting item") diff --git a/sqlx/src/models.rs b/sqlx/src/models.rs index f6dc391..511007a 100644 --- a/sqlx/src/models.rs +++ b/sqlx/src/models.rs @@ -37,7 +37,7 @@ impl Item { Ok(items) } - pub async fn insert(item_data: NewItem, connection: &PgPool) -> Result<(), sqlx::Error> { + pub async fn create(item_data: NewItem, connection: &PgPool) -> Result<(), sqlx::Error> { let mut updates = Vec::new(); let mut params = HashMap::new(); @@ -115,7 +115,7 @@ impl Item { Ok(()) } - pub async fn delete_with_id(id: Uuid, connection: &PgPool) -> Result<(), sqlx::Error> { + pub async fn delete(id: Uuid, connection: &PgPool) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM items From c1cbcda5693b19a88be7e640614dd3530ff0d0ab Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 11 Feb 2025 01:39:18 +0300 Subject: [PATCH 07/12] fix(docs): seaorm README.md actualized --- seaorm/README.md | 113 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/seaorm/README.md b/seaorm/README.md index 88fab09..69b5ecb 100644 --- a/seaorm/README.md +++ b/seaorm/README.md @@ -1,25 +1,110 @@ -![screenshot](Screenshot.png) +# SeaORM -# Actix 4 with SeaORM example app +Basic integration of [seaorm](https://github.com/SeaQL/sea-orm) using PostgreSQL for Actix Web. -1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database +## Usage -1. Turn on the appropriate database feature for your chosen db in `service/Cargo.toml` (the `"sqlx-mysql",` line) +### Prerequisites +Set up PostgreSQL -1. Execute `cargo run` to start the server +```sh +# on any OS +docker run -d --restart unless-stopped --name postgresql -e POSTGRES_USER=seaorm-user -e POSTGRES_PASSWORD=password -p 5432:5432 -v postgres_data:/var/lib/postgresql/data postgres:alpine +``` + +### Initialize PostgreSQL Database + +Create database +```sh +docker exec -i postgresql createdb -U seaorm-user searom-db +``` + +Create tables +```sh +cd seaorm +cargo install cargo install sea-orm-cli -1. Visit [localhost:8000](http://localhost:8000) in browser +echo DATABASE_URL=postgres://sqlx-user:password@localhost:5432/searom-db > .env +sqlx sea-orm-cli migrate up +``` + +You can verify that the database has been created in your PostgreSQL instance. +```sh +docker exec -i postgresql psql -U seaorm-user -c "\l" +``` -Run server with auto-reloading: +### Running Server -```bash -cargo install systemfd cargo-watch -systemfd --no-pid -s http::8000 -- cargo watch -x run +```sh +cargo run + +# Started http server: 127.0.0.1:8080 ``` -Run mock test on the service logic crate: +### Available Routes + +#### `POST /items` + +Inserts a new item into the PostgreSQL DB. + +Provide a JSON payload with a name with/out description. Eg: + +```json +{ "name": "thingamajig", "description": "This is awesome thing!" } +``` + +On success, a response like the following is returned: + +```json +{ + "id": "01948982-67d0-7a55-b4b1-8b8b962d8c6b", + "name": "thingamajig", + "description": "This is awesome thing!" +} +``` + +
+ Client Examples + +Using [HTTPie]: -```bash -cd service -cargo test --features mock +```sh +http POST localhost:8080/items name=thingamajig ``` + +Using cURL: + +```sh +curl -S -X POST --header "Content-Type: application/json" --data '{"name":"thingamajig"}' http://localhost:8080/items +``` + +
+ +#### `GET /items/{item_id}` + +Gets an item from the DB using its ID (UUID) (returned from the insert request or taken from the DB directly). Returns a 404 when no item exists with that ID. + +
+ Client Examples + +Using [HTTPie]: + +```sh +http localhost:8080/items/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 +``` + +Using cURL: + +```sh +curl -S http://localhost:8080/items/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 +``` + +
+ +### Explore The PostgreSQL DB + +```sh +docker exec -i postgresql psql -U sqlx-user -d sqlx-db -c "select * from items" +``` + +[httpie]: https://httpie.io/cli From 079bc474393e6c3fc11a894e23bed541782d0956 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 11 Feb 2025 01:41:05 +0300 Subject: [PATCH 08/12] fix(docs): sqlx README.md updated --- sqlx/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx/README.md b/sqlx/README.md index cde2608..f201ac0 100644 --- a/sqlx/README.md +++ b/sqlx/README.md @@ -25,7 +25,7 @@ sqlx migrate run You can verify that the database has been created in your PostgreSQL instance. ```sh -docker exec -i postgresql psql -U test-user -c "\l" +docker exec -i postgresql psql -U sqlx-user -c "\l" ``` ### Running Server From 0ef7bc880b3c9c77907c133908066583dcb812ee Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 11 Feb 2025 19:06:49 +0300 Subject: [PATCH 09/12] fix(docs): added some improvements and optimizations --- Cargo.toml | 4 +++- diesel/README.md | 2 +- diesel_async/Cargo.toml | 1 + diesel_async/README.md | 10 +++++----- .../down.sql | 0 .../up.sql | 1 - ...esel.toml => this_is_optional_cfg_file_diesel.toml} | 2 +- seaorm/Cargo.toml | 4 ++-- sqlx/src/api.rs | 4 ++-- sqlx/src/main.rs | 4 ++-- 10 files changed, 17 insertions(+), 15 deletions(-) rename diesel_async/migrations/{2025-01-18-144029_create_items => 2025-01-18-144029_create_table_items}/down.sql (100%) rename diesel_async/migrations/{2025-01-18-144029_create_items => 2025-01-18-144029_create_table_items}/up.sql (83%) rename diesel_async/{diesel.toml => this_is_optional_cfg_file_diesel.toml} (76%) diff --git a/Cargo.toml b/Cargo.toml index b12095a..801bb54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,9 +31,11 @@ actix-web = { version = "4.9", features = ["openssl", "rustls"] } actix-files = "0.6" serde = { version = "1", features = ["derive"] } serde_json = "1" +uuid = { version = "1.11", features = ["v7", "serde"] } futures = "0.3" futures-util = "0.3" dotenvy = "0.15" env_logger = "0.11" log = "0.4" -uuid = { version = "1.11", features = ["v7", "serde"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + diff --git a/diesel/README.md b/diesel/README.md index c24e083..f202aed 100644 --- a/diesel/README.md +++ b/diesel/README.md @@ -1,4 +1,4 @@ -# diesel +# Diesel Basic integration of [Diesel](https://diesel.rs) using SQLite for Actix Web. diff --git a/diesel_async/Cargo.toml b/diesel_async/Cargo.toml index 2775ca4..c9f3ae8 100644 --- a/diesel_async/Cargo.toml +++ b/diesel_async/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" actix-web.workspace = true diesel = { version = "2", default-features = false, features = ["uuid"] } diesel-async = { version = "0.5", features = ["postgres", "bb8", "async-connection-wrapper"] } +tracing-subscriber.workspace = true serde.workspace = true uuid.workspace = true dotenvy.workspace = true diff --git a/diesel_async/README.md b/diesel_async/README.md index 8eae699..21ab6b1 100644 --- a/diesel_async/README.md +++ b/diesel_async/README.md @@ -1,6 +1,6 @@ -# diesel +# Diesel-async -Basic integration of [Diesel-async](https://github.com/weiznich/diesel_async) using PostgreSQL for Actix Web. +A basic example of interaction between Actix Web and [Diesel-async](https://github.com/weiznich/diesel_async) using PostgreSQL. ## Usage @@ -8,7 +8,7 @@ Basic integration of [Diesel-async](https://github.com/weiznich/diesel_async) us ```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 +docker run -d --restart unless-stopped --name postgresql -e POSTGRES_USER=adiesel-user -e POSTGRES_PASSWORD=password -p 5432:5432 -v postgres_data:/var/lib/postgresql/data postgres:alpine ``` ### Initialize PostgreSQL Database @@ -17,7 +17,7 @@ docker run -d --restart unless-stopped --name postgresql -e POSTGRES_USER=test-u 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 +echo DATABASE_URL=postgres://adiesel-user:password@localhost:5432/adiesel-db > .env diesel setup diesel migration run ``` @@ -98,7 +98,7 @@ curl -S http://localhost:8080/items/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" +docker exec -i postgresql psql -U test-user -d adiesel-db -c "select * from public.items" ``` ## Using Other Databases diff --git a/diesel_async/migrations/2025-01-18-144029_create_items/down.sql b/diesel_async/migrations/2025-01-18-144029_create_table_items/down.sql similarity index 100% rename from diesel_async/migrations/2025-01-18-144029_create_items/down.sql rename to diesel_async/migrations/2025-01-18-144029_create_table_items/down.sql diff --git a/diesel_async/migrations/2025-01-18-144029_create_items/up.sql b/diesel_async/migrations/2025-01-18-144029_create_table_items/up.sql similarity index 83% rename from diesel_async/migrations/2025-01-18-144029_create_items/up.sql rename to diesel_async/migrations/2025-01-18-144029_create_table_items/up.sql index 7066001..719260a 100644 --- a/diesel_async/migrations/2025-01-18-144029_create_items/up.sql +++ b/diesel_async/migrations/2025-01-18-144029_create_table_items/up.sql @@ -1,4 +1,3 @@ --- Your SQL goes here CREATE TABLE IF NOT EXISTS items ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, diff --git a/diesel_async/diesel.toml b/diesel_async/this_is_optional_cfg_file_diesel.toml similarity index 76% rename from diesel_async/diesel.toml rename to diesel_async/this_is_optional_cfg_file_diesel.toml index 34c0a18..5f49e32 100644 --- a/diesel_async/diesel.toml +++ b/diesel_async/this_is_optional_cfg_file_diesel.toml @@ -6,4 +6,4 @@ file = "src/schema.rs" custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] [migrations_directory] -dir = "/home/alex/CLionProjects/actix-with-async-diesel/migrations" +dir = "/home/alex/CLionProjects/actix-practice/diesel_async/migrations" diff --git a/seaorm/Cargo.toml b/seaorm/Cargo.toml index e1e3e8e..4026b1a 100644 --- a/seaorm/Cargo.toml +++ b/seaorm/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" [dependencies] actix-web.workspace = true -dotenvy = "0.15" +dotenvy.workspace = true listenfd = "1" serde.workspace = true -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-subscriber.workspace = true sea-orm = { version = "1.1", features = [ "sqlx-postgres", "runtime-tokio", "macros", "with-uuid" ] } migration = { path = "./migration" } diff --git a/sqlx/src/api.rs b/sqlx/src/api.rs index 02319b9..473bd6f 100644 --- a/sqlx/src/api.rs +++ b/sqlx/src/api.rs @@ -45,7 +45,7 @@ pub struct UpdateForm { _method: String, } -pub async fn update( +pub async fn update_item( pool: web::Data, id: web::Path, data: web::Json, @@ -57,7 +57,7 @@ pub async fn update( Ok("/") } -pub async fn delete( +pub async fn delete_item( pool: web::Data, params: web::Path, ) -> Result<&'static str, Error> { diff --git a/sqlx/src/main.rs b/sqlx/src/main.rs index 7669bd7..e4d786d 100644 --- a/sqlx/src/main.rs +++ b/sqlx/src/main.rs @@ -41,8 +41,8 @@ async fn main() -> io::Result<()> { // .wrap(error_handlers) .service(web::resource("/items").route(web::get().to(api::get_items))) .service(web::resource("/items").route(web::post().to(api::create_item))) - .service(web::resource("/items/{id}").route(web::patch().to(api::update))) - .service(web::resource("/items/{id}").route(web::delete().to(api::delete))) + .service(web::resource("/items/{id}").route(web::patch().to(api::update_item))) + .service(web::resource("/items/{id}").route(web::delete().to(api::delete_item))) }) .bind(("127.0.0.1", 8080))? .workers(2) From b6e9e6c6a149389dfc67e5f0fa291a04bb0a1331 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 11 Feb 2025 19:20:05 +0300 Subject: [PATCH 10/12] refactor(structure): added new api module for diesel-async example --- diesel_async/src/api.rs | 58 +++++++++++++++++++ diesel_async/src/main.rs | 71 +++-------------------- seaorm/src/actions.rs | 8 +-- seaorm/src/api.rs | 100 ++++++++++++++++++++++++++++++++ seaorm/src/main.rs | 121 ++++----------------------------------- 5 files changed, 178 insertions(+), 180 deletions(-) create mode 100644 diesel_async/src/api.rs create mode 100644 seaorm/src/api.rs diff --git a/diesel_async/src/api.rs b/diesel_async/src/api.rs new file mode 100644 index 0000000..2b8169f --- /dev/null +++ b/diesel_async/src/api.rs @@ -0,0 +1,58 @@ +use actix_web::{error, get, post, web, HttpResponse, Responder}; +use uuid::Uuid; +use crate::{actions, models, DbPool}; + +/// 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)) +} \ No newline at end of file diff --git a/diesel_async/src/main.rs b/diesel_async/src/main.rs index c4ea5ad..71d3bdf 100644 --- a/diesel_async/src/main.rs +++ b/diesel_async/src/main.rs @@ -1,76 +1,22 @@ #[macro_use] extern crate diesel; -use actix_web::{error, get, middleware, post, web, App, HttpResponse, HttpServer, Responder}; +use actix_web::{ middleware, web, App, HttpServer, }; use diesel_async::{ pooled_connection::{bb8::Pool, AsyncDieselConnectionManager}, AsyncPgConnection, }; use dotenvy::dotenv; use std::{env, io}; -use uuid::Uuid; + mod actions; mod models; mod schema; +mod api; 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(); @@ -88,8 +34,8 @@ async fn main() -> io::Result<()> { // add request logger middleware .wrap(middleware::Logger::default()) // add route handlers - .service(add_item) - .service(get_item) + .service(api::add_item) + .service(api::get_item) }) .bind(("127.0.0.1", 8080))? .run() @@ -106,12 +52,13 @@ async fn initialize_db_pool() -> DbPool { 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 uuid::Uuid; use super::*; #[actix_web::test] @@ -125,8 +72,8 @@ mod tests { App::new() .app_data(web::Data::new(pool.clone())) .wrap(middleware::Logger::default()) - .service(get_item) - .service(add_item), + .service(api::get_item) + .service(api::add_item), ) .await; diff --git a/seaorm/src/actions.rs b/seaorm/src/actions.rs index 351fe3b..ce2aad2 100644 --- a/seaorm/src/actions.rs +++ b/seaorm/src/actions.rs @@ -1,4 +1,3 @@ -// use ::entity::{item, Entity as Item}; use crate::models::{ActiveModel, Column, Entity as Item, Model}; use sea_orm::prelude::Uuid; use sea_orm::*; @@ -51,9 +50,6 @@ impl Mutation { } } -// use ::entity::{item, Entity as Item}; -// use sea_orm::*; - pub struct Query; impl Query { @@ -67,13 +63,11 @@ impl Query { page: u64, items_per_page: u64, ) -> Result<(Vec, u64), DbErr> { - // Setup paginator let paginator = Item::find() .order_by_asc(Column::Id) .paginate(db, items_per_page); let num_pages = paginator.num_pages().await?; - - // Fetch paginated items + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) } } diff --git a/seaorm/src/api.rs b/seaorm/src/api.rs new file mode 100644 index 0000000..c011819 --- /dev/null +++ b/seaorm/src/api.rs @@ -0,0 +1,100 @@ +use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse, Responder}; +use sea_orm::prelude::Uuid; +use serde::{Deserialize, Serialize}; +use crate::{AppState, DEFAULT_POSTS_PER_PAGE}; +use crate::actions::{Mutation, Query}; +use crate::models::Model; + +#[derive(Debug, Deserialize)] +pub struct Params { + page: Option, + items_per_page: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +struct FlashData { + kind: String, + message: String, +} + +#[get("/items")] +async fn list_items( + req: HttpRequest, + data: web::Data, +) -> actix_web::Result { + let conn = &data.conn; + + // get params + let params = web::Query::::from_query(req.query_string())?; + + let page = params.page.unwrap_or(1); + let items_per_page = params.items_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + + let (items, num_pages) = Query::find_items_in_page(conn, page, items_per_page) + .await + .expect("Cannot find items in page"); + + Ok(HttpResponse::Ok().json(items)) +} + +#[post("/items")] +async fn create_item( + data: web::Data, + item_data: web::Json, +) -> actix_web::Result { + let conn = &data.conn; + + let item = Mutation::create_item(conn, item_data.into_inner()) + .await + .expect("could not insert item"); + + Ok(HttpResponse::Created().json(item)) +} + +#[get("/items/{id}")] +async fn get_item( + data: web::Data, + id: web::Path, +) -> actix_web::Result { + let conn = &data.conn; + + let id = id.into_inner(); + + let item: Option = Query::find_item_by_id(conn, id) + .await + .expect("could not find item"); + + Ok(HttpResponse::Ok().json(item)) +} + +#[put("/{id}")] +async fn update_item( + data: web::Data, + id: web::Path, + item_form: web::Json, +) -> actix_web::Result { + let conn = &data.conn; + let form = item_form.into_inner(); + let id = id.into_inner(); + + let item = Mutation::update_item_by_id(conn, id, form) + .await + .expect("could not edit item"); + + Ok(HttpResponse::Accepted().json(item)) +} + +#[delete("/items/{id}")] +async fn delete_item( + data: web::Data, + id: web::Path, +) -> actix_web::Result { + let conn = &data.conn; + let id = id.into_inner(); + + Mutation::delete_item(conn, id) + .await + .expect("could not delete item"); + + Ok(HttpResponse::NoContent()) +} \ No newline at end of file diff --git a/seaorm/src/main.rs b/seaorm/src/main.rs index 736acf3..6d7262e 100644 --- a/seaorm/src/main.rs +++ b/seaorm/src/main.rs @@ -1,17 +1,14 @@ mod actions; mod models; +mod api; use actix_web::{ - delete, get, middleware, post, put, web, App, HttpRequest, HttpResponse, HttpServer, Responder, + middleware, web, App, HttpServer, }; -use crate::actions::{Mutation, Query}; -use crate::models::Model; use listenfd::ListenFd; use migration::{Migrator, MigratorTrait}; -use sea_orm::prelude::Uuid; use sea_orm::{Database, DatabaseConnection}; -use serde::{Deserialize, Serialize}; use std::env; const DEFAULT_POSTS_PER_PAGE: u64 = 5; @@ -21,120 +18,22 @@ struct AppState { conn: DatabaseConnection, } -#[derive(Debug, Deserialize)] -pub struct Params { - page: Option, - items_per_page: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -struct FlashData { - kind: String, - message: String, -} - -#[get("/items")] -async fn list_items( - req: HttpRequest, - data: web::Data, -) -> actix_web::Result { - let conn = &data.conn; - - // get params - let params = web::Query::::from_query(req.query_string())?; - - let page = params.page.unwrap_or(1); - let items_per_page = params.items_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); - - let (items, num_pages) = Query::find_items_in_page(conn, page, items_per_page) - .await - .expect("Cannot find items in page"); - - Ok(HttpResponse::Ok().json(items)) -} - -#[post("/items")] -async fn create_item( - data: web::Data, - item_data: web::Json, -) -> actix_web::Result { - let conn = &data.conn; - - let item = Mutation::create_item(conn, item_data.into_inner()) - .await - .expect("could not insert item"); - - Ok(HttpResponse::Created().json(item)) -} - -#[get("/items/{id}")] -async fn get_item( - data: web::Data, - id: web::Path, -) -> actix_web::Result { - let conn = &data.conn; - - let id = id.into_inner(); - - let item: Option = Query::find_item_by_id(conn, id) - .await - .expect("could not find item"); - - Ok(HttpResponse::Ok().json(item)) -} - -#[put("/{id}")] -async fn update_item( - data: web::Data, - id: web::Path, - item_form: web::Json, -) -> actix_web::Result { - let conn = &data.conn; - let form = item_form.into_inner(); - let id = id.into_inner(); - - let item = Mutation::update_item_by_id(conn, id, form) - .await - .expect("could not edit item"); - - Ok(HttpResponse::Accepted().json(item)) -} - -#[delete("/items/{id}")] -async fn delete_item( - data: web::Data, - id: web::Path, -) -> actix_web::Result { - let conn = &data.conn; - let id = id.into_inner(); - - Mutation::delete_item(conn, id) - .await - .expect("could not delete item"); - - Ok(HttpResponse::NoContent()) -} - #[actix_web::main] async fn start() -> std::io::Result<()> { std::env::set_var("RUST_LOG", "debug"); tracing_subscriber::fmt::init(); - - // get env vars + dotenvy::dotenv().ok(); let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); let host = env::var("HOST").expect("HOST is not set in .env file"); let port = env::var("PORT").expect("PORT is not set in .env file"); let server_url = format!("{host}:{port}"); - - // establish connection to database and apply migrations - // -> create table `item` if it not exists + let conn = Database::connect(&db_url).await.unwrap(); Migrator::up(&conn, None).await.unwrap(); let state = AppState { conn }; //, templates }; - - // create server and try to serve over socket if possible + let mut listenfd = ListenFd::from_env(); let mut server = HttpServer::new(move || { App::new() @@ -155,11 +54,11 @@ async fn start() -> std::io::Result<()> { } fn init(cfg: &mut web::ServiceConfig) { - cfg.service(list_items); - cfg.service(create_item); - cfg.service(get_item); - cfg.service(update_item); - cfg.service(delete_item); + cfg.service(api::list_items); + cfg.service(api::create_item); + cfg.service(api::get_item); + cfg.service(api::update_item); + cfg.service(api::delete_item); } pub fn main() { From a6ba4dd6960437eece2a624fc8103614fd251550 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Tue, 11 Feb 2025 19:22:30 +0300 Subject: [PATCH 11/12] refactor(structure): added new api module for sqlx example --- sqlx/src/{db.rs => actions.rs} | 0 sqlx/src/api.rs | 10 +++++----- sqlx/src/main.rs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename sqlx/src/{db.rs => actions.rs} (100%) diff --git a/sqlx/src/db.rs b/sqlx/src/actions.rs similarity index 100% rename from sqlx/src/db.rs rename to sqlx/src/actions.rs diff --git a/sqlx/src/api.rs b/sqlx/src/api.rs index 473bd6f..c3abbbe 100644 --- a/sqlx/src/api.rs +++ b/sqlx/src/api.rs @@ -7,7 +7,7 @@ use sqlx::PgPool; use uuid::Uuid; use crate::{ - db, + actions, models }; use crate::models::UpdateItem; @@ -15,7 +15,7 @@ use crate::models::UpdateItem; pub async fn get_items( pool: web::Data, ) -> Result { - let items = db::get_all_items(&pool) + let items = actions::get_all_items(&pool) .await .map_err(error::ErrorInternalServerError)?; @@ -27,7 +27,7 @@ pub async fn create_item( params: web::Json, pool: web::Data, ) -> Result { - db::create_item(params.into_inner(), &pool) + actions::create_item(params.into_inner(), &pool) .await .map_err(error::ErrorInternalServerError)?; @@ -50,7 +50,7 @@ pub async fn update_item( id: web::Path, data: web::Json, ) -> Result<&'static str, Error> { - db::update_item(id.into_inner(), data.into_inner(), &pool) + actions::update_item(id.into_inner(), data.into_inner(), &pool) .await .map_err(error::ErrorInternalServerError)?; @@ -61,7 +61,7 @@ pub async fn delete_item( pool: web::Data, params: web::Path, ) -> Result<&'static str, Error> { - db::delete_item(params.id, &pool) + actions::delete_item(params.id, &pool) .await .map_err(error::ErrorInternalServerError)?; diff --git a/sqlx/src/main.rs b/sqlx/src/main.rs index e4d786d..8721703 100644 --- a/sqlx/src/main.rs +++ b/sqlx/src/main.rs @@ -8,7 +8,7 @@ use actix_web::{ use dotenvy::dotenv; mod api; -mod db; +mod actions; mod models; @@ -18,7 +18,7 @@ async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - let pool = db::init_pool(&database_url) + let pool = actions::init_pool(&database_url) .await .expect("Failed to create pool"); From 6be2d386b9b5af6af9479571f0a4baa6a81b81d1 Mon Sep 17 00:00:00 2001 From: Alex Ted Date: Thu, 13 Feb 2025 17:42:05 +0300 Subject: [PATCH 12/12] fix(sea-orm): fixed sea-orm app --- diesel_async/README.md | 10 +-- .../up.sql | 2 +- diesel_async/src/actions.rs | 4 +- diesel_async/src/api.rs | 6 +- diesel_async/src/main.rs | 14 ++-- diesel_async/src/models.rs | 12 ++-- diesel_async/src/schema.rs | 2 +- seaorm/README.md | 10 +-- seaorm/migration/Cargo.toml | 4 +- .../src/m20220120_000001_create_item_table.rs | 4 +- seaorm/src/actions.rs | 23 +++---- seaorm/src/api.rs | 64 ++++++++++--------- seaorm/src/main.rs | 14 ++-- seaorm/src/models.rs | 4 +- seaorm/src/tests/mock.rs | 8 +-- seaorm/src/tests/prepare.rs | 12 ++-- sqlx/src/actions.rs | 7 +- sqlx/src/api.rs | 23 ++----- sqlx/src/main.rs | 3 +- sqlx/src/models.rs | 17 ++--- 20 files changed, 117 insertions(+), 126 deletions(-) diff --git a/diesel_async/README.md b/diesel_async/README.md index 21ab6b1..a6ac2de 100644 --- a/diesel_async/README.md +++ b/diesel_async/README.md @@ -42,10 +42,10 @@ cargo run Inserts a new item into the PostgreSQL DB. -Provide a JSON payload with a name. Eg: +Provide a JSON payload with a title. Eg: ```json -{ "name": "thingamajig" } +{ "title": "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": "thingamajig" + "title": "thingamajig" } ``` @@ -63,13 +63,13 @@ On success, a response like the following is returned: Using [HTTPie]: ```sh -http POST localhost:8080/items name=thingamajig +http POST localhost:8080/items title=thingamajig ``` Using cURL: ```sh -curl -S -X POST --header "Content-Type: application/json" --data '{"name":"thingamajig"}' http://localhost:8080/items +curl -S -X POST --header "Content-Type: application/json" --data '{"title":"thingamajig"}' http://localhost:8080/items ``` diff --git a/diesel_async/migrations/2025-01-18-144029_create_table_items/up.sql b/diesel_async/migrations/2025-01-18-144029_create_table_items/up.sql index 719260a..062d72a 100644 --- a/diesel_async/migrations/2025-01-18-144029_create_table_items/up.sql +++ b/diesel_async/migrations/2025-01-18-144029_create_table_items/up.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS items ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, - name VARCHAR NOT NULL + title VARCHAR NOT NULL ); diff --git a/diesel_async/src/actions.rs b/diesel_async/src/actions.rs index 2bbb2c9..4488fb5 100644 --- a/diesel_async/src/actions.rs +++ b/diesel_async/src/actions.rs @@ -29,7 +29,7 @@ pub async fn find_item_by_uid( /// 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 + nm: &str, // prevent collision with `title` 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) @@ -38,7 +38,7 @@ pub async fn insert_new_item( let new_item = models::Item { id: Uuid::new_v7(Timestamp::now(NoContext)), - name: nm.to_owned(), + title: nm.to_owned(), }; let item = diesel::insert_into(items) diff --git a/diesel_async/src/api.rs b/diesel_async/src/api.rs index 2b8169f..4274d6a 100644 --- a/diesel_async/src/api.rs +++ b/diesel_async/src/api.rs @@ -1,6 +1,6 @@ +use crate::{actions, models, DbPool}; use actix_web::{error, get, post, web, HttpResponse, Responder}; use uuid::Uuid; -use crate::{actions, models, DbPool}; /// Finds item by UID. /// @@ -48,11 +48,11 @@ async fn add_item( .await .expect("Couldn't get db connection from the pool"); - let item = actions::insert_new_item(&mut conn, &form.name) + let item = actions::insert_new_item(&mut conn, &form.title) .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)) -} \ No newline at end of file +} diff --git a/diesel_async/src/main.rs b/diesel_async/src/main.rs index 71d3bdf..e0c9390 100644 --- a/diesel_async/src/main.rs +++ b/diesel_async/src/main.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate diesel; -use actix_web::{ middleware, web, App, HttpServer, }; +use actix_web::{middleware, web, App, HttpServer}; use diesel_async::{ pooled_connection::{bb8::Pool, AsyncDieselConnectionManager}, AsyncPgConnection, @@ -9,11 +9,10 @@ use diesel_async::{ use dotenvy::dotenv; use std::{env, io}; - mod actions; +mod api; mod models; mod schema; -mod api; type DbPool = Pool; @@ -37,7 +36,7 @@ async fn main() -> io::Result<()> { .service(api::add_item) .service(api::get_item) }) - .bind(("127.0.0.1", 8080))? + .bind(("127.0.0.1", 5000))? .run() .await } @@ -52,14 +51,13 @@ async fn initialize_db_pool() -> DbPool { Pool::builder().build(connection_manager).await.unwrap() } - #[cfg(test)] mod tests { + use super::*; use actix_web::{http::StatusCode, test}; use diesel::prelude::*; use diesel_async::RunQueryDsl; use uuid::Uuid; - use super::*; #[actix_web::test] async fn item_routes() { @@ -105,14 +103,14 @@ mod tests { .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"); + assert_eq!(res.title, "Test item"); // get an item let req = test::TestRequest::get() .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"); + assert_eq!(res.title, "Test item"); // delete new item from table use crate::schema::items::dsl::*; diff --git a/diesel_async/src/models.rs b/diesel_async/src/models.rs index 6d08565..905112e 100644 --- a/diesel_async/src/models.rs +++ b/diesel_async/src/models.rs @@ -7,19 +7,21 @@ use uuid::Uuid; #[diesel(table_name = items)] pub struct Item { pub id: Uuid, - pub name: String, + pub title: String, } /// New item details. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct NewItem { - pub name: String, + pub title: String, } impl NewItem { - /// Constructs new item details from name. + /// Constructs new item details from title. #[cfg(test)] // only needed in tests - pub fn new(name: impl Into) -> Self { - Self { name: name.into() } + pub fn new(title: impl Into) -> Self { + Self { + title: title.into(), + } } } diff --git a/diesel_async/src/schema.rs b/diesel_async/src/schema.rs index a9038e3..2023fd9 100644 --- a/diesel_async/src/schema.rs +++ b/diesel_async/src/schema.rs @@ -3,6 +3,6 @@ diesel::table! { items (id) { id -> Uuid, - name -> Varchar, + title -> Varchar, } } diff --git a/seaorm/README.md b/seaorm/README.md index 69b5ecb..75b883d 100644 --- a/seaorm/README.md +++ b/seaorm/README.md @@ -47,10 +47,10 @@ cargo run Inserts a new item into the PostgreSQL DB. -Provide a JSON payload with a name with/out description. Eg: +Provide a JSON payload with a title with/out description. Eg: ```json -{ "name": "thingamajig", "description": "This is awesome thing!" } +{ "title": "thingamajig", "description": "This is awesome thing!" } ``` On success, a response like the following is returned: @@ -58,7 +58,7 @@ On success, a response like the following is returned: ```json { "id": "01948982-67d0-7a55-b4b1-8b8b962d8c6b", - "name": "thingamajig", + "title": "thingamajig", "description": "This is awesome thing!" } ``` @@ -69,13 +69,13 @@ On success, a response like the following is returned: Using [HTTPie]: ```sh -http POST localhost:8080/items name=thingamajig +http POST localhost:8080/items title=thingamajig ``` Using cURL: ```sh -curl -S -X POST --header "Content-Type: application/json" --data '{"name":"thingamajig"}' http://localhost:8080/items +curl -S -X POST --header "Content-Type: application/json" --data '{"title":"thingamajig"}' http://localhost:8080/items ``` diff --git a/seaorm/migration/Cargo.toml b/seaorm/migration/Cargo.toml index 9c48ad9..21cd3f5 100644 --- a/seaorm/migration/Cargo.toml +++ b/seaorm/migration/Cargo.toml @@ -16,6 +16,6 @@ async-std = { version = "1.13", features = ["attributes", "tokio1"] } version = "~1.1.4" # sea-orm-migration version features = [ # Enable following runtime and db backend features if you want to run migration via CLI - # "runtime-actix-native-tls", - # "sqlx-mysql", + "runtime-tokio", + "sqlx-postgres", ] diff --git a/seaorm/migration/src/m20220120_000001_create_item_table.rs b/seaorm/migration/src/m20220120_000001_create_item_table.rs index cf5bfe1..556cb58 100644 --- a/seaorm/migration/src/m20220120_000001_create_item_table.rs +++ b/seaorm/migration/src/m20220120_000001_create_item_table.rs @@ -11,9 +11,9 @@ impl MigrationTrait for Migration { Table::create() .table(Items::Table) .if_not_exists() - .col(pk_uuid(Items::Id)) + .col(pk_uuid(Items::Id).default(Expr::cust("gen_random_uuid()"))) .col(string(Items::Title)) - .col(string(Items::Description)) + .col(string_null(Items::Description).null()) .to_owned(), ) .await diff --git a/seaorm/src/actions.rs b/seaorm/src/actions.rs index ce2aad2..4ac62bd 100644 --- a/seaorm/src/actions.rs +++ b/seaorm/src/actions.rs @@ -1,3 +1,4 @@ +use crate::api::UpdateItemModel; use crate::models::{ActiveModel, Column, Entity as Item, Model}; use sea_orm::prelude::Uuid; use sea_orm::*; @@ -5,21 +6,17 @@ use sea_orm::*; pub struct Mutation; impl Mutation { - pub async fn create_item(db: &DbConn, form_data: Model) -> Result { + pub async fn create_item(db: &DbConn, data: Model) -> Result { ActiveModel { - name: Set(form_data.name.to_owned()), - description: Set(form_data.description.to_owned()), + title: Set(data.title.to_owned()), + description: Set(data.description.to_owned()), ..Default::default() } .insert(db) .await } - pub async fn update_item_by_id( - db: &DbConn, - id: Uuid, - form_data: Model, - ) -> Result { + pub async fn update_item(db: &DbConn, id: Uuid, data: UpdateItemModel) -> Result { let item: ActiveModel = Item::find_by_id(id) .one(db) .await? @@ -28,8 +25,8 @@ impl Mutation { ActiveModel { id: item.id, - name: Set(form_data.name.to_owned()), - description: Set(form_data.description.to_owned()), + title: data.title.map(Set).unwrap_or(item.title), + description: data.description.into_active_value(), } .update(db) .await @@ -44,10 +41,6 @@ impl Mutation { item.delete(db).await } - - pub async fn delete_all_items(db: &DbConn) -> Result { - Item::delete_many().exec(db).await - } } pub struct Query; @@ -67,7 +60,7 @@ impl Query { .order_by_asc(Column::Id) .paginate(db, items_per_page); let num_pages = paginator.num_pages().await?; - + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) } } diff --git a/seaorm/src/api.rs b/seaorm/src/api.rs index c011819..edf9dae 100644 --- a/seaorm/src/api.rs +++ b/seaorm/src/api.rs @@ -1,9 +1,9 @@ -use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse, Responder}; -use sea_orm::prelude::Uuid; -use serde::{Deserialize, Serialize}; -use crate::{AppState, DEFAULT_POSTS_PER_PAGE}; use crate::actions::{Mutation, Query}; use crate::models::Model; +use crate::{AppState, DEFAULT_POSTS_PER_PAGE}; +use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse, Responder}; +use sea_orm::prelude::Uuid; +use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize)] pub struct Params { @@ -17,26 +17,6 @@ struct FlashData { message: String, } -#[get("/items")] -async fn list_items( - req: HttpRequest, - data: web::Data, -) -> actix_web::Result { - let conn = &data.conn; - - // get params - let params = web::Query::::from_query(req.query_string())?; - - let page = params.page.unwrap_or(1); - let items_per_page = params.items_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); - - let (items, num_pages) = Query::find_items_in_page(conn, page, items_per_page) - .await - .expect("Cannot find items in page"); - - Ok(HttpResponse::Ok().json(items)) -} - #[post("/items")] async fn create_item( data: web::Data, @@ -67,17 +47,43 @@ async fn get_item( Ok(HttpResponse::Ok().json(item)) } -#[put("/{id}")] +#[get("/items")] +async fn list_items( + req: HttpRequest, + data: web::Data, +) -> actix_web::Result { + let conn = &data.conn; + + // get params + let params = web::Query::::from_query(req.query_string())?; + + let page = params.page.unwrap_or(1); + let items_per_page = params.items_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + + let (items, num_pages) = Query::find_items_in_page(conn, page, items_per_page) + .await + .expect("Cannot find items in page"); + + Ok(HttpResponse::Ok().json(items)) +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct UpdateItemModel { + pub title: Option, + pub description: Option, +} + +#[patch("/items/{id}")] async fn update_item( data: web::Data, id: web::Path, - item_form: web::Json, + item_data: web::Json, ) -> actix_web::Result { let conn = &data.conn; - let form = item_form.into_inner(); + // let form = item_data.into_inner(); let id = id.into_inner(); - let item = Mutation::update_item_by_id(conn, id, form) + let item = Mutation::update_item(conn, id, item_data.into_inner()) .await .expect("could not edit item"); @@ -97,4 +103,4 @@ async fn delete_item( .expect("could not delete item"); Ok(HttpResponse::NoContent()) -} \ No newline at end of file +} diff --git a/seaorm/src/main.rs b/seaorm/src/main.rs index 6d7262e..fc8892a 100644 --- a/seaorm/src/main.rs +++ b/seaorm/src/main.rs @@ -1,10 +1,8 @@ mod actions; -mod models; mod api; +mod models; -use actix_web::{ - middleware, web, App, HttpServer, -}; +use actix_web::{middleware, web, App, HttpServer}; use listenfd::ListenFd; use migration::{Migrator, MigratorTrait}; @@ -22,18 +20,18 @@ struct AppState { async fn start() -> std::io::Result<()> { std::env::set_var("RUST_LOG", "debug"); tracing_subscriber::fmt::init(); - + dotenvy::dotenv().ok(); let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); let host = env::var("HOST").expect("HOST is not set in .env file"); let port = env::var("PORT").expect("PORT is not set in .env file"); let server_url = format!("{host}:{port}"); - + let conn = Database::connect(&db_url).await.unwrap(); Migrator::up(&conn, None).await.unwrap(); let state = AppState { conn }; //, templates }; - + let mut listenfd = ListenFd::from_env(); let mut server = HttpServer::new(move || { App::new() @@ -54,8 +52,8 @@ async fn start() -> std::io::Result<()> { } fn init(cfg: &mut web::ServiceConfig) { - cfg.service(api::list_items); cfg.service(api::create_item); + cfg.service(api::list_items); cfg.service(api::get_item); cfg.service(api::update_item); cfg.service(api::delete_item); diff --git a/seaorm/src/models.rs b/seaorm/src/models.rs index 80e223c..51f4af7 100644 --- a/seaorm/src/models.rs +++ b/seaorm/src/models.rs @@ -7,9 +7,9 @@ pub struct Model { #[sea_orm(primary_key)] #[serde(skip_deserializing)] pub id: Uuid, - pub name: String, + pub title: String, #[sea_orm(column_type = "Text")] - pub description: String, + pub description: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/seaorm/src/tests/mock.rs b/seaorm/src/tests/mock.rs index 9da3da2..45cdba3 100644 --- a/seaorm/src/tests/mock.rs +++ b/seaorm/src/tests/mock.rs @@ -25,7 +25,7 @@ async fn main() { db, item::Model { id: 0, - name: "Title D".to_owned(), + title: "Title D".to_owned(), description: "Description D".to_owned(), }, ) @@ -36,7 +36,7 @@ async fn main() { item, item::ActiveModel { id: sea_orm::ActiveValue::Unchanged(6), - name: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), description: sea_orm::ActiveValue::Unchanged("Description D".to_owned()) } ); @@ -48,7 +48,7 @@ async fn main() { 1, item::Model { id: 1, - name: "New Title A".to_owned(), + title: "New Title A".to_owned(), description: "New Description A".to_owned(), }, ) @@ -59,7 +59,7 @@ async fn main() { item, item::Model { id: 1, - name: "New Title A".to_owned(), + title: "New Title A".to_owned(), description: "New Description A".to_owned(), } ); diff --git a/seaorm/src/tests/prepare.rs b/seaorm/src/tests/prepare.rs index 5906e81..2cff786 100644 --- a/seaorm/src/tests/prepare.rs +++ b/seaorm/src/tests/prepare.rs @@ -7,32 +7,32 @@ pub fn prepare_mock_db() -> DatabaseConnection { .append_query_results([ [item::Model { id: 1, - name: "Title A".to_owned(), + title: "Title A".to_owned(), description: "Description A".to_owned(), }], [item::Model { id: 5, - name: "Title C".to_owned(), + title: "Title C".to_owned(), description: "Description C".to_owned(), }], [item::Model { id: 6, - name: "Title D".to_owned(), + title: "Title D".to_owned(), description: "Description D".to_owned(), }], [item::Model { id: 1, - name: "Title A".to_owned(), + title: "Title A".to_owned(), description: "Description A".to_owned(), }], [item::Model { id: 1, - name: "New Title A".to_owned(), + title: "New Title A".to_owned(), description: "New Description A".to_owned(), }], [item::Model { id: 5, - name: "Title C".to_owned(), + title: "Title C".to_owned(), description: "Description C".to_owned(), }], ]) diff --git a/sqlx/src/actions.rs b/sqlx/src/actions.rs index 042c6f7..c42e1e1 100644 --- a/sqlx/src/actions.rs +++ b/sqlx/src/actions.rs @@ -1,6 +1,6 @@ +use crate::models::{Item, NewItem, UpdateItem}; use sqlx::postgres::{PgPool, PgPoolOptions}; use sqlx::types::Uuid; -use crate::models::{NewItem, Item, UpdateItem}; pub async fn init_pool(database_url: &str) -> Result { PgPoolOptions::new() @@ -14,7 +14,10 @@ pub async fn get_all_items(pool: &PgPool) -> Result, &'static str> { } pub async fn create_item(item_data: NewItem, pool: &PgPool) -> Result<(), &'static str> { - let new_item = NewItem { title: item_data.title, description: item_data.description }; + let new_item = NewItem { + title: item_data.title, + description: item_data.description, + }; Item::create(new_item, pool) .await .map(|_| ()) diff --git a/sqlx/src/api.rs b/sqlx/src/api.rs index c3abbbe..24383de 100644 --- a/sqlx/src/api.rs +++ b/sqlx/src/api.rs @@ -1,20 +1,12 @@ -use actix_web::{ - error, http::StatusCode, web, Error, HttpResponse, - Responder, Result, -}; +use actix_web::{error, http::StatusCode, web, Error, HttpResponse, Responder, Result}; use serde::Deserialize; use sqlx::PgPool; use uuid::Uuid; -use crate::{ - actions, - models -}; use crate::models::UpdateItem; +use crate::{actions, models}; -pub async fn get_items( - pool: web::Data, -) -> Result { +pub async fn get_items(pool: web::Data) -> Result { let items = actions::get_all_items(&pool) .await .map_err(error::ErrorInternalServerError)?; @@ -22,7 +14,6 @@ pub async fn get_items( Ok(HttpResponse::Ok().json(items)) } - pub async fn create_item( params: web::Json, pool: web::Data, @@ -75,10 +66,10 @@ pub async fn delete_item( // .respond_to(res.request()) // .map_into_boxed_body() // .map_into_right_body(); -// +// // Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) // } -// +// // pub fn not_found(res: dev::ServiceResponse) -> Result> { // let new_resp = NamedFile::open("static/errors/404.html")? // .customize() @@ -86,7 +77,7 @@ pub async fn delete_item( // .respond_to(res.request()) // .map_into_boxed_body() // .map_into_right_body(); -// +// // Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) // } @@ -97,6 +88,6 @@ pub async fn delete_item( // .respond_to(res.request()) // .map_into_boxed_body() // .map_into_right_body(); -// +// // Ok(ErrorHandlerResponse::Response(res.into_response(new_resp))) // } diff --git a/sqlx/src/main.rs b/sqlx/src/main.rs index 8721703..28b7931 100644 --- a/sqlx/src/main.rs +++ b/sqlx/src/main.rs @@ -7,11 +7,10 @@ use actix_web::{ }; use dotenvy::dotenv; -mod api; mod actions; +mod api; mod models; - #[actix_web::main] async fn main() -> io::Result<()> { dotenv().ok(); diff --git a/sqlx/src/models.rs b/sqlx/src/models.rs index 511007a..5802827 100644 --- a/sqlx/src/models.rs +++ b/sqlx/src/models.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use sqlx::PgPool; use sqlx::types::Uuid; +use sqlx::PgPool; +use std::collections::HashMap; #[derive(Debug, Deserialize)] pub struct NewItem { @@ -67,7 +67,7 @@ impl Item { // query_builder = query_builder.bind(id); query_builder.execute(connection).await?; - + // sqlx::query!( // r#" // INSERT INTO items (title, description) @@ -82,7 +82,11 @@ impl Item { Ok(()) } - pub async fn update(id: Uuid, item_data: UpdateItem, connection: &PgPool) -> Result<(), sqlx::Error> { + pub async fn update( + id: Uuid, + item_data: UpdateItem, + connection: &PgPool, + ) -> Result<(), sqlx::Error> { let mut updates = Vec::new(); let mut params = HashMap::new(); @@ -99,10 +103,7 @@ impl Item { return Ok(()); // Нет данных для обновления } - let query = format!( - "UPDATE items SET {} WHERE id = $3", - updates.join(", ") - ); + let query = format!("UPDATE items SET {} WHERE id = $3", updates.join(", ")); let mut query_builder = sqlx::query(&query); for (i, value) in params {