chore: normalize code

This commit is contained in:
Maulana Sodiqin
2025-05-17 02:08:59 +07:00
parent 874f25aa80
commit a43ef8ec04
104 changed files with 691 additions and 655 deletions
Generated
+11 -35
View File
@@ -1847,7 +1847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]] [[package]]
name = "imphnen-backend-service" name = "imphnen-backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@@ -1855,7 +1855,7 @@ dependencies = [
"axum-test", "axum-test",
"chrono", "chrono",
"imphnen-entities", "imphnen-entities",
"imphnen-gateway-service", "imphnen-gateway",
"imphnen-libs", "imphnen-libs",
"imphnen-utils", "imphnen-utils",
"lazy_static", "lazy_static",
@@ -1872,31 +1872,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "imphnen-cms-service" name = "imphnen-dimentorin"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"axum-test",
"chrono",
"imphnen-entities",
"imphnen-libs",
"imphnen-utils",
"lazy_static",
"rand 0.9.0",
"regex",
"serde",
"serde_json",
"surrealdb",
"tokio",
"tower-http",
"utoipa",
"utoipa-swagger-ui",
"validator",
]
[[package]]
name = "imphnen-dimentorin-service"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@@ -1931,7 +1907,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "imphnen-gacha-service" name = "imphnen-gacha"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@@ -1955,7 +1931,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "imphnen-gateway-service" name = "imphnen-gateway"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@@ -1963,9 +1939,9 @@ dependencies = [
"axum-test", "axum-test",
"chrono", "chrono",
"imphnen-entities", "imphnen-entities",
"imphnen-iam-service", "imphnen-iam",
"imphnen-libs", "imphnen-libs",
"imphnen-middleware-service", "imphnen-middleware",
"imphnen-utils", "imphnen-utils",
"lazy_static", "lazy_static",
"rand 0.9.0", "rand 0.9.0",
@@ -1981,7 +1957,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "imphnen-iam-service" name = "imphnen-iam"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@@ -2020,7 +1996,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "imphnen-middleware-service" name = "imphnen-middleware"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@@ -2029,7 +2005,7 @@ dependencies = [
"chrono", "chrono",
"futures", "futures",
"imphnen-entities", "imphnen-entities",
"imphnen-iam-service", "imphnen-iam",
"imphnen-libs", "imphnen-libs",
"imphnen-utils", "imphnen-utils",
"lazy_static", "lazy_static",
@@ -4317,7 +4293,7 @@ dependencies = [
"axum-test", "axum-test",
"chrono", "chrono",
"imphnen-entities", "imphnen-entities",
"imphnen-iam-service", "imphnen-iam",
"imphnen-libs", "imphnen-libs",
"imphnen-utils", "imphnen-utils",
"serde_json", "serde_json",
+11 -11
View File
@@ -1,18 +1,16 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
# "imphnen-*",
"imphnen-utils",
"imphnen-libs",
"imphnen-entities",
"imphnen-cms-service",
"imphnen-iam-service",
"imphnen-gacha-service",
"imphnen-gateway-service",
"imphnen-dimentorin-service",
"imphnen-middleware-service",
"imphnen-core-service",
"tests", "tests",
"imphnen-iam",
"imphnen-libs",
"imphnen-utils",
"imphnen-gacha",
"imphnen-gateway",
"imphnen-backend",
"imphnen-entities",
"imphnen-dimentorin",
"imphnen-middleware",
] ]
@@ -38,6 +36,8 @@ lazy_static = "1.4.0"
regex = "1.11.1" regex = "1.11.1"
axum-test = "17.2.0" axum-test = "17.2.0"
fancy-regex = "0.14.0" fancy-regex = "0.14.0"
futures = "0.3.31"
tower = "0.5.2"
[profile.release] [profile.release]
lto = "fat" lto = "fat"
@@ -1,13 +1,13 @@
[package] [package]
name = "imphnen-backend-service" name = "imphnen-backend"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
imphnen-gateway = { version = "0.1.0", path = "../imphnen-gateway" }
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
imphnen-gateway-service = { version = "0.1.0", path = "../imphnen-gateway-service" }
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -1,4 +1,4 @@
use imphnen_gateway_service::gateway_service; use imphnen_gateway::gateway_service;
use imphnen_libs::axum_init; use imphnen_libs::axum_init;
#[tokio::main] #[tokio::main]
@@ -140,6 +140,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
.await?; .await?;
println!("✅ Inserted: {}", name); println!("✅ Inserted: {}", name);
} }
println!("Semua permissions berhasil disimpan ke SurrealDB!"); println!("All Permissions seeded");
Ok(()) Ok(())
} }
@@ -65,6 +65,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
.await?; .await?;
println!("✅ Inserted role: {}", name); println!("✅ Inserted role: {}", name);
} }
println!("Semua role berhasil disimpan ke SurrealDB!"); println!("All Roles seeded");
Ok(()) Ok(())
} }
@@ -19,24 +19,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
let role_permissions = vec![ let role_permissions = vec![
( (
"50133429-f4b1-4249-9f97-7b86e6ee9d86", // Staf "50133429-f4b1-4249-9f97-7b86e6ee9d86",
vec![ vec![
"7c15e31d-36e2-49f9-97db-138c03fb0cf6", // Read List Users "7c15e31d-36e2-49f9-97db-138c03fb0cf6",
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6", // Read Detail Users "319ee593-ff0a-4f29-bbaf-9feb3174a3a6",
], ],
), ),
( (
"f6b03f25-e416-4893-ac88-caaa690afb07", // Admin "f6b03f25-e416-4893-ac88-caaa690afb07",
vec![ vec![
"023e2dfe-93c3-4008-94a8-b5dff403f73b", // Create Users "023e2dfe-93c3-4008-94a8-b5dff403f73b",
"96df0689-2ae9-4894-bf00-837c19415e5c", // Delete Users "96df0689-2ae9-4894-bf00-837c19415e5c",
"98b3dc4c-0124-461f-afcd-166637c5e6e8", // Update Users "98b3dc4c-0124-461f-afcd-166637c5e6e8",
"319ee593-ff0a-4f29-bbaf-9feb3174a3a6", // Read Detail Users "319ee593-ff0a-4f29-bbaf-9feb3174a3a6",
"7c15e31d-36e2-49f9-97db-138c03fb0cf6", // Read List Users "7c15e31d-36e2-49f9-97db-138c03fb0cf6",
"9164ca6e-c7e3-4238-a15f-f36ab9577e7e", // Read List Roles "9164ca6e-c7e3-4238-a15f-f36ab9577e7e",
"319ee593-ff0a-4f29-bbaf-9feb3174a3a2", // Create Roles "319ee593-ff0a-4f29-bbaf-9feb3174a3a2",
"a00d5608-4c48-4542-845c-dfe004687022", // Update Roles "a00d5608-4c48-4542-845c-dfe004687022",
"35b0d992-65c8-4b62-b030-e6e0320e4048", // Delete Roles "35b0d992-65c8-4b62-b030-e6e0320e4048",
], ],
), ),
]; ];
@@ -53,7 +53,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.await?; .await?;
println!("✅ Updated permissions for role ID: {}", role_id); println!("✅ Updated permissions for role ID: {}", role_id);
} }
println!("Semua roles telah diperbarui dengan permissions di SurrealDB!"); println!("All Roles permissions seeded");
Ok(()) Ok(())
} }
@@ -90,6 +90,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
println!("✅ Inserted user: {} ({})", fullname, email); println!("✅ Inserted user: {} ({})", fullname, email);
} }
println!("Semua users berhasil disimpan ke SurrealDB!"); println!("All Users seeded");
Ok(()) Ok(())
} }
@@ -1,4 +1,4 @@
use imphnen_gateway_service::gateway_service; use imphnen_gateway::gateway_service;
use imphnen_libs::axum_init; use imphnen_libs::axum_init;
#[tokio::main] #[tokio::main]
-14
View File
@@ -1,14 +0,0 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
@@ -1,12 +1,12 @@
[package] [package]
name = "imphnen-cms-service" name = "imphnen-dimentorin"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -21,4 +21,4 @@ tokio.workspace = true
chrono.workspace = true chrono.workspace = true
anyhow.workspace = true anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
+2 -2
View File
@@ -6,6 +6,6 @@ edition = "2024"
[dependencies] [dependencies]
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
surrealdb = { workspace = true, features = ["kv-mem"] }
thiserror.workspace = true
utoipa.workspace = true utoipa.workspace = true
surrealdb.workspace = true
thiserror.workspace = true
@@ -1,12 +1,12 @@
[package] [package]
name = "imphnen-iam-service" name = "imphnen-gacha"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -21,4 +21,4 @@ tokio.workspace = true
chrono.workspace = true chrono.workspace = true
anyhow.workspace = true anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
-26
View File
@@ -1,26 +0,0 @@
[package]
name = "imphnen-gateway-service"
version = "0.1.0"
edition = "2024"
[dependencies]
imphnen-iam-service = { version = "0.1.0", path = "../imphnen-iam-service" }
imphnen-middleware-service = { version = "0.1.0", path = "../imphnen-middleware-service" }
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
axum.workspace = true
serde.workspace = true
serde_json.workspace = true
utoipa.workspace = true
lazy_static.workspace = true
regex.workspace = true
validator.workspace = true
axum-test.workspace = true
surrealdb.workspace = true
rand.workspace = true
tokio.workspace = true
chrono.workspace = true
anyhow.workspace = true
tower-http.workspace = true
utoipa-swagger-ui.workspace = true
@@ -1,12 +1,14 @@
[package] [package]
name = "imphnen-dimentorin-service" name = "imphnen-gateway"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
imphnen-middleware = { version = "0.1.0", path = "../imphnen-middleware" }
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -21,4 +23,4 @@ tokio.workspace = true
chrono.workspace = true chrono.workspace = true
anyhow.workspace = true anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
@@ -1,4 +1,4 @@
use imphnen_iam_service::{ use imphnen_iam::{
auth, permissions, roles, users, AuthLoginRequestDto, AuthLoginResponsetDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, auth, permissions, roles, users, AuthLoginRequestDto, AuthLoginResponsetDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto,
AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto, PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto, RolesItemDto, RolesRequestCreateDto, RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto, UsersItemDto, UsersListItemDto, UsersUpdateRequestDto AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, MessageResponseDto, MetaRequestDto, MetaResponseDto, PermissionsItemDto, PermissionsRequestDto, ResponseListSuccessDto, ResponseSuccessDto, RolesItemDto, RolesRequestCreateDto, RolesRequestUpdateDto, TokenDto, UsersCreateRequestDto, UsersDetailItemDto, UsersItemDto, UsersListItemDto, UsersUpdateRequestDto
}; };
@@ -102,4 +102,4 @@ impl Modify for SecurityAddon {
pub fn docs_router() -> utoipa::openapi::OpenApi { pub fn docs_router() -> utoipa::openapi::OpenApi {
ApiDoc::openapi() ApiDoc::openapi()
} }
@@ -2,12 +2,12 @@ use axum::{
Extension, Router, middleware::from_fn, response::Redirect, routing::get, Extension, Router, middleware::from_fn, response::Redirect, routing::get,
}; };
use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient}; use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient};
use imphnen_iam_service::{iam_protected_routes, iam_public_routes}; use imphnen_iam::{iam_protected_routes, iam_public_routes};
use imphnen_middleware::{auth_middleware, cors_middleware};
use utoipa_swagger_ui::SwaggerUi;
pub mod docs; pub mod docs;
pub use docs::*; pub use docs::*;
use imphnen_middleware_service::{auth_middleware, cors_middleware};
use utoipa_swagger_ui::SwaggerUi;
pub async fn gateway_service( pub async fn gateway_service(
surrealdb_ws: SurrealWsClient, surrealdb_ws: SurrealWsClient,
@@ -1,66 +0,0 @@
use crate::{PermissionsItemDto, PermissionsItemDtoRaw};
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
use utoipa::ToSchema;
use validator::Validate;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct RolesRequestUpdateDto {
#[validate(length(min = 1, message = "Role name must not be empty"))]
pub name: Option<String>,
pub permissions: Option<Vec<String>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct RolesRequestCreateDto {
#[validate(length(min = 1, message = "Role name must not be empty"))]
pub name: String,
pub permissions: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RolesItemListDto {
pub id: String,
pub name: String,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RolesItemDto {
pub id: String,
pub name: String,
pub permissions: Vec<PermissionsItemDto>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RolesItemByIdDto {
pub id: String,
pub name: String,
pub is_deleted: bool,
pub permissions: Vec<PermissionsItemDto>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RolesItemByIdDtoRaw {
pub id: Thing,
pub name: String,
pub permissions: Vec<PermissionsItemDtoRaw>,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RolesItemDtoRaw {
pub id: Thing,
pub name: String,
pub permissions: Vec<PermissionsItemDtoRaw>,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
@@ -1,317 +0,0 @@
use super::{
UsersActiveInactiveSchema, UsersItemDto, UsersItemDtoRaw, UsersListItemDto,
UsersListItemDtoRaw, UsersSchema, UsersSetNewPasswordSchema,
};
use crate::{
extract_id, get_id, make_thing, query_list_with_meta, AppState, MetaRequestDto,
PermissionsItemDto, PermissionsItemDtoRaw, ResourceEnum, ResponseListSuccessDto,
RolesItemDto, RolesItemDtoRaw,
};
use anyhow::{anyhow, bail, Result};
pub struct UsersRepository<'a> {
state: &'a AppState,
}
impl<'a> UsersRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
pub async fn query_user_list(
&self,
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<UsersListItemDto>>> {
let db = &self.state.surrealdb_ws;
let mut conditions = vec!["is_deleted = false".to_string()];
if let Some(search) = meta.search.as_deref() {
if !search.is_empty() {
conditions.push("string::contains(fullname ?? '', $search)".to_string());
}
}
if let (Some(filter_by), Some(_filter)) =
(meta.filter_by.as_ref(), meta.filter.as_ref())
{
conditions.push(format!("{} = $filter", filter_by));
}
let where_clause = if !conditions.is_empty() {
format!("WHERE {}", conditions.join(" AND "))
} else {
String::new()
};
let limit = meta.per_page.unwrap_or(10);
let page = meta.page.unwrap_or(1);
if page < 1 || limit < 1 {
return Err(anyhow!("Invalid pagination parameters"));
}
let start = (page - 1) * limit;
let select_query = format!(
"
SELECT
id,
role.name AS role,
fullname,
email,
avatar,
phone_number,
is_active,
gender,
birthdate,
created_at,
updated_at
FROM {}
{}
LIMIT {} START {}
FETCH role
",
ResourceEnum::Users.to_string(),
where_clause,
limit,
start
);
let raw_result = query_list_with_meta::<UsersListItemDtoRaw>(
db,
&ResourceEnum::Users.to_string(),
&meta,
vec![],
Some(select_query),
)
.await?;
let transformed_data = raw_result
.data
.into_iter()
.map(|user| UsersListItemDto {
id: extract_id(&user.id),
fullname: user.fullname,
email: user.email,
avatar: user.avatar,
phone_number: user.phone_number,
is_active: user.is_active,
role: user.role.unwrap_or_else(|| "-".into()),
created_at: user.created_at,
updated_at: user.updated_at,
})
.collect::<Vec<_>>();
Ok(ResponseListSuccessDto {
data: transformed_data,
meta: raw_result.meta,
})
}
pub async fn query_user_by_email(&self, email: String) -> Result<UsersItemDtoRaw> {
let db = &self.state.surrealdb_ws;
let sql = format!(
"SELECT *, role AS role FROM {} WHERE email = $email AND is_deleted = false LIMIT 1 FETCH role, role.permissions",
ResourceEnum::Users.to_string()
);
let response: Option<UsersItemDtoRaw> = db
.query(sql)
.bind(("email", email.clone()))
.await?
.take(0)?;
match response {
Some(user) if !user.role.is_deleted => {
let permissions = user
.role
.permissions
.into_iter()
.map(|perm| PermissionsItemDtoRaw {
id: perm.id,
name: perm.name,
created_at: perm.created_at,
updated_at: perm.updated_at,
})
.collect::<Vec<_>>();
Ok(UsersItemDtoRaw {
id: user.id,
fullname: user.fullname,
email: user.email,
avatar: user.avatar,
phone_number: user.phone_number,
is_active: user.is_active,
is_deleted: user.is_deleted,
gender: user.gender,
birthdate: user.birthdate,
password: user.password,
created_at: user.created_at,
updated_at: user.updated_at,
role: RolesItemDtoRaw {
id: user.role.id,
name: user.role.name,
permissions,
created_at: user.role.created_at,
updated_at: user.role.updated_at,
is_deleted: user.role.is_deleted,
},
})
}
_ => bail!("User not found"),
}
}
pub async fn query_user_by_id(&self, id: String) -> Result<UsersItemDto> {
let db = &self.state.surrealdb_ws;
let query = format!(
"SELECT *, role AS role FROM app_users:⟨{}⟩ FETCH role, role.permissions",
id
);
let mut result = db.query(query).await?;
let response: Option<UsersItemDtoRaw> = result.take(0)?;
match response {
Some(user) if !user.role.is_deleted => {
let permissions = user
.role
.permissions
.into_iter()
.map(|perm| PermissionsItemDto {
id: extract_id(&perm.id),
name: perm.name,
created_at: perm.created_at,
updated_at: perm.updated_at,
})
.collect::<Vec<_>>();
Ok(UsersItemDto {
id: extract_id(&user.id),
fullname: user.fullname,
email: user.email,
avatar: user.avatar,
phone_number: user.phone_number,
is_active: user.is_active,
is_deleted: user.is_deleted,
gender: user.gender,
birthdate: user.birthdate,
password: user.password,
role: RolesItemDto {
id: extract_id(&user.role.id),
name: user.role.name,
permissions,
created_at: user.role.created_at,
updated_at: user.role.updated_at,
},
created_at: user.created_at,
updated_at: user.updated_at,
})
}
_ => bail!("User not found"),
}
}
pub async fn query_create_user(&self, data: UsersSchema) -> Result<String> {
let db = &self.state.surrealdb_ws;
let record: Option<UsersSchema> = db
.create(ResourceEnum::Users.to_string())
.content(data)
.await?;
match record {
Some(_) => Ok("Success create user".into()),
None => bail!("Failed to create user"),
}
}
pub async fn query_update_user(&self, data: UsersSchema) -> Result<String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(&data.id)?;
let existing = self.query_user_by_id(data.id.id.to_raw()).await?;
if existing.is_deleted {
bail!("User already deleted");
}
let merged = UsersSchema {
password: existing.password,
created_at: existing.created_at,
role: make_thing("app_roles", &existing.role.id),
..data.clone()
};
let record: Option<UsersSchema> = db.update(record_key).merge(merged).await?;
match record {
Some(_) => Ok("Success update user".into()),
None => bail!("Failed to update user"),
}
}
pub async fn query_active_inactive_user(
&self,
email: String,
data: UsersActiveInactiveSchema,
) -> Result<String> {
let db = &self.state.surrealdb_ws;
let user = self.query_user_by_email(email.clone()).await?;
if user.is_deleted {
bail!("User already deleted");
}
let record_key = get_id(&user.id)?;
let record: Option<UsersSchema> = db
.update(record_key)
.merge(UsersActiveInactiveSchema {
is_active: data.is_active,
})
.await?;
match record {
Some(_) => Ok("Success update user".into()),
None => bail!("Failed to update user"),
}
}
pub async fn query_active_inactive_user_by_id(
&self,
id: String,
data: UsersActiveInactiveSchema,
) -> Result<String> {
let db = &self.state.surrealdb_ws;
let record: Option<UsersSchema> = db
.update((ResourceEnum::Users.to_string(), id))
.merge(UsersActiveInactiveSchema {
is_active: data.is_active,
})
.await?;
match record {
Some(_) => Ok("Success update user".into()),
None => bail!("Failed to update user"),
}
}
pub async fn query_update_password_user(
&self,
email: String,
data: UsersSetNewPasswordSchema,
) -> Result<String> {
let db = &self.state.surrealdb_ws;
let user = self.query_user_by_email(email).await?;
let record: Option<UsersSetNewPasswordSchema> = db
.update((ResourceEnum::Users.to_string(), user.id.id.to_raw()))
.merge(UsersSetNewPasswordSchema {
password: data.password.clone(),
})
.await?;
dbg!(record.clone());
match record {
Some(_) => Ok("Success update password user".into()),
None => bail!("Failed to update password user"),
}
}
pub async fn query_delete_user(&self, id: String) -> Result<String> {
let db = &self.state.surrealdb_ws;
let user_id = make_thing(&ResourceEnum::Users.to_string(), &id);
let user = self.query_user_by_id(user_id.id.to_raw()).await?;
if user.is_deleted {
bail!("User already deleted");
}
let id = make_thing(&ResourceEnum::Users.to_string(), &user.id);
let record_key = get_id(&id)?;
let record: Option<UsersSchema> = db
.update(record_key)
.merge(serde_json::json!({ "is_deleted": true }))
.await?;
match record {
Some(_) => Ok("Success delete user".into()),
None => bail!("Failed to delete user"),
}
}
}
@@ -1,56 +0,0 @@
use crate::{get_iso_date, make_thing, ResourceEnum};
use serde::{Deserialize, Serialize};
use surrealdb::{sql::Thing, Uuid};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UsersSchema {
pub id: Thing,
pub fullname: String,
pub email: String,
pub password: String,
pub avatar: Option<String>,
pub phone_number: String,
pub is_active: bool,
pub is_deleted: bool,
pub gender: Option<String>,
pub birthdate: Option<String>,
pub role: Thing,
pub created_at: String,
pub updated_at: String,
}
impl Default for UsersSchema {
fn default() -> Self {
UsersSchema {
id: make_thing(
&ResourceEnum::Users.to_string(),
&Uuid::new_v4().to_string(),
),
fullname: String::new(),
email: String::new(),
password: String::new(),
avatar: None,
phone_number: String::new(),
is_active: false,
is_deleted: false,
gender: None,
birthdate: None,
role: make_thing(
&ResourceEnum::Roles.to_string(),
&Uuid::new_v4().to_string(),
),
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UsersSetNewPasswordSchema {
pub password: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UsersActiveInactiveSchema {
pub is_active: bool,
}
@@ -1,12 +1,12 @@
[package] [package]
name = "imphnen-gacha-service" name = "imphnen-iam"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -21,4 +21,4 @@ tokio.workspace = true
chrono.workspace = true chrono.workspace = true
anyhow.workspace = true anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
@@ -7,6 +7,7 @@ pub mod v1;
pub use imphnen_entities::*; pub use imphnen_entities::*;
pub use imphnen_libs::*; pub use imphnen_libs::*;
pub use imphnen_utils::*;
pub use v1::*; pub use v1::*;
pub fn create_test_user( pub fn create_test_user(
@@ -27,6 +28,7 @@ pub fn create_test_user(
gender: None, gender: None,
birthdate: None, birthdate: None,
role: make_thing("app_roles", role_id), role: make_thing("app_roles", role_id),
..Default::default() created_at: get_iso_date(),
updated_at: get_iso_date(),
} }
} }
@@ -18,7 +18,7 @@ pub struct PermissionsItemDto {
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PermissionsItemDtoRaw { pub struct PermissionsQueryDto {
pub id: Thing, pub id: Thing,
pub name: String, pub name: String,
pub created_at: Option<String>, pub created_at: Option<String>,
+40
View File
@@ -0,0 +1,40 @@
use crate::{PermissionsItemDto, PermissionsQueryDto};
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
use utoipa::ToSchema;
use validator::Validate;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct RolesRequestDto {
#[validate(length(min = 1, message = "Role name must not be empty"))]
pub name: String,
pub permissions: Option<Vec<String>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RolesListItemDto {
pub id: String,
pub name: String,
pub permissions_count: u64,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RolesDetailItemDto {
pub id: String,
pub name: String,
pub permissions: Vec<PermissionsItemDto>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RolesDetailQueryDto {
pub id: Thing,
pub name: String,
pub permissions: Vec<PermissionsQueryDto>,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
@@ -1,3 +1,4 @@
use crate::{RolesDetailQueryDto, RolesItemDto};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -5,17 +6,10 @@ use surrealdb::sql::Thing;
use utoipa::ToSchema; use utoipa::ToSchema;
use validator::Validate; use validator::Validate;
use crate::{RolesItemDto, RolesItemDtoRaw};
lazy_static! { lazy_static! {
static ref PASSWORD_REGEX: Regex = Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap(); static ref PASSWORD_REGEX: Regex = Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap();
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct UsersActiveInactiveRequestDto {
pub is_active: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct UsersCreateRequestDto { pub struct UsersCreateRequestDto {
#[validate( #[validate(
@@ -23,6 +17,7 @@ pub struct UsersCreateRequestDto {
email(message = "Email not valid") email(message = "Email not valid")
)] )]
pub email: String, pub email: String,
#[validate(length( #[validate(length(
min = 8, min = 8,
message = "Password must have at least 8 characters" message = "Password must have at least 8 characters"
@@ -32,8 +27,10 @@ pub struct UsersCreateRequestDto {
message = "Password must include uppercase, lowercase, number, and special character" message = "Password must include uppercase, lowercase, number, and special character"
))] ))]
pub password: String, pub password: String,
#[validate(length(min = 2, message = "Fullname at least have 2 character"))] #[validate(length(min = 2, message = "Fullname at least have 2 character"))]
pub fullname: String, pub fullname: String,
#[validate(length( #[validate(length(
min = 10, min = 10,
message = "Phone number at least have 10 character" message = "Phone number at least have 10 character"
@@ -71,23 +68,6 @@ pub struct UsersUpdateRequestDto {
pub role_id: String, pub role_id: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct UsersItemDto {
pub id: String,
pub role: RolesItemDto,
pub fullname: String,
pub email: String,
pub avatar: Option<String>,
pub phone_number: String,
pub is_active: bool,
pub is_deleted: bool,
pub gender: Option<String>,
pub birthdate: Option<String>,
pub password: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct UsersDetailItemDto { pub struct UsersDetailItemDto {
pub id: String, pub id: String,
@@ -99,6 +79,8 @@ pub struct UsersDetailItemDto {
pub is_active: bool, pub is_active: bool,
pub gender: Option<String>, pub gender: Option<String>,
pub birthdate: Option<String>, pub birthdate: Option<String>,
pub created_at: String,
pub updated_at: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
@@ -115,9 +97,9 @@ pub struct UsersListItemDto {
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UsersListItemDtoRaw { pub struct UsersListQueryDto {
pub id: Thing, pub id: Thing,
pub role: Option<String>, pub role: RolesDetailQueryDto,
pub fullname: String, pub fullname: String,
pub email: String, pub email: String,
pub avatar: Option<String>, pub avatar: Option<String>,
@@ -127,8 +109,24 @@ pub struct UsersListItemDtoRaw {
pub updated_at: String, pub updated_at: String,
} }
impl UsersListQueryDto {
pub fn list_from(&self, role: String) -> UsersListItemDto {
UsersListItemDto {
id: self.id.id.to_raw(),
role,
fullname: self.fullname.clone(),
email: self.email.clone(),
avatar: self.avatar.clone(),
phone_number: self.phone_number.clone(),
is_active: self.is_active,
created_at: self.created_at.clone(),
updated_at: self.updated_at.clone(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UsersItemDtoRaw { pub struct UsersDetailQueryDto {
pub id: Thing, pub id: Thing,
pub fullname: String, pub fullname: String,
pub email: String, pub email: String,
@@ -138,8 +136,8 @@ pub struct UsersItemDtoRaw {
pub is_deleted: bool, pub is_deleted: bool,
pub gender: Option<String>, pub gender: Option<String>,
pub birthdate: Option<String>, pub birthdate: Option<String>,
pub role: RolesItemDtoRaw,
pub password: String, pub password: String,
pub role: RolesDetailQueryDto,
pub created_at: String, pub created_at: String,
pub updated_at: String, pub updated_at: String,
} }
@@ -0,0 +1,296 @@
use super::{UsersDetailQueryDto, UsersListItemDto, UsersListQueryDto, UsersSchema};
use crate::{
AppState, MetaRequestDto, PermissionsItemDto, PermissionsItemDtoRaw, ResourceEnum,
ResponseListSuccessDto, RolesDetailQueryDto, extract_id, get_id, make_thing,
query_list_with_meta,
};
use anyhow::{Result, bail};
pub struct UsersRepository<'a> {
state: &'a AppState,
}
pub fn build_user_by_field_query(field: &str) -> String {
format!(
r#"
SELECT *, role AS role
FROM {}
WHERE {} = $value AND is_deleted = false
LIMIT 1
FETCH role, role.permissions
"#,
ResourceEnum::Users.to_string(),
field
)
}
impl<'a> UsersRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
pub async fn query_user_list(
&self,
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<UsersListItemDto>>> {
let db = &self.state.surrealdb_ws;
let raw_result = query_list_with_meta::<UsersListQueryDto>(
db,
&ResourceEnum::Users.to_string(),
&meta,
vec!["is_deleted = false".into()],
None,
"fullname",
Some(vec![
"email",
"fullname",
"id",
"is_active",
"is_deleted",
"role",
"role.permissions",
]),
)
.await?;
let data = raw_result
.data
.into_iter()
.map(|schema| {
let role = schema.clone().role.name;
schema.list_from(role)
})
.collect::<Vec<_>>();
Ok(ResponseListSuccessDto {
data,
meta: raw_result.meta,
})
}
pub async fn query_user_by_email(
&self,
email: String,
) -> Result<UsersDetailQueryDto> {
let db = &self.state.surrealdb_ws;
let sql = build_user_by_field_query("email");
let user_opt: Option<UsersDetailQueryDto> = db
.query(sql)
.bind(("email", email.clone()))
.await?
.take(0)?;
let Some(user) = user_opt else {
bail!("User not found");
};
if user.role.is_deleted {
bail!("User not found");
}
let permissions = user
.role
.permissions
.into_iter()
.map(|perm| PermissionsItemDtoRaw {
id: perm.id,
name: perm.name,
created_at: perm.created_at,
updated_at: perm.updated_at,
})
.collect();
Ok(UsersDetailQueryDto {
id: user.id,
fullname: user.fullname,
email: user.email,
avatar: user.avatar,
phone_number: user.phone_number,
is_active: user.is_active,
is_deleted: user.is_deleted,
gender: user.gender,
birthdate: user.birthdate,
password: user.password,
created_at: user.created_at,
updated_at: user.updated_at,
role: RolesDetailQueryDto {
id: user.role.id,
name: user.role.name,
created_at: user.role.created_at,
updated_at: user.role.updated_at,
is_deleted: user.role.is_deleted,
permissions,
},
})
}
pub async fn query_user_by_id(&self, id: String) -> Result<UsersDetailQueryDto> {
let db = &self.state.surrealdb_ws;
let sql = build_user_by_field_query(&make_thing("app_users", &id).to_raw());
let user_opt: Option<UsersDetailQueryDto> = db
.query(sql)
.bind(("email", make_thing("app_users", &id).to_raw()))
.await?
.take(0)?;
let Some(user) = user_opt else {
bail!("User not found");
};
if user.role.is_deleted {
bail!("User not found");
}
let permissions = user
.role
.permissions
.into_iter()
.map(|perm| PermissionsItemDtoRaw {
id: perm.id,
name: perm.name,
created_at: perm.created_at,
updated_at: perm.updated_at,
})
.collect();
Ok(UsersDetailQueryDto {
id: user.id,
fullname: user.fullname,
email: user.email,
avatar: user.avatar,
phone_number: user.phone_number,
is_active: user.is_active,
is_deleted: user.is_deleted,
gender: user.gender,
birthdate: user.birthdate,
password: user.password,
created_at: user.created_at,
updated_at: user.updated_at,
role: RolesDetailQueryDto {
id: user.role.id,
name: user.role.name,
created_at: user.role.created_at,
updated_at: user.role.updated_at,
is_deleted: user.role.is_deleted,
permissions,
},
})
}
pub async fn query_create_user(&self, data: UsersSchema) -> Result<String> {
let db = &self.state.surrealdb_ws;
let record: Option<UsersSchema> = db
.create(ResourceEnum::Users.to_string())
.content(data)
.await?;
match record {
Some(_) => Ok("Success create user".into()),
None => bail!("Failed to create user"),
}
}
pub async fn query_update_user(&self, data: UsersSchema) -> Result<String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(&data.id)?;
let existing = self.query_user_by_id(data.id.id.to_raw()).await?;
if existing.is_deleted {
bail!("User already deleted");
}
let merged = UsersSchema {
password: existing.password,
created_at: existing.created_at,
role: make_thing("app_roles", &existing.role.id),
..data.clone()
};
let record: Option<UsersSchema> = db.update(record_key).merge(merged).await?;
match record {
Some(_) => Ok("Success update user".into()),
None => bail!("Failed to update user"),
}
}
pub async fn query_active_inactive_user(
&self,
email: String,
data: UsersActiveInactiveSchema,
) -> Result<String> {
let db = &self.state.surrealdb_ws;
let user = self.query_user_by_email(email.clone()).await?;
if user.is_deleted {
bail!("User already deleted");
}
let record_key = get_id(&user.id)?;
let record: Option<UsersSchema> = db
.update(record_key)
.merge(UsersActiveInactiveSchema {
is_active: data.is_active,
})
.await?;
match record {
Some(_) => Ok("Success update user".into()),
None => bail!("Failed to update user"),
}
}
pub async fn query_active_inactive_user_by_id(
&self,
id: String,
data: UsersActiveInactiveSchema,
) -> Result<String> {
let db = &self.state.surrealdb_ws;
let record: Option<UsersSchema> = db
.update((ResourceEnum::Users.to_string(), id))
.merge(UsersActiveInactiveSchema {
is_active: data.is_active,
})
.await?;
match record {
Some(_) => Ok("Success update user".into()),
None => bail!("Failed to update user"),
}
}
pub async fn query_update_password_user(
&self,
email: String,
data: UsersSetNewPasswordSchema,
) -> Result<String> {
let db = &self.state.surrealdb_ws;
let user = self.query_user_by_email(email).await?;
let record: Option<UsersSetNewPasswordSchema> = db
.update((ResourceEnum::Users.to_string(), user.id.id.to_raw()))
.merge(UsersSetNewPasswordSchema {
password: data.password.clone(),
})
.await?;
dbg!(record.clone());
match record {
Some(_) => Ok("Success update password user".into()),
None => bail!("Failed to update password user"),
}
}
pub async fn query_delete_user(&self, id: String) -> Result<String> {
let db = &self.state.surrealdb_ws;
let user_id = make_thing(&ResourceEnum::Users.to_string(), &id);
let user = self.query_user_by_id(user_id.id.to_raw()).await?;
if user.is_deleted {
bail!("User already deleted");
}
let id = make_thing(&ResourceEnum::Users.to_string(), &user.id);
let record_key = get_id(&id)?;
let record: Option<UsersSchema> = db
.update(record_key)
.merge(serde_json::json!({ "is_deleted": true }))
.await?;
match record {
Some(_) => Ok("Success delete user".into()),
None => bail!("Failed to delete user"),
}
}
}
+66
View File
@@ -0,0 +1,66 @@
use super::{UsersDetailItemDto, UsersListItemDto};
use crate::RolesItemDto;
use imphnen_utils::Crud;
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UsersSchema {
pub id: Thing,
pub fullname: String,
pub email: String,
pub password: String,
pub avatar: Option<String>,
pub phone_number: String,
pub is_active: bool,
pub is_deleted: bool,
pub gender: Option<String>,
pub birthdate: Option<String>,
pub role: Thing,
pub created_at: String,
pub updated_at: String,
}
impl Crud<UsersListItemDto, String> for UsersSchema {
fn list(&self, role: String) -> UsersListItemDto {
UsersListItemDto {
id: self.id.id.to_raw(),
role,
fullname: self.fullname.clone(),
email: self.email.clone(),
avatar: self.avatar.clone(),
phone_number: self.phone_number.clone(),
is_active: self.is_active,
created_at: self.created_at.clone(),
updated_at: self.updated_at.clone(),
}
}
}
impl Crud<UsersDetailItemDto, RolesItemDto> for UsersSchema {
fn detail(&self, role: RolesItemDto) -> UsersDetailItemDto {
UsersDetailItemDto {
id: self.id.id.to_raw(),
role,
fullname: self.fullname.clone(),
email: self.email.clone(),
avatar: self.avatar.clone(),
phone_number: self.phone_number.clone(),
is_active: self.is_active,
gender: self.gender.clone(),
birthdate: self.birthdate.clone(),
created_at: self.created_at.clone(),
updated_at: self.updated_at.clone(),
}
}
}
impl UsersSchema {
pub fn list_from(&self, role: String) -> UsersListItemDto {
self.list(role)
}
pub fn detail_from(&self, role: RolesItemDto) -> UsersDetailItemDto {
self.detail(role)
}
}
+4 -4
View File
@@ -5,11 +5,11 @@ edition = "2024"
[dependencies] [dependencies]
imphnen-entities = { path = "../imphnen-entities" } imphnen-entities = { path = "../imphnen-entities" }
argon2.workspace = true
axum.workspace = true axum.workspace = true
tokio.workspace = true tokio.workspace = true
chrono.workspace = true
jsonwebtoken.workspace = true
serde.workspace = true serde.workspace = true
argon2.workspace = true
lettre.workspace = true lettre.workspace = true
surrealdb = { workspace = true, features = ["kv-mem"] } chrono.workspace = true
surrealdb.workspace = true
jsonwebtoken.workspace = true
@@ -1,13 +1,13 @@
[package] [package]
name = "imphnen-middleware-service" name = "imphnen-middleware"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-iam-service = { version = "0.1.0", path = "../imphnen-iam-service" } imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
axum.workspace = true axum.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -22,6 +22,6 @@ tokio.workspace = true
chrono.workspace = true chrono.workspace = true
anyhow.workspace = true anyhow.workspace = true
tower-http.workspace = true tower-http.workspace = true
futures.workspace = true
tower.workspace = true
utoipa-swagger-ui.workspace = true utoipa-swagger-ui.workspace = true
futures = "0.3.31"
tower = "0.5.2"
@@ -2,7 +2,7 @@ use axum::{
Extension, extract::Request, http::StatusCode, middleware::Next, Extension, extract::Request, http::StatusCode, middleware::Next,
response::Response, response::Response,
}; };
use imphnen_iam_service::{UsersItemDtoRaw, UsersRepository}; use imphnen_iam::{UsersItemDtoRaw, UsersRepository};
use imphnen_libs::AppState; use imphnen_libs::AppState;
use imphnen_utils::{common_response, extract_email}; use imphnen_utils::{common_response, extract_email};
use std::convert::Infallible; use std::convert::Infallible;
@@ -3,7 +3,7 @@ use axum::{
http::{Request, Response, StatusCode}, http::{Request, Response, StatusCode},
}; };
use futures::future::BoxFuture; use futures::future::BoxFuture;
use imphnen_iam_service::{AuthRepository, PermissionsEnum}; use imphnen_iam::{AuthRepository, PermissionsEnum};
use imphnen_libs::AppState; use imphnen_libs::AppState;
use imphnen_utils::{common_response, extract_email}; use imphnen_utils::{common_response, extract_email};
use std::task::{Context, Poll}; use std::task::{Context, Poll};
+1 -1
View File
@@ -14,4 +14,4 @@ anyhow.workspace = true
axum-test.workspace = true axum-test.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
validator.workspace = true validator.workspace = true
+8
View File
@@ -0,0 +1,8 @@
pub trait Crud<T, Args> {
fn list(&self, _args: Args) -> T {
unimplemented!("list() is not implemented for this type")
}
fn detail(&self, _args: Args) -> T {
unimplemented!("detail() is not implemented for this type")
}
}
+4
View File
@@ -2,17 +2,20 @@ use imphnen_entities::*;
use imphnen_libs::*; use imphnen_libs::*;
pub mod bind_filter; pub mod bind_filter;
pub mod crud;
pub mod extract_email; pub mod extract_email;
pub mod generate_date; pub mod generate_date;
pub mod generate_otp; pub mod generate_otp;
pub mod get_id; pub mod get_id;
pub mod make_thing; pub mod make_thing;
pub mod mock_test; pub mod mock_test;
pub mod query_builder;
pub mod query_list; pub mod query_list;
pub mod response_format; pub mod response_format;
pub mod validator; pub mod validator;
pub use bind_filter::*; pub use bind_filter::*;
pub use crud::*;
pub use extract_email::*; pub use extract_email::*;
pub use generate_date::*; pub use generate_date::*;
pub use generate_otp::*; pub use generate_otp::*;
@@ -21,6 +24,7 @@ pub use imphnen_entities::*;
pub use imphnen_libs::*; pub use imphnen_libs::*;
pub use make_thing::*; pub use make_thing::*;
pub use mock_test::*; pub use mock_test::*;
pub use query_builder::*;
pub use query_list::*; pub use query_list::*;
pub use response_format::*; pub use response_format::*;
pub use validator::*; pub use validator::*;
+137
View File
@@ -0,0 +1,137 @@
use imphnen_libs::MetaRequestDto;
pub struct ListQueryBuilder {
resource: String,
conditions: Vec<String>,
limit: usize,
start: usize,
order_by: Option<String>,
order: Option<String>,
fetch: Vec<String>,
select_fields: Vec<String>,
}
impl ListQueryBuilder {
pub fn from_meta(
resource: impl Into<String>,
meta: &MetaRequestDto,
search_field: impl Into<String>,
select_fields: Option<Vec<&str>>,
) -> Self {
let mut builder = Self::new(resource)
.with_search(meta.search.as_deref(), &search_field.into())
.with_filter(meta.filter_by.as_deref(), meta.filter.as_deref())
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
.with_pagination(meta.page, meta.per_page);
if let Some(fields) = select_fields {
builder = builder.with_select_fields(fields);
}
builder
}
pub fn new(resource: impl Into<String>) -> Self {
Self {
resource: resource.into(),
conditions: vec!["is_deleted = false".into()],
limit: 10,
start: 0,
order_by: None,
order: None,
fetch: vec![],
select_fields: vec![],
}
}
pub fn with_select_fields(mut self, fields: Vec<&str>) -> Self {
self.select_fields = fields.into_iter().map(String::from).collect();
self
}
pub fn with_search(mut self, search: Option<&str>, field: &str) -> Self {
if let Some(search) = search {
if !search.is_empty() {
self
.conditions
.push(format!("string::contains({} ?? '', $search)", field));
}
}
self
}
pub fn with_filter(mut self, field: Option<&str>, value: Option<&str>) -> Self {
if let (Some(f), Some(v)) = (field, value) {
if !v.is_empty() {
self.conditions.push(format!("{} = $filter", f));
}
}
self
}
pub fn with_pagination(
mut self,
page: Option<u64>,
per_page: Option<u64>,
) -> Self {
let limit = per_page.unwrap_or(10).max(1);
let page = page.unwrap_or(1).max(1);
self.limit = limit as usize;
self.start = ((page - 1) * limit) as usize;
self
}
pub fn with_sorting(mut self, sort_by: Option<&str>, order: Option<&str>) -> Self {
self.order_by = sort_by.map(|s| s.to_string());
self.order = order.map(|o| o.to_uppercase());
self
}
pub fn with_fetch(mut self, fetch: impl Into<String>) -> Self {
self.fetch.push(fetch.into());
self
}
pub fn build(self) -> String {
let where_clause = if !self.conditions.is_empty() {
format!("WHERE {}", self.conditions.join(" AND "))
} else {
String::new()
};
let order_clause = if let Some(field) = self.order_by {
let ord = self.order.unwrap_or_else(|| "ASC".into());
format!("ORDER BY {} {}", field, ord)
} else {
String::new()
};
let fetch_clause = if !self.fetch.is_empty() {
format!("FETCH {}", self.fetch.join(", "))
} else {
String::new()
};
let select_clause = if self.select_fields.is_empty() {
"*".to_string()
} else {
self.select_fields.join(", ")
};
format!(
r#"
SELECT {} FROM {}
{}
{}
LIMIT {} START {}
{}
"#,
select_clause,
self.resource,
where_clause,
order_clause,
self.limit,
self.start,
fetch_clause
)
}
}
+31 -45
View File
@@ -1,8 +1,8 @@
use super::bind_filter_value; use super::{ListQueryBuilder, bind_filter_value};
use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto}; use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto};
use anyhow::{bail, Result}; use anyhow::Result;
use serde::{de::DeserializeOwned, Serialize}; use serde::{Serialize, de::DeserializeOwned};
use surrealdb::{engine::remote::ws::Client, Surreal}; use surrealdb::{Surreal, engine::remote::ws::Client};
pub async fn query_list_with_meta<T>( pub async fn query_list_with_meta<T>(
db: &Surreal<Client>, db: &Surreal<Client>,
@@ -10,75 +10,61 @@ pub async fn query_list_with_meta<T>(
meta: &MetaRequestDto, meta: &MetaRequestDto,
conditions: Vec<String>, conditions: Vec<String>,
custom_select: Option<String>, custom_select: Option<String>,
search_field: &str,
select_fields: Option<Vec<&str>>,
) -> Result<ResponseListSuccessDto<Vec<T>>> ) -> Result<ResponseListSuccessDto<Vec<T>>>
where where
T: DeserializeOwned + Serialize, T: DeserializeOwned + Serialize,
{ {
let page = meta.page.unwrap_or(1); let page = meta.page.unwrap_or(1).max(1);
let per_page = meta.per_page.unwrap_or(10); let per_page = meta.per_page.unwrap_or(10).max(1);
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 start = (page - 1) * per_page;
let sql = custom_select.unwrap_or_else(|| { let sql = custom_select.unwrap_or_else(|| {
let mut s = format!("SELECT * FROM {}", table); ListQueryBuilder::from_meta(table, meta, search_field, select_fields).build()
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); let mut query_exec = db.query(sql);
if let Some(search) = &meta.search { if let Some(search) = &meta.search {
if !search.is_empty() { if !search.is_empty() {
query_exec = query_exec.bind(("search", search.clone())); query_exec = query_exec.bind(("search", search.clone()));
} }
} }
if let Some(filter_val) = meta.filter.clone() { if let Some(filter_val) = &meta.filter {
query_exec = bind_filter_value(query_exec, filter_val); query_exec = bind_filter_value(query_exec, filter_val.clone());
} }
query_exec = query_exec query_exec = query_exec
.bind(("per_page", per_page)) .bind(("per_page", per_page))
.bind(("start", start)); .bind(("start", start));
let raw: Vec<T> = query_exec.await?.take(0)?; let raw: Vec<T> = query_exec.await?.take(0)?;
let mut count_sql = format!("SELECT count() FROM {}", table);
if !conditions.is_empty() { let mut count_query = db.query(format!(
count_sql.push_str(" WHERE "); "SELECT count() FROM {} {}",
count_sql.push_str(&conditions.join(" AND ")); table,
} if conditions.is_empty() {
let mut count_query = db.query(count_sql); String::new()
} else {
format!("WHERE {}", conditions.join(" AND "))
}
));
if let Some(search) = &meta.search { if let Some(search) = &meta.search {
if !search.is_empty() { if !search.is_empty() {
count_query = count_query.bind(("search", search.clone())); count_query = count_query.bind(("search", search.clone()));
} }
} }
if let Some(filter_val) = meta.filter.clone() { if let Some(filter_val) = &meta.filter {
count_query = bind_filter_value(count_query, filter_val); count_query = bind_filter_value(count_query, filter_val.clone());
} }
let count_result: Vec<CountResult> = count_query.await?.take(0)?; let count_result: Vec<CountResult> = count_query.await?.take(0)?;
let total = count_result.first().map(|c| c.count); let total = count_result.first().map(|c| c.count);
let meta = MetaResponseDto {
page: Some(page),
per_page: Some(per_page),
total,
};
Ok(ResponseListSuccessDto { Ok(ResponseListSuccessDto {
data: raw, data: raw,
meta: Some(meta), meta: Some(MetaResponseDto {
page: Some(page),
per_page: Some(per_page),
total,
}),
}) })
} }
+1 -1
View File
@@ -4,10 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" }
imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" }
imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" }
imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" }
imphnen-iam-service = { version = "0.1.0", path = "../imphnen-iam-service" }
axum.workspace = true axum.workspace = true
axum-test.workspace = true axum-test.workspace = true
chrono.workspace = true chrono.workspace = true

Some files were not shown because too many files have changed in this diff Show More