remove unbounded get users api endpoint
This commit is contained in:
parent
c993ef87ee
commit
95e5c47c69
@ -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"
|
||||||
|
15
README.md
15
README.md
@ -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.
|
||||||
|
@ -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)?)
|
||||||
|
11
src/lib.rs
11
src/lib.rs
@ -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),
|
||||||
|
10
src/main.rs
10
src/main.rs
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
0
static/dummy.js
Normal 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]
|
||||||
|
Loading…
Reference in New Issue
Block a user