remove unbounded get users api endpoint

This commit is contained in:
Rohan Sircar 2021-05-20 18:39:20 +05:30
parent c993ef87ee
commit 95e5c47c69
9 changed files with 36 additions and 102 deletions

View File

@ -12,18 +12,14 @@ actix-http = "2.2.0"
bytes = "1.0.1" bytes = "1.0.1"
futures = "0.3.14" futures = "0.3.14"
serde_json = "1.0.64" serde_json = "1.0.64"
# json = "0.12.4"
# listenfd = "0.3.3"
dotenv = "0.15.0" dotenv = "0.15.0"
r2d2 = "0.8.9" r2d2 = "0.8.9"
# jsonwebtoken = "7.2.0"
actix-identity = "0.3.1" actix-identity = "0.3.1"
actix-web-httpauth = "0.5.1" actix-web-httpauth = "0.5.1"
rand = "0.8.3" rand = "0.8.3"
nanoid = "0.4.0" nanoid = "0.4.0"
bcrypt = "0.9.0" bcrypt = "0.9.0"
timeago = "0.3.0" timeago = "0.3.0"
# comp = "0.2.1"
regex = "1.4.5" regex = "1.4.5"
lazy_static = "1.4.0" lazy_static = "1.4.0"
lazy-regex = "0.1.4" lazy-regex = "0.1.4"

View File

@ -40,7 +40,7 @@ curl -X GET http://localhost:7800/api/users
``` ```
curl -H "content-type: application/json" \ curl -H "content-type: application/json" \
-X POST \ -X PUT \
-i http://localhost:7800/api/users \ -i http://localhost:7800/api/users \
--data '{"name":"user4","password":"test"}' --data '{"name":"user4","password":"test"}'
``` ```
@ -66,19 +66,6 @@ curl -H "content-type: application/json" \
] ]
``` ```
### DTO Validation
```
curl -H "content-type: application/json" \
-X POST \
-i http://localhost:7800/api/users \
--data '{"name":"abc","password":"test"}' # min length for name is 4
```
```
ValidationErrors({"name": Field([ValidationError { code: "length", message: None, params: {"value": String("abc"), "min": Number(4), "max": Number(10)} }])})
```
## Memory Usage ## Memory Usage
Memory usage as compared to interpreted languages was my primary motivation for looking into rust as a backend language. As of writing, the demo app uses less than 50MB of memory. Memory usage as compared to interpreted languages was my primary motivation for looking into rust as a backend language. As of writing, the demo app uses less than 50MB of memory.

View File

