chore: re-map project structure to using multi workspace paradigm

This commit is contained in:
Maulana Sodiqin
2025-04-06 13:25:44 +07:00
parent de9c5a6dc1
commit b4bf78f0d6
87 changed files with 325 additions and 192 deletions
+14
View File
@@ -0,0 +1,14 @@
use surrealdb::{engine::remote::ws::Client, method::Query};
pub fn bind_filter_value(
query: Query<'_, Client>,
val: String,
) -> Query<'_, Client> {
if let Ok(b) = val.parse::<bool>() {
query.bind(("filter", b))
} else if let Ok(i) = val.parse::<i64>() {
query.bind(("filter", i))
} else {
query.bind(("filter", val))
}
}
+28
View File
@@ -0,0 +1,28 @@
use crate::decode_access_token;
use axum::http::{header::AUTHORIZATION, HeaderMap};
pub fn extract_email(headers: &HeaderMap) -> Option<String> {
println!("📥 Received headers: {:?}", headers);
let auth_header = headers.get(AUTHORIZATION)?.to_str().ok()?;
println!("🔍 Authorization Header: {}", auth_header);
let token = auth_header.strip_prefix("Bearer ")?;
println!("🧪 Token: {}", token);
match decode_access_token(token) {
Ok(data) => {
println!("✅ Token claims: {:?}", data.claims);
Some(data.claims.sub)
}
Err(e) => {
eprintln!("❌ Failed to decode token: {}", e);
None
}
}
}
pub fn extract_email_token(token: String) -> Option<String> {
let token_data = decode_access_token(&token).ok()?;
Some(token_data.claims.sub)
}
+6
View File
@@ -0,0 +1,6 @@
use chrono::{DateTime, Utc};
pub fn get_iso_date() -> String {
let now: DateTime<Utc> = Utc::now();
now.to_rfc3339()
}
+13
View File
@@ -0,0 +1,13 @@
use rand::{rng, Rng};
pub struct OtpManager;
impl OtpManager {
pub fn generate_otp() -> u32 {
rng().random_range(100_000..1_000_000)
}
pub fn validate_otp(stored_otp: u32, user_otp: u32) -> bool {
stored_otp == user_otp
}
}
+16
View File
@@ -0,0 +1,16 @@
use anyhow::{bail, Result};
use surrealdb::sql::Thing;
pub fn get_id(thing: &Thing) -> Result<(&str, &str)> {
let table = thing.tb.as_str();
let id = match &thing.id {
surrealdb::sql::Id::String(s) => s.as_str(),
_ => bail!("Unsupported ID type"),
};
Ok((table, id))
}
pub fn extract_id(thing: &Thing) -> String {
let id = thing.id.to_raw();
id
}
+26
View File
@@ -0,0 +1,26 @@
use imphnen_entities::*;
use imphnen_libs::*;
pub mod bind_filter;
pub mod extract_email;
pub mod generate_date;
pub mod generate_otp;
pub mod get_id;
pub mod make_thing;
pub mod mock_test;
pub mod query_list;
pub mod response_format;
pub mod validator;
pub use bind_filter::*;
pub use extract_email::*;
pub use generate_date::*;
pub use generate_otp::*;
pub use get_id::*;
pub use imphnen_entities::*;
pub use imphnen_libs::*;
pub use make_thing::*;
pub use mock_test::*;
pub use query_list::*;
pub use response_format::*;
pub use validator::*;
+5
View File
@@ -0,0 +1,5 @@
use surrealdb::sql::Thing;
pub fn make_thing(table: &str, id: &str) -> Thing {
Thing::from((table, id))
}
+51
View File
@@ -0,0 +1,51 @@
use crate::AppState;
use surrealdb::{
Surreal,
engine::{local::Mem, remote::ws::Ws},
opt::auth::Root,
};
pub async fn create_mock_app_state() -> AppState {
let db_mem = Surreal::new::<Mem>(()).await.unwrap();
let db_ws = Surreal::new::<Ws>("localhost:8000").await.unwrap();
db_mem.use_ns("test").use_db("test").await.unwrap();
db_ws
.signin(Root {
username: "root",
password: "root",
})
.await
.unwrap();
db_ws.use_ns("test").use_db("test").await.unwrap();
AppState {
surrealdb_mem: db_mem,
surrealdb_ws: db_ws,
}
}
pub async fn cleanup_db() {
let app_state = create_mock_app_state().await;
let _ = app_state
.surrealdb_mem
.query(
r#"
REMOVE TABLE app_users;
REMOVE TABLE app_roles;
REMOVE TABLE app_users_cache;
REMOVE TABLE app_otp_cache;
"#,
)
.await;
let _ = app_state
.surrealdb_ws
.query(
r#"
REMOVE TABLE app_users;
REMOVE TABLE app_roles;
REMOVE TABLE app_users_cache;
REMOVE TABLE app_otp_cache;
"#,
)
.await;
}
+84
View File
@@ -0,0 +1,84 @@
use super::bind_filter_value;
use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto};
use anyhow::{bail, Result};
use serde::{de::DeserializeOwned, Serialize};
use surrealdb::{engine::remote::ws::Client, Surreal};
pub async fn query_list_with_meta<T>(
db: &Surreal<Client>,
table: &str,
meta: &MetaRequestDto,
conditions: Vec<String>,
custom_select: Option<String>,
) -> Result<ResponseListSuccessDto<Vec<T>>>
where
T: DeserializeOwned + Serialize,
{
let page = meta.page.unwrap_or(1);
let per_page = meta.per_page.unwrap_or(10);
if page < 1 || per_page < 1 {
bail!("Invalid pagination: page and per_page must be greater than 0");
}
let start = (page - 1) * per_page;
let sql = custom_select.unwrap_or_else(|| {
let mut s = format!("SELECT * FROM {}", table);
if !conditions.is_empty() {
s.push_str(" WHERE ");
s.push_str(&conditions.join(" AND "));
}
if let Some(sort_by) = &meta.sort_by {
let order = match meta
.order
.clone()
.unwrap_or_default()
.to_uppercase()
.as_str()
{
"DESC" => "DESC",
_ => "ASC",
};
s.push_str(&format!(" ORDER BY {} {}", sort_by, order));
}
s.push_str(" LIMIT $per_page START $start");
s
});
let mut query_exec = db.query(sql);
if let Some(search) = &meta.search {
if !search.is_empty() {
query_exec = query_exec.bind(("search", search.clone()));
}
}
if let Some(filter_val) = meta.filter.clone() {
query_exec = bind_filter_value(query_exec, filter_val);
}
query_exec = query_exec
.bind(("per_page", per_page))
.bind(("start", start));
let raw: Vec<T> = query_exec.await?.take(0)?;
let mut count_sql = format!("SELECT count() FROM {}", table);
if !conditions.is_empty() {
count_sql.push_str(" WHERE ");
count_sql.push_str(&conditions.join(" AND "));
}
let mut count_query = db.query(count_sql);
if let Some(search) = &meta.search {
if !search.is_empty() {
count_query = count_query.bind(("search", search.clone()));
}
}
if let Some(filter_val) = meta.filter.clone() {
count_query = bind_filter_value(count_query, filter_val);
}
let count_result: Vec<CountResult> = count_query.await?.take(0)?;
let total = count_result.first().map(|c| c.count);
let meta = MetaResponseDto {
page: Some(page),
per_page: Some(per_page),
total,
};
Ok(ResponseListSuccessDto {
data: raw,
meta: Some(meta),
})
}
+45
View File
@@ -0,0 +1,45 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
use serde_json::json;
use crate::{ResponseListSuccessDto, ResponseSuccessDto};
pub fn success_response<T: Serialize>(params: ResponseSuccessDto<T>) -> Response {
(
StatusCode::OK,
Json(json!({
"data": params.data,
"version": "0.1.0",
})),
)
.into_response()
}
pub fn success_list_response<T: Serialize>(
params: ResponseListSuccessDto<T>,
) -> Response {
(
StatusCode::OK,
Json(json!({
"data": params.data,
"meta": params.meta,
"version": "0.1.0",
})),
)
.into_response()
}
pub fn common_response(status: StatusCode, message: &str) -> Response {
(
status,
Json(json!({
"message": message,
"version": "0.1.0",
})),
)
.into_response()
}
+28
View File
@@ -0,0 +1,28 @@
use axum::http::StatusCode;
use validator::Validate;
pub fn validate_request<T: Validate>(
payload: &T,
) -> Result<(), (StatusCode, String)> {
if let Err(validation_errors) = payload.validate() {
let error_messages: Vec<String> = validation_errors
.field_errors()
.iter()
.flat_map(|(_, errors)| {
errors.iter().map(move |error| {
format!(
"{}",
error
.message
.clone()
.unwrap_or_else(|| "Invalid value".into())
)
})
})
.collect();
return Err((StatusCode::BAD_REQUEST, error_messages.join(", ")));
}
Ok(())
}