@ -1,6 +1,6 @@
use diesel::prelude::*; use diesel::prelude::*;
use crate::models::{self, Pagination, UserId}; use crate::models::{self, Pagination, UserId, Username};
use crate::{errors, models::Password}; use crate::{errors, models::Password};
use bcrypt::{hash, verify, DEFAULT_COST}; use bcrypt::{hash, verify, DEFAULT_COST};
use validators::prelude::*; use validators::prelude::*;
@ -21,7 +21,7 @@ pub fn find_user_by_uid(
} }
pub fn _find_user_by_name( pub fn _find_user_by_name(
user_name: String, user_name: Username,
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>, conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
) -> Result<Option<models::User>, errors::DomainError> { ) -> Result<Option<models::User>, errors::DomainError> {
use crate::schema::users::dsl::*; use crate::schema::users::dsl::*;
@ -33,15 +33,6 @@ pub fn _find_user_by_name(
Ok(maybe_user?) Ok(maybe_user?)
} }
pub fn get_all(
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
) -> Result<Vec<models::User>, errors::DomainError> {
use crate::schema::users::dsl::*;
Ok(users
.select(users::all_columns())
.load::<models::User>(conn)?)
}
// def findAll(userId: Long, limit: Int, offset: Int) = db.run { // def findAll(userId: Long, limit: Int, offset: Int) = db.run {
// for { // for {
// comments <- query.filter(_.creatorId === userId) // comments <- query.filter(_.creatorId === userId)
@ -56,12 +47,10 @@ pub fn get_all(
// ) // )
// } // }
pub fn get_users_paginated( pub fn get_all_users(
// user_id: UserId,
pagination: &Pagination, pagination: &Pagination,
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>, conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
) -> Result<Vec<models::User>, errors::DomainError> { ) -> Result<Vec<models::User>, errors::DomainError> {
// use crate::schema::users::dsl::*;
Ok(query::_paginate_result(&pagination).load::<models::User>(conn)?) Ok(query::_paginate_result(&pagination).load::<models::User>(conn)?)
} }
@ -71,12 +60,6 @@ pub fn search_users(
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>, conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
) -> Result<Vec<models::User>, errors::DomainError> { ) -> Result<Vec<models::User>, errors::DomainError> {
use crate::schema::users::dsl::*; use crate::schema::users::dsl::*;
// Ok(users
// .filter(name.like(format!("%{}%", query)))
// .order_by(created_at)
// .offset(pagination.calc_offset().as_uint().into())
// .limit(pagination.limit.as_uint().into())
// .load::<models::User>(conn)?)
Ok(query::_paginate_result(&pagination) Ok(query::_paginate_result(&pagination)
.filter(name.like(format!("%{}%", query))) .filter(name.like(format!("%{}%", query)))
.load::<models::User>(conn)?) .load::<models::User>(conn)?)

View File

@ -69,24 +69,17 @@ pub fn configure_app(app_data: AppData) -> Box<dyn Fn(&mut ServiceConfig)> {
web::scope("/api") web::scope("/api")
.service( .service(
web::scope("/users") web::scope("/users")
.route( .route("", web::get().to(routes::users::get_users))
"",
web::get().to(routes::users::get_all_users),
)
.route( .route(
"/search", "/search",
web::get().to(routes::users::search_users), web::get().to(routes::users::search_users),
) )
.route("", web::post().to(routes::users::add_user)) .route("", web::put().to(routes::users::add_user))
.route( .route(
"/{user_id}", "/{user_id}",
web::get().to(routes::users::get_user), web::get().to(routes::users::get_user),
), ),
) )
.route(
"/pagination",
web::get().to(routes::users::get_users_paginated),
)
.route( .route(
"/build-info", "/build-info",
web::get().to(routes::misc::build_info_req), web::get().to(routes::misc::build_info_req),

View File

@ -5,6 +5,7 @@ use diesel_tracing::sqlite::InstrumentedSqliteConnection;
use io::ErrorKind; use io::ErrorKind;
use std::io; use std::io;
use tracing::subscriber::set_global_default; use tracing::subscriber::set_global_default;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_log::LogTracer; use tracing_log::LogTracer;
use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::fmt::format::FmtSpan;
@ -30,7 +31,8 @@ async fn main() -> io::Result<()> {
) )
})?; })?;
let _ = setup_logger(env_config.logger_format)?; //bind guard to variable instead of _
let _guard = setup_logger(env_config.logger_format)?;
let connspec = &env_config.database_url; let connspec = &env_config.database_url;
let manager = let manager =
@ -69,7 +71,7 @@ async fn main() -> io::Result<()> {
actix_demo::run(format!("{}:7800", env_config.http_host), app_data).await actix_demo::run(format!("{}:7800", env_config.http_host), app_data).await
} }
pub fn setup_logger(format: LoggerFormat) -> io::Result<()> { pub fn setup_logger(format: LoggerFormat) -> io::Result<WorkerGuard> {
let env_filter = let env_filter =
EnvFilter::try_from_env("ACTIX_DEMO_RUST_LOG").map_err(|err| { EnvFilter::try_from_env("ACTIX_DEMO_RUST_LOG").map_err(|err| {
io::Error::new( io::Error::new(
@ -115,6 +117,8 @@ pub fn setup_logger(format: LoggerFormat) -> io::Result<()> {
.with_span_events(FmtSpan::NEW) .with_span_events(FmtSpan::NEW)
.with_span_events(FmtSpan::CLOSE) .with_span_events(FmtSpan::CLOSE)
.with_env_filter(env_filter) .with_env_filter(env_filter)
.with_writer(non_blocking)
.with_thread_names(true)
.finish(); .finish();
let _ = set_global_default(subscriber).map_err(|err| { let _ = set_global_default(subscriber).map_err(|err| {
io::Error::new( io::Error::new(
@ -124,5 +128,5 @@ pub fn setup_logger(format: LoggerFormat) -> io::Result<()> {
})?; })?;
} }
}; };
Ok(()) Ok(_guard)
} }

View File

@ -167,17 +167,17 @@ impl Pagination {
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct SearchQuery(String); pub struct SearchQueryString(String);
impl SearchQuery { impl SearchQueryString {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
&self.0 &self.0
} }
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct UserSearchRequest { pub struct SearchQuery {
pub q: SearchQuery, pub q: SearchQueryString,
// pub pagination: Pagination // pub pagination: Pagination
} }

View File

@ -2,10 +2,9 @@ use actix_web::{web, HttpResponse};
use crate::{ use crate::{
actions, actions,
models::{self, ApiResponse, Pagination, UserId, UserSearchRequest}, models::{self, ApiResponse, Pagination, SearchQuery, UserId},
}; };
use crate::{errors::DomainError, AppData}; use crate::{errors::DomainError, AppData};
use actix_web::error::ResponseError;
/// Finds user by UID. /// Finds user by UID.
#[tracing::instrument( #[tracing::instrument(
@ -60,33 +59,8 @@ pub async fn get_user(
// } // }
// } // }
///List all users
#[tracing::instrument(level = "debug", skip(app_data))] #[tracing::instrument(level = "debug", skip(app_data))]
pub async fn get_all_users( pub async fn get_users(
app_data: web::Data<AppData>,
) -> Result<HttpResponse, DomainError> {
// use web::block to offload blocking Diesel code without blocking server thread
let users = web::block(move || {
let pool = &app_data.pool;
let conn = pool.get()?;
actions::get_all(&conn)
})
.await
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?;
let _ = tracing::trace!("{:?}", users);
if !users.is_empty() {
Ok(HttpResponse::Ok().json(ApiResponse::successful(users)))
} else {
Err(DomainError::new_entity_does_not_exist_error(
"No users available".to_owned(),
))
}
}
#[tracing::instrument(level = "debug", skip(app_data))]
pub async fn get_users_paginated(
app_data: web::Data<AppData>, app_data: web::Data<AppData>,
pagination: web::Query<Pagination>, pagination: web::Query<Pagination>,
) -> Result<HttpResponse, DomainError> { ) -> Result<HttpResponse, DomainError> {
@ -95,7 +69,7 @@ pub async fn get_users_paginated(
let pool = &app_data.pool; let pool = &app_data.pool;
let conn = pool.get()?; let conn = pool.get()?;
let p: Pagination = pagination.into_inner(); let p: Pagination = pagination.into_inner();
actions::get_users_paginated(&p, &conn) actions::get_all_users(&p, &conn)
}) })
.await .await
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?; .map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?;
@ -108,7 +82,7 @@ pub async fn get_users_paginated(
#[tracing::instrument(level = "debug", skip(app_data))] #[tracing::instrument(level = "debug", skip(app_data))]
pub async fn search_users( pub async fn search_users(
app_data: web::Data<AppData>, app_data: web::Data<AppData>,
query: web::Query<UserSearchRequest>, query: web::Query<SearchQuery>,
pagination: web::Query<Pagination>, pagination: web::Query<Pagination>,
) -> Result<HttpResponse, DomainError> { ) -> Result<HttpResponse, DomainError> {
let _ = tracing::info!("Search users request"); let _ = tracing::info!("Search users request");
@ -126,21 +100,21 @@ pub async fn search_users(
Ok(HttpResponse::Ok().json(ApiResponse::successful(users))) Ok(HttpResponse::Ok().json(ApiResponse::successful(users)))
} }
/// Inserts new user with name defined in form. /// Inserts a new user
#[tracing::instrument(level = "debug", skip(app_data))] #[tracing::instrument(level = "debug", skip(app_data))]
pub async fn add_user( pub async fn add_user(
app_data: web::Data<AppData>, app_data: web::Data<AppData>,
form: web::Json<models::NewUser>, form: web::Json<models::NewUser>,
) -> Result<HttpResponse, impl ResponseError> { ) -> Result<HttpResponse, DomainError> {
// use web::block to offload blocking Diesel code without blocking server thread let user = web::block(move || {
web::block(move || {
let pool = &app_data.pool; let pool = &app_data.pool;
let conn = pool.get()?; let conn = pool.get()?;
actions::insert_new_user(form.0, &conn, Some(app_data.config.hash_cost)) actions::insert_new_user(form.0, &conn, Some(app_data.config.hash_cost))
}) })
.await .await
.map(|user| { .map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?;
let _ = tracing::trace!("{:?}", user);
HttpResponse::Created().json(ApiResponse::successful(user)) let _ = tracing::trace!("{:?}", user);
})
Ok(HttpResponse::Created().json(ApiResponse::successful(user)))
} }

0
static/dummy.js Normal file
View File

View File

@ -12,19 +12,16 @@ mod tests {
use super::*; use super::*;
#[actix_rt::test] #[actix_rt::test]
async fn should_return_error_message_if_no_users_exist() { async fn should_return_empty_array_if_no_users_exist() {
let req = test::TestRequest::get().uri("/api/users").to_request(); let req = test::TestRequest::get()
.uri("/api/users?page=0&limit=2")
.to_request();
let resp = let resp =
common::test_app().await.unwrap().call(req).await.unwrap(); common::test_app().await.unwrap().call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND); assert_eq!(resp.status(), StatusCode::OK);
let body: ApiResponse<String> = test::read_body_json(resp).await; let body: ApiResponse<Vec<_>> = test::read_body_json(resp).await;
let _ = tracing::debug!("{:?}", body); let _ = tracing::debug!("{:?}", body);
assert_eq!( assert_eq!(body, ApiResponse::successful(vec![1; 0]));
body,
ApiResponse::failure(
"Entity does not exist - No users available".to_owned()
)
);
} }
#[actix_rt::test] #[actix_rt::test]