postgress
This commit is contained in:
+31
-30
@@ -1,30 +1,31 @@
|
||||
[package]
|
||||
name = "imphnen-cms"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
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
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[package.metadata.validator.regex]
|
||||
VALID_URL_REGEX = "^https?://"
|
||||
[package]
|
||||
name = "imphnen-cms"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
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
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
sea-orm.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[package.metadata.validator.regex]
|
||||
VALID_URL_REGEX = "^https?://"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
pub mod v1;
|
||||
|
||||
pub use v1::landing;
|
||||
pub use v1::landing::events;
|
||||
pub use v1::landing::testimonials;
|
||||
pub use v1::landing::events::events_public_routes;
|
||||
pub use v1::landing::events::events_protected_routes;
|
||||
pub use v1::landing::testimonials::testimonials_public_routes;
|
||||
pub use v1::landing::testimonials::testimonials_protected_routes;
|
||||
pub mod v1;
|
||||
|
||||
pub use v1::landing;
|
||||
pub use v1::landing::events;
|
||||
pub use v1::landing::testimonials;
|
||||
pub use v1::landing::events::events_public_routes;
|
||||
pub use v1::landing::events::events_protected_routes;
|
||||
pub use v1::landing::testimonials::testimonials_public_routes;
|
||||
pub use v1::landing::testimonials::testimonials_protected_routes;
|
||||
|
||||
@@ -1,129 +1,139 @@
|
||||
use super::{
|
||||
events_dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto,
|
||||
EventsUpdateRequestDto,
|
||||
},
|
||||
events_service::EventsService,
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, http::HeaderMap};
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto, ValidatedJson,
|
||||
};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
("filter" = Option<String>, Query, description = "Filter value"),
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_list(
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::get_event_list(&state, meta).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_by_id(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::get_event_by_id(&state, id).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/create",
|
||||
request_body = EventsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "[ADMIN] Create new event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn post_create_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::create_event(&state, payload).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
request_body = EventsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Update event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn patch_update_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::update_event(&state, id, payload).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Soft delete event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn delete_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::delete_event(&state, id).await
|
||||
})
|
||||
}
|
||||
use super::{
|
||||
events_dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto,
|
||||
EventsUpdateRequestDto,
|
||||
},
|
||||
events_service::EventsService,
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, http::HeaderMap, http::StatusCode};
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto, ValidatedJson,
|
||||
};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::common_response;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
("filter" = Option<String>, Query, description = "Filter value"),
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get event list", body = ResponseListSuccessDto<Vec<EventsListItemDto>>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_list(
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
EventsService::get_event_list(&state, meta).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/events/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get event by ID", body = ResponseSuccessDto<EventsDetailItemDto>)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn get_event_by_id(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
EventsService::get_event_by_id(&state, parsed_id).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/create",
|
||||
request_body = EventsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "[ADMIN] Create new event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn post_create_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::create_event(&state, payload).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
request_body = EventsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Update event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn patch_update_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::update_event(&state, id, payload).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/events/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Event ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Soft delete event", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Events"
|
||||
)]
|
||||
pub async fn delete_event(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
EventsService::delete_event(&state, parsed_id).await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,154 +1,152 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
// Custom URL validator that ensures valid HTTP/HTTPS URLs
|
||||
pub fn validate_url(url: &str) -> Result<(), ValidationError> {
|
||||
lazy_static! {
|
||||
static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://[^\s$.?#].[^\s]*$").unwrap();
|
||||
}
|
||||
if VALID_URL_REGEX.is_match(url) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("invalid_url"))
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validator for future dates
|
||||
pub fn validate_future_date(end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||
let now = Utc::now();
|
||||
if end_date > &now {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("future_date"))
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validator for event date ranges (for combined validation)
|
||||
pub fn validate_date_range(start_date: &DateTime<Utc>, end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||
if start_date <= end_date {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("date_range"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct EventsCreateRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Name must be between 1 and 100 characters"))]
|
||||
pub name: String,
|
||||
|
||||
#[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
|
||||
pub description: String,
|
||||
|
||||
#[validate(custom(
|
||||
function = "validate_url",
|
||||
message = "Detail link must be a valid HTTP/HTTPS URL"
|
||||
))]
|
||||
pub detail_link: String,
|
||||
|
||||
#[validate(range(min = 0.0, max = 1_000_000.0, message = "Price must be between 0 and 1,000,000"))]
|
||||
pub price: f64,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
#[validate(custom(
|
||||
function = "validate_future_date",
|
||||
message = "End date must be in the future"
|
||||
))]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
#[validate(length(max = 200, message = "Location name cannot exceed 200 characters"))]
|
||||
pub location: Option<String>,
|
||||
pub is_online: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct EventsUpdateRequestDto {
|
||||
#[validate(length(min = 1, message = "Name is required"))]
|
||||
pub name: String,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub description: String,
|
||||
#[validate(url(message = "Detail link must be a valid URL"))]
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsListItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub location: Option<String>,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsDetailItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsQueryDto {
|
||||
pub id: Thing,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl EventsQueryDto {
|
||||
pub fn from(self) -> EventsListItemDto {
|
||||
EventsListItemDto {
|
||||
id: self.id.id.to_raw(),
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
detail_link: self.detail_link,
|
||||
price: self.price,
|
||||
location: self.location,
|
||||
is_online: self.is_online,
|
||||
start_date: self.start_date,
|
||||
end_date: self.end_date,
|
||||
created_at: self.created_at,
|
||||
is_deleted: self.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
use chrono::{DateTime, Utc};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::LazyLock;
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
// Custom URL validator that ensures valid HTTP/HTTPS URLs
|
||||
static VALID_URL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^https?://[^\s$.?#].[^\s]*$").unwrap());
|
||||
|
||||
pub fn validate_url(url: &str) -> Result<(), ValidationError> {
|
||||
if VALID_URL_REGEX.is_match(url) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("invalid_url"))
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validator for future dates
|
||||
pub fn validate_future_date(end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||
let now = Utc::now();
|
||||
if end_date > &now {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("future_date"))
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validator for event date ranges (for combined validation)
|
||||
pub fn validate_date_range(start_date: &DateTime<Utc>, end_date: &DateTime<Utc>) -> Result<(), ValidationError> {
|
||||
if start_date <= end_date {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("date_range"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct EventsCreateRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Name must be between 1 and 100 characters"))]
|
||||
pub name: String,
|
||||
|
||||
#[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
|
||||
pub description: String,
|
||||
|
||||
#[validate(custom(
|
||||
function = "validate_url",
|
||||
message = "Detail link must be a valid HTTP/HTTPS URL"
|
||||
))]
|
||||
pub detail_link: String,
|
||||
|
||||
#[validate(range(min = 0.0, max = 1_000_000.0, message = "Price must be between 0 and 1,000,000"))]
|
||||
pub price: f64,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
#[validate(custom(
|
||||
function = "validate_future_date",
|
||||
message = "End date must be in the future"
|
||||
))]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
#[validate(length(max = 200, message = "Location name cannot exceed 200 characters"))]
|
||||
pub location: Option<String>,
|
||||
pub is_online: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct EventsUpdateRequestDto {
|
||||
#[validate(length(min = 1, message = "Name is required"))]
|
||||
pub name: String,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub description: String,
|
||||
#[validate(url(message = "Detail link must be a valid URL"))]
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsListItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub location: Option<String>,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventsDetailItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsQueryDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl EventsQueryDto {
|
||||
pub fn from(self) -> EventsListItemDto {
|
||||
EventsListItemDto {
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
detail_link: self.detail_link,
|
||||
price: self.price,
|
||||
location: self.location,
|
||||
is_online: self.is_online,
|
||||
start_date: self.start_date,
|
||||
end_date: self.end_date,
|
||||
created_at: self.created_at,
|
||||
is_deleted: self.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date, make_thing};
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, QueryOrder};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
use uuid::Uuid; // Added Uuid import
|
||||
use imphnen_entities::seaorm::common::events::{Entity as EventsEntity, Column as EventsColumn, Model as EventsModel};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto, AppStatePostgresExt};
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_utils::Result;
|
||||
use crate::events::events_dto::EventsQueryDto;
|
||||
use crate::events::events_schema::EventsSchema;
|
||||
|
||||
pub struct EventsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -21,14 +25,54 @@ impl<'a> EventsRepository<'a> {
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<EventsQueryDto>>> {
|
||||
let now = Instant::now();
|
||||
let query = ListQueryBuilder::new(ResourceEnum::Events.to_string())
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_pagination(meta.page, Some(10))
|
||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||
.build();
|
||||
info!(query = %query, "Executing SurrealDB query");
|
||||
let res: Vec<EventsQueryDto> =
|
||||
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let page_size = 10u64;
|
||||
let _offset = (page - 1) * page_size;
|
||||
|
||||
let mut query = EventsEntity::find()
|
||||
.filter(EventsColumn::IsDeleted.eq(false)); // Add sorting
|
||||
if let Some(sort_by) = &meta.sort_by {
|
||||
match sort_by.as_str() {
|
||||
"created_at" => {
|
||||
if meta.order.as_deref() == Some("desc") {
|
||||
query = query.order_by_desc(EventsColumn::CreatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(EventsColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
"name" => {
|
||||
if meta.order.as_deref() == Some("desc") {
|
||||
query = query.order_by_desc(EventsColumn::Name);
|
||||
} else {
|
||||
query = query.order_by_asc(EventsColumn::Name);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
query = query.order_by_desc(EventsColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query = query.order_by_desc(EventsColumn::CreatedAt);
|
||||
}
|
||||
|
||||
let paginator = query.paginate(self.state.postgres_db(), page_size);
|
||||
let events: Vec<EventsModel> = paginator.fetch_page(page - 1).await?;
|
||||
|
||||
let res: Vec<EventsQueryDto> = events.into_iter().map(|model| EventsQueryDto {
|
||||
id: model.id.to_string(),
|
||||
name: model.name,
|
||||
description: model.description,
|
||||
detail_link: model.detail_link,
|
||||
price: model.price,
|
||||
is_online: model.is_online,
|
||||
is_deleted: model.is_deleted,
|
||||
start_date: model.start_date.to_rfc3339(),
|
||||
end_date: model.end_date.to_rfc3339(),
|
||||
created_at: model.created_at.to_rfc3339(),
|
||||
updated_at: model.updated_at.to_rfc3339(),
|
||||
location: model.location,
|
||||
}).collect();
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
@@ -43,25 +87,30 @@ impl<'a> EventsRepository<'a> {
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
|
||||
pub async fn query_event_by_id(&self, id: Uuid) -> Result<EventsQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
// Attempt to parse the ID. If it's a full Thing (e.g., "events:some_id"), extract the ID part.
|
||||
// Otherwise, assume it's already the raw ID.
|
||||
let parsed_id = if id.contains(":") {
|
||||
let thing = make_thing(ResourceEnum::Events.to_string().as_str(), &id);
|
||||
get_id(&thing)?.1.to_string()
|
||||
} else {
|
||||
id.clone()
|
||||
};
|
||||
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
|
||||
.with_id(&parsed_id)
|
||||
.with_select_fields(vec!["*"]);
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query");
|
||||
let result: Option<EventsQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
|
||||
let event = EventsEntity::find_by_id(id)
|
||||
.filter(EventsColumn::IsDeleted.eq(false))
|
||||
.one(self.state.postgres_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Event not found"))?;
|
||||
|
||||
let result = EventsQueryDto {
|
||||
id: event.id.to_string(),
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
detail_link: event.detail_link,
|
||||
price: event.price,
|
||||
is_online: event.is_online,
|
||||
is_deleted: event.is_deleted,
|
||||
start_date: event.start_date.to_rfc3339(),
|
||||
end_date: event.end_date.to_rfc3339(),
|
||||
created_at: event.created_at.to_rfc3339(),
|
||||
updated_at: event.updated_at.to_rfc3339(),
|
||||
location: event.location,
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
@@ -69,27 +118,30 @@ impl<'a> EventsRepository<'a> {
|
||||
println!("Query 'query_event_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match result {
|
||||
Some(event) => {
|
||||
if event.is_deleted {
|
||||
bail!("Event not found");
|
||||
}
|
||||
Ok(event)
|
||||
}
|
||||
None => bail!("Event not found"),
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let query_str = format!("CREATE {} CONTENT ...", ResourceEnum::Events);
|
||||
info!(query = %query_str, "Executing SurrealDB query");
|
||||
let record: Option<EventsSchema> = db
|
||||
.create(ResourceEnum::Events.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
|
||||
let active_model = imphnen_entities::seaorm::common::events::ActiveModel {
|
||||
id: ActiveValue::Set(Uuid::parse_str(&data.id)?),
|
||||
name: ActiveValue::Set(data.name),
|
||||
description: ActiveValue::Set(data.description),
|
||||
detail_link: ActiveValue::Set(data.detail_link),
|
||||
price: ActiveValue::Set(data.price),
|
||||
is_online: ActiveValue::Set(data.is_online),
|
||||
is_deleted: ActiveValue::Set(data.is_deleted),
|
||||
location: ActiveValue::Set(data.location),
|
||||
start_date: ActiveValue::Set(chrono::DateTime::parse_from_rfc3339(&data.start_date)?.with_timezone(&chrono::Utc)),
|
||||
end_date: ActiveValue::Set(chrono::DateTime::parse_from_rfc3339(&data.end_date)?.with_timezone(&chrono::Utc)),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
};
|
||||
|
||||
let _result = EventsEntity::insert(active_model).exec(self.state.postgres_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
@@ -97,32 +149,36 @@ impl<'a> EventsRepository<'a> {
|
||||
println!("Query 'query_create_event' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success create event".into()),
|
||||
None => bail!("Failed to create event"),
|
||||
}
|
||||
Ok("Success create event".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_event(&self, data: EventsSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let existing = self.query_event_by_id(data.id.id.to_raw()).await?;
|
||||
|
||||
let existing = self.query_event_by_id(Uuid::parse_str(&data.id)?).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Event already deleted");
|
||||
return Err(AppError::BadRequestError("Event already deleted".to_string()));
|
||||
}
|
||||
|
||||
let merged = EventsSchema {
|
||||
created_at: existing.created_at,
|
||||
updated_at: get_iso_date(),
|
||||
..data
|
||||
};
|
||||
|
||||
let record_key = get_id(&merged.id)?;
|
||||
let query_str = format!("UPDATE {:?} MERGE ...", record_key);
|
||||
info!(query = %query_str, "Executing SurrealDB query");
|
||||
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
|
||||
|
||||
let mut active_model: imphnen_entities::seaorm::common::events::ActiveModel = EventsEntity::find_by_id(Uuid::parse_str(&data.id)?)
|
||||
.one(self.state.postgres_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Event not found"))?
|
||||
.into();
|
||||
|
||||
active_model.name = ActiveValue::Set(data.name);
|
||||
active_model.description = ActiveValue::Set(data.description);
|
||||
active_model.detail_link = ActiveValue::Set(data.detail_link);
|
||||
active_model.price = ActiveValue::Set(data.price);
|
||||
active_model.is_online = ActiveValue::Set(data.is_online);
|
||||
active_model.location = ActiveValue::Set(data.location);
|
||||
active_model.start_date = ActiveValue::Set(chrono::DateTime::parse_from_rfc3339(&data.start_date)?.with_timezone(&chrono::Utc));
|
||||
active_model.end_date = ActiveValue::Set(chrono::DateTime::parse_from_rfc3339(&data.end_date)?.with_timezone(&chrono::Utc));
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(self.state.postgres_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
@@ -130,28 +186,28 @@ impl<'a> EventsRepository<'a> {
|
||||
println!("Query 'query_update_event' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success update event".into()),
|
||||
None => bail!("Failed to update event"),
|
||||
}
|
||||
Ok("Success update event".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_event(&self, id: String) -> Result<String> {
|
||||
pub async fn query_delete_event(&self, id: Uuid) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let event = self.query_event_by_id(id).await?;
|
||||
if event.is_deleted {
|
||||
bail!("Event not found");
|
||||
return Err(AppError::NotFoundError("Event not found".to_string()));
|
||||
}
|
||||
|
||||
let record_key = get_id(&event.id)?;
|
||||
let query_str = format!("UPDATE {:?} MERGE {{ is_deleted: true }}", record_key);
|
||||
info!(query = %query_str, "Executing SurrealDB query");
|
||||
let record: Option<EventsSchema> = db
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
|
||||
let mut active_model: imphnen_entities::seaorm::common::events::ActiveModel = EventsEntity::find_by_id(id)
|
||||
.one(self.state.postgres_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Event not found"))?
|
||||
.into();
|
||||
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(self.state.postgres_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
@@ -159,9 +215,6 @@ impl<'a> EventsRepository<'a> {
|
||||
println!("Query 'query_delete_event' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success delete event".into()),
|
||||
None => bail!("Failed to delete event"),
|
||||
}
|
||||
Ok("Success delete event".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +1,94 @@
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use super::events_dto::{
|
||||
EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsSchema {
|
||||
pub id: Thing,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
pub name: String,
|
||||
pub end_date: String,
|
||||
pub start_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for EventsSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Events.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
detail_link: String::new(),
|
||||
price: 0.0,
|
||||
location: None,
|
||||
is_online: false,
|
||||
is_deleted: false,
|
||||
start_date: String::new(),
|
||||
end_date: String::new(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventsSchema {
|
||||
pub fn from(dto: EventsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
detail_link: dto.detail_link,
|
||||
price: dto.price,
|
||||
location: dto.location,
|
||||
is_online: dto.is_online,
|
||||
is_deleted: false,
|
||||
start_date: dto.start_date,
|
||||
end_date: dto.end_date,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(payload: EventsCreateRequestDto) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Events.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
price: payload.price,
|
||||
location: payload.location,
|
||||
is_online: payload.is_online,
|
||||
is_deleted: false,
|
||||
end_date: payload.end_date.to_string(),
|
||||
start_date: payload.start_date.to_string(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(payload: EventsUpdateRequestDto, id: String) -> Self {
|
||||
Self {
|
||||
id: make_thing(&ResourceEnum::Events.to_string(), &id),
|
||||
name: payload.name,
|
||||
price: payload.price,
|
||||
location: payload.location,
|
||||
is_online: payload.is_online,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
end_date: payload.end_date.to_string(),
|
||||
start_date: payload.start_date.to_string(),
|
||||
updated_at: get_iso_date(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
use imphnen_utils::get_iso_date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::events_dto::{
|
||||
EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsSchema {
|
||||
pub id: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
pub name: String,
|
||||
pub end_date: String,
|
||||
pub start_date: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for EventsSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
detail_link: String::new(),
|
||||
price: 0.0,
|
||||
location: None,
|
||||
is_online: false,
|
||||
is_deleted: false,
|
||||
start_date: String::new(),
|
||||
end_date: String::new(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventsSchema {
|
||||
pub fn from(dto: EventsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
detail_link: dto.detail_link,
|
||||
price: dto.price,
|
||||
location: dto.location,
|
||||
is_online: dto.is_online,
|
||||
is_deleted: false,
|
||||
start_date: dto.start_date,
|
||||
end_date: dto.end_date,
|
||||
created_at: dto.created_at,
|
||||
updated_at: dto.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(payload: EventsCreateRequestDto) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
price: payload.price,
|
||||
location: payload.location,
|
||||
is_online: payload.is_online,
|
||||
is_deleted: false,
|
||||
end_date: payload.end_date.to_string(),
|
||||
start_date: payload.start_date.to_string(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(payload: EventsUpdateRequestDto, id: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name: payload.name,
|
||||
price: payload.price,
|
||||
location: payload.location,
|
||||
is_online: payload.is_online,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
end_date: payload.end_date.to_string(),
|
||||
start_date: payload.start_date.to_string(),
|
||||
updated_at: get_iso_date(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,102 @@
|
||||
use super::{
|
||||
events_dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto,
|
||||
EventsUpdateRequestDto,
|
||||
},
|
||||
events_repository::EventsRepository,
|
||||
events_schema::EventsSchema,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use imphnen_libs::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validate_request,
|
||||
};
|
||||
|
||||
pub struct EventsService;
|
||||
|
||||
impl EventsService {
|
||||
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<EventsListItemDto> = data
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(EventsQueryDto::from)
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_by_id(id).await {
|
||||
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
|
||||
data: EventsDetailItemDto {
|
||||
id: event.id.id.to_raw(),
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
detail_link: event.detail_link,
|
||||
price: event.price,
|
||||
is_online: event.is_online,
|
||||
start_date: event.start_date,
|
||||
end_date: event.end_date,
|
||||
created_at: event.created_at,
|
||||
updated_at: event.updated_at,
|
||||
location: event.location,
|
||||
},
|
||||
}),
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_event(
|
||||
state: &AppState,
|
||||
payload: EventsCreateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::create(payload);
|
||||
match repo.query_create_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_event(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
payload: EventsUpdateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::update(payload, id);
|
||||
match repo.query_update_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_event(state: &AppState, id: String) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_delete_event(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
use super::{
|
||||
events_dto::{
|
||||
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto,
|
||||
EventsUpdateRequestDto,
|
||||
},
|
||||
events_repository::EventsRepository,
|
||||
events_schema::EventsSchema,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use imphnen_libs::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validate_request,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct EventsService;
|
||||
|
||||
impl EventsService {
|
||||
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<EventsListItemDto> = data
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(EventsQueryDto::from)
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_event_by_id(state: &AppState, id: Uuid) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_event_by_id(id).await {
|
||||
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
|
||||
data: EventsDetailItemDto {
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
detail_link: event.detail_link,
|
||||
price: event.price,
|
||||
is_online: event.is_online,
|
||||
start_date: event.start_date,
|
||||
end_date: event.end_date,
|
||||
created_at: event.created_at,
|
||||
updated_at: event.updated_at,
|
||||
location: event.location,
|
||||
},
|
||||
}),
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_event(
|
||||
state: &AppState,
|
||||
payload: EventsCreateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::create(payload);
|
||||
match repo.query_create_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_event(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
payload: EventsUpdateRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::update(payload, id);
|
||||
match repo.query_update_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_event(state: &AppState, id: Uuid) -> Response {
|
||||
let repo = EventsRepository::new(state);
|
||||
match repo.query_delete_event(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
pub mod events_controller;
|
||||
pub mod events_dto;
|
||||
pub mod events_repository;
|
||||
pub mod events_schema;
|
||||
pub mod events_service;
|
||||
|
||||
// Export only the necessary public items
|
||||
pub use events_dto::{
|
||||
EventsCreateRequestDto,
|
||||
EventsUpdateRequestDto,
|
||||
EventsListItemDto,
|
||||
EventsDetailItemDto,
|
||||
};
|
||||
pub use events_controller::{
|
||||
get_event_list,
|
||||
get_event_by_id,
|
||||
post_create_event,
|
||||
patch_update_event,
|
||||
delete_event,
|
||||
};
|
||||
|
||||
pub fn events_public_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/events", get(get_event_list))
|
||||
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
|
||||
}
|
||||
|
||||
pub fn events_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/events/create", post(post_create_event))
|
||||
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
|
||||
.route("/cms/landing/events/delete/{id}", delete(delete_event))
|
||||
}
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
pub mod events_controller;
|
||||
pub mod events_dto;
|
||||
pub mod events_repository;
|
||||
pub mod events_schema;
|
||||
pub mod events_service;
|
||||
|
||||
// Export only the necessary public items
|
||||
pub use events_dto::{
|
||||
EventsCreateRequestDto,
|
||||
EventsUpdateRequestDto,
|
||||
EventsListItemDto,
|
||||
EventsDetailItemDto,
|
||||
};
|
||||
pub use events_controller::{
|
||||
get_event_list,
|
||||
get_event_by_id,
|
||||
post_create_event,
|
||||
patch_update_event,
|
||||
delete_event,
|
||||
};
|
||||
|
||||
pub fn events_public_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/events", get(get_event_list))
|
||||
.route("/cms/landing/events/detail/{id}", get(get_event_by_id))
|
||||
}
|
||||
|
||||
pub fn events_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/events/create", post(post_create_event))
|
||||
.route("/cms/landing/events/update/{id}", patch(patch_update_event))
|
||||
.route("/cms/landing/events/delete/{id}", delete(delete_event))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod events;
|
||||
pub mod testimonials;
|
||||
|
||||
pub use events::events_public_routes;
|
||||
pub use events::events_protected_routes;
|
||||
pub use testimonials::testimonials_public_routes;
|
||||
pub use testimonials::testimonials_protected_routes;
|
||||
pub mod events;
|
||||
pub mod testimonials;
|
||||
|
||||
pub use events::events_public_routes;
|
||||
pub use events::events_protected_routes;
|
||||
pub use testimonials::testimonials_public_routes;
|
||||
pub use testimonials::testimonials_protected_routes;
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
pub mod testimonials_controller;
|
||||
pub mod testimonials_dto;
|
||||
pub mod testimonials_repository;
|
||||
pub mod testimonials_schema;
|
||||
pub mod testimonials_service;
|
||||
|
||||
// Export only the necessary public items
|
||||
pub use testimonials_dto::{
|
||||
TestimonialsCreateRequestDto,
|
||||
TestimonialsUpdateRequestDto,
|
||||
TestimonialsListItemDto,
|
||||
TestimonialsDetailItemDto,
|
||||
};
|
||||
pub use testimonials_controller::{
|
||||
get_testimonial_list,
|
||||
get_testimonial_by_id,
|
||||
post_create_testimonial,
|
||||
patch_update_testimonial,
|
||||
delete_testimonial,
|
||||
};
|
||||
|
||||
pub fn testimonials_public_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/testimonials", get(get_testimonial_list))
|
||||
.route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
|
||||
}
|
||||
|
||||
pub fn testimonials_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/testimonials/create", post(post_create_testimonial))
|
||||
.route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
|
||||
.route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
|
||||
}
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
pub mod testimonials_controller;
|
||||
pub mod testimonials_dto;
|
||||
pub mod testimonials_repository;
|
||||
pub mod testimonials_schema;
|
||||
pub mod testimonials_service;
|
||||
|
||||
// Export only the necessary public items
|
||||
pub use testimonials_dto::{
|
||||
TestimonialsCreateRequestDto,
|
||||
TestimonialsUpdateRequestDto,
|
||||
TestimonialsListItemDto,
|
||||
TestimonialsDetailItemDto,
|
||||
};
|
||||
pub use testimonials_controller::{
|
||||
get_testimonial_list,
|
||||
get_testimonial_by_id,
|
||||
post_create_testimonial,
|
||||
patch_update_testimonial,
|
||||
delete_testimonial,
|
||||
};
|
||||
|
||||
pub fn testimonials_public_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/testimonials", get(get_testimonial_list))
|
||||
.route("/cms/landing/testimonials/detail/{id}", get(get_testimonial_by_id))
|
||||
}
|
||||
|
||||
pub fn testimonials_protected_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/cms/landing/testimonials/create", post(post_create_testimonial))
|
||||
.route("/cms/landing/testimonials/update/{id}", patch(patch_update_testimonial))
|
||||
.route("/cms/landing/testimonials/delete/{id}", delete(delete_testimonial))
|
||||
}
|
||||
|
||||
@@ -1,132 +1,142 @@
|
||||
use super::{
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
testimonials_service::TestimonialsService,
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, http::HeaderMap};
|
||||
use imphnen_iam::{UsersDetailQueryDto, require_auth};
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto, ValidatedJson,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
("filter" = Option<String>, Query, description = "Filter value"),
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_list(
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::get_testimonial_list(&state, meta).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_by_id(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::get_testimonial_by_id(&state, id).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/create",
|
||||
request_body = TestimonialsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "[USER] Create new testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn post_create_testimonial(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
request_body = TestimonialsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[USER] Update testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn patch_update_testimonial(
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[USER] Soft delete testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn delete_testimonial(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
use super::{
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
testimonials_service::TestimonialsService,
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, http::HeaderMap};
|
||||
use imphnen_iam::{UsersDetailQueryDto, require_auth};
|
||||
use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto, ValidatedJson,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::common_response;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials",
|
||||
params(
|
||||
("page" = Option<i64>, Query, description = "Page number"),
|
||||
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search keyword"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||
("filter" = Option<String>, Query, description = "Filter value"),
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get testimonial list", body = ResponseListSuccessDto<Vec<TestimonialsListItemDto>>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_list(
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
TestimonialsService::get_testimonial_list(&state, meta).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/cms/landing/testimonials/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Get testimonial by ID", body = ResponseSuccessDto<TestimonialsDetailItemDto>)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn get_testimonial_by_id(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(axum::http::StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
TestimonialsService::get_testimonial_by_id(&state, parsed_id).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/create",
|
||||
request_body = TestimonialsCreateRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "[USER] Create new testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn post_create_testimonial(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
request_body = TestimonialsUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[USER] Update testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn patch_update_testimonial(
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/cms/landing/testimonials/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Testimonial ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[USER] Soft delete testimonial", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Testimonials"
|
||||
)]
|
||||
pub async fn delete_testimonial(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(axum::http::StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
TestimonialsService::delete_testimonial(&state, parsed_id, &authenticated_user).await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,100 +1,99 @@
|
||||
use imphnen_iam::v1::users::UsersSchema;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
// Custom validator for content length and format
|
||||
pub fn validate_testimonial_content(content: &str) -> Result<(), ValidationError> {
|
||||
lazy_static! {
|
||||
static ref CONTENT_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9\s.,!?'-]+$").unwrap();
|
||||
}
|
||||
if CONTENT_REGEX.is_match(content) && content.len() <= 1000 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("invalid_content"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct TestimonialsCreateRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
|
||||
pub role: String,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 1000,
|
||||
message = "Content must be between 1 and 1000 characters"
|
||||
))]
|
||||
#[validate(custom(
|
||||
function = "validate_testimonial_content",
|
||||
message = "Content contains invalid characters or is too long"
|
||||
))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct TestimonialsUpdateRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
|
||||
pub role: String,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 1000,
|
||||
message = "Content must be between 1 and 1000 characters"
|
||||
))]
|
||||
#[validate(custom(
|
||||
function = "validate_testimonial_content",
|
||||
message = "Content contains invalid characters or is too long"
|
||||
))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsListItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsDetailItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsQueryDto {
|
||||
pub id: Thing,
|
||||
pub user: UsersSchema,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl TestimonialsQueryDto {
|
||||
pub fn from(self) -> TestimonialsListItemDto {
|
||||
TestimonialsListItemDto {
|
||||
id: self.id.id.to_raw(),
|
||||
user_id: self.user.id.id.to_raw(),
|
||||
user_fullname: self.user.fullname,
|
||||
role: self.role,
|
||||
content: self.content,
|
||||
created_at: self.created_at,
|
||||
is_deleted: self.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
// Custom validator for content length and format
|
||||
pub fn validate_testimonial_content(content: &str) -> Result<(), ValidationError> {
|
||||
lazy_static! {
|
||||
static ref CONTENT_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9\s.,!?'-]+$").unwrap();
|
||||
}
|
||||
if CONTENT_REGEX.is_match(content) && content.len() <= 1000 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new("invalid_content"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct TestimonialsCreateRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
|
||||
pub role: String,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 1000,
|
||||
message = "Content must be between 1 and 1000 characters"
|
||||
))]
|
||||
#[validate(custom(
|
||||
function = "validate_testimonial_content",
|
||||
message = "Content contains invalid characters or is too long"
|
||||
))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct TestimonialsUpdateRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Role must be between 1 and 100 characters"))]
|
||||
pub role: String,
|
||||
|
||||
#[validate(length(
|
||||
min = 1,
|
||||
max = 1000,
|
||||
message = "Content must be between 1 and 1000 characters"
|
||||
))]
|
||||
#[validate(custom(
|
||||
function = "validate_testimonial_content",
|
||||
message = "Content contains invalid characters or is too long"
|
||||
))]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsListItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TestimonialsDetailItemDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsQueryDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl TestimonialsQueryDto {
|
||||
pub fn from(self) -> TestimonialsListItemDto {
|
||||
TestimonialsListItemDto {
|
||||
id: self.id,
|
||||
user_id: self.user_id,
|
||||
user_fullname: self.user_fullname,
|
||||
role: self.role,
|
||||
content: self.content,
|
||||
created_at: self.created_at,
|
||||
is_deleted: self.is_deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,187 +1,247 @@
|
||||
use super::{
|
||||
testimonials_dto::TestimonialsQueryDto, testimonials_schema::TestimonialsSchema,
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
|
||||
use serde_json;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
|
||||
pub struct TestimonialsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> TestimonialsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_testimonial_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> {
|
||||
let now = Instant::now();
|
||||
let query = ListQueryBuilder::new(ResourceEnum::Testimonials.to_string())
|
||||
.with_select_fields(vec!["*", "user.* as user"])
|
||||
.with_pagination(meta.page, Some(10))
|
||||
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
|
||||
.build();
|
||||
info!(query = %query, "Executing SurrealDB query");
|
||||
let res: Vec<TestimonialsQueryDto> =
|
||||
self.state.surrealdb_ws.query(query).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_testimonial_list' took: {elapsed:.2?}");
|
||||
}
|
||||
let data = ResponseListSuccessDto {
|
||||
data: res,
|
||||
meta: None,
|
||||
};
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_testimonial_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<TestimonialsQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
// Extract raw id if id is a thing string
|
||||
let raw_id = if id.contains(':') {
|
||||
id.split(':').last().unwrap().trim_matches(|c| c == '⟨' || c == '⟩').to_string()
|
||||
} else {
|
||||
id
|
||||
};
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
|
||||
.with_id(&raw_id)
|
||||
.with_condition("is_deleted = false")
|
||||
.with_select_fields(vec!["*", "user.* as user"]);
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query");
|
||||
let result: Option<TestimonialsQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_testimonial_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match result {
|
||||
Some(testimonial) => {
|
||||
if testimonial.is_deleted {
|
||||
bail!("Testimonial not found");
|
||||
}
|
||||
Ok(testimonial)
|
||||
}
|
||||
None => bail!("Testimonial not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<TestimonialsSchema> { // Change return type from String to TestimonialsSchema
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
info!(
|
||||
resource = %ResourceEnum::Testimonials.to_string(),
|
||||
payload = ?data,
|
||||
"Executing SurrealDB create"
|
||||
);
|
||||
let record: Option<TestimonialsSchema> = db
|
||||
.create(ResourceEnum::Testimonials.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(created_testimonial) => Ok(created_testimonial), // Return the created testimonial
|
||||
None => bail!("Failed to create testimonial"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Testimonial already deleted");
|
||||
}
|
||||
|
||||
let merged = TestimonialsSchema {
|
||||
created_at: existing.created_at,
|
||||
updated_at: get_iso_date(),
|
||||
user: existing.user.id,
|
||||
..data
|
||||
};
|
||||
|
||||
let record_key = get_id(&merged.id)?;
|
||||
info!(
|
||||
record_key = ?record_key,
|
||||
payload = ?merged,
|
||||
"Executing SurrealDB update"
|
||||
);
|
||||
let record: Option<TestimonialsSchema> =
|
||||
db.update(record_key).merge(merged).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success update testimonial".into()),
|
||||
None => bail!("Failed to update testimonial"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_testimonial(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let testimonial = self.query_testimonial_by_id(id).await?;
|
||||
if testimonial.is_deleted {
|
||||
bail!("Testimonial not found");
|
||||
}
|
||||
|
||||
let record_key = get_id(&testimonial.id)?;
|
||||
info!(
|
||||
record_key = ?record_key,
|
||||
"Executing SurrealDB soft delete"
|
||||
);
|
||||
let record: Option<TestimonialsSchema> = db
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success delete testimonial".into()),
|
||||
None => bail!("Failed to delete testimonial"),
|
||||
}
|
||||
}
|
||||
}
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, ActiveModelTrait, QueryOrder};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid; // Added Uuid import
|
||||
use imphnen_entities::seaorm::common::testimonials::{Entity as TestimonialsEntity, Column as TestimonialsColumn};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as UsersEntity;
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto, AppStatePostgresExt};
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_utils::Result;
|
||||
use crate::testimonials::testimonials_schema::TestimonialsSchema;
|
||||
use crate::testimonials::testimonials_dto::TestimonialsQueryDto;
|
||||
|
||||
pub struct TestimonialsRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> TestimonialsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
fn get_db(&self) -> &DatabaseConnection {
|
||||
self.state.postgres_db()
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_testimonial_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let per_page = meta.per_page.unwrap_or(10);
|
||||
|
||||
let mut query = TestimonialsEntity::find()
|
||||
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||
.find_also_related(UsersEntity); // Apply sorting
|
||||
if let Some(sort_by) = &meta.sort_by {
|
||||
let order = meta.order.as_deref().unwrap_or("asc");
|
||||
match sort_by.as_str() {
|
||||
"created_at" => {
|
||||
if order == "desc" {
|
||||
query = query.order_by_desc(TestimonialsColumn::CreatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(TestimonialsColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
"updated_at" => {
|
||||
if order == "desc" {
|
||||
query = query.order_by_desc(TestimonialsColumn::UpdatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(TestimonialsColumn::UpdatedAt);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
query = query.order_by_desc(TestimonialsColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query = query.order_by_desc(TestimonialsColumn::CreatedAt);
|
||||
}
|
||||
|
||||
let paginator = query.paginate(db, per_page);
|
||||
let total_pages = paginator.num_pages().await?;
|
||||
let testimonials = paginator.fetch_page(page - 1).await?;
|
||||
|
||||
let data: Vec<TestimonialsQueryDto> = testimonials
|
||||
.into_iter()
|
||||
.filter_map(|(testimonial, user)| {
|
||||
user.map(|u| TestimonialsQueryDto {
|
||||
id: testimonial.id.to_string(),
|
||||
user_id: testimonial.user_id.to_string(),
|
||||
user_fullname: format!("{} {}", u.first_name.as_deref().unwrap_or(""), u.last_name.as_deref().unwrap_or("")).trim().to_string(),
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
is_deleted: testimonial.is_deleted,
|
||||
created_at: testimonial.created_at.to_rfc3339(),
|
||||
updated_at: testimonial.updated_at.to_rfc3339(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_testimonial_list' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let response = ResponseListSuccessDto {
|
||||
data,
|
||||
meta: Some(imphnen_entities::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total: Some(total_pages),
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_testimonial_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
) -> Result<TestimonialsQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let (testimonial, user) = TestimonialsEntity::find_by_id(id)
|
||||
.filter(TestimonialsColumn::IsDeleted.eq(false))
|
||||
.find_also_related(UsersEntity)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Testimonial not found"))?;
|
||||
|
||||
let user = user.ok_or_else(|| anyhow::anyhow!("User not found for testimonial"))?;
|
||||
|
||||
let result = TestimonialsQueryDto {
|
||||
id: testimonial.id.to_string(),
|
||||
user_id: testimonial.user_id.to_string(),
|
||||
user_fullname: format!("{} {}", user.first_name.as_deref().unwrap_or(""), user.last_name.as_deref().unwrap_or("")).trim().to_string(),
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
is_deleted: testimonial.is_deleted,
|
||||
created_at: testimonial.created_at.to_rfc3339(),
|
||||
updated_at: testimonial.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_testimonial_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<TestimonialsSchema> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let active_model = imphnen_entities::seaorm::common::testimonials::ActiveModel {
|
||||
id: ActiveValue::Set(Uuid::parse_str(&data.id)?),
|
||||
user_id: ActiveValue::Set(Uuid::parse_str(&data.user_id)?),
|
||||
role: ActiveValue::Set(data.role.clone()),
|
||||
content: ActiveValue::Set(data.content.clone()),
|
||||
is_deleted: ActiveValue::Set(data.is_deleted),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
};
|
||||
|
||||
let inserted = active_model.insert(db).await?;
|
||||
let created_testimonial = TestimonialsSchema {
|
||||
id: inserted.id.to_string(),
|
||||
user_id: inserted.user_id.to_string(),
|
||||
role: inserted.role,
|
||||
content: inserted.content,
|
||||
is_deleted: inserted.is_deleted,
|
||||
created_at: inserted.created_at.to_rfc3339(),
|
||||
updated_at: inserted.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(created_testimonial)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let existing = self.query_testimonial_by_id(Uuid::parse_str(&data.id)?).await?;
|
||||
if existing.is_deleted {
|
||||
return Err(AppError::BadRequestError("Testimonial already deleted".to_string()));
|
||||
}
|
||||
|
||||
let mut active_model: imphnen_entities::seaorm::common::testimonials::ActiveModel = TestimonialsEntity::find_by_id(Uuid::parse_str(&data.id)?)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Testimonial not found"))?
|
||||
.into();
|
||||
|
||||
active_model.role = ActiveValue::Set(data.role.clone());
|
||||
active_model.content = ActiveValue::Set(data.content.clone());
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(db).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Success update testimonial".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_testimonial(&self, id: Uuid) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let testimonial = self.query_testimonial_by_id(id).await?;
|
||||
if testimonial.is_deleted {
|
||||
return Err(AppError::NotFoundError("Testimonial not found".to_string()));
|
||||
}
|
||||
|
||||
let mut active_model: imphnen_entities::seaorm::common::testimonials::ActiveModel = TestimonialsEntity::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Testimonial not found".to_string()))?
|
||||
.into();
|
||||
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model.update(db).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Success delete testimonial".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use imphnen_utils::get_iso_date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto,
|
||||
@@ -10,8 +8,8 @@ use super::testimonials_dto::{
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsSchema {
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
@@ -22,14 +20,8 @@ pub struct TestimonialsSchema {
|
||||
impl Default for TestimonialsSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Testimonials.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: make_thing(
|
||||
&ResourceEnum::Users.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
id: Uuid::new_v4().to_string(),
|
||||
user_id: Uuid::new_v4().to_string(),
|
||||
role: String::new(),
|
||||
content: String::new(),
|
||||
is_deleted: false,
|
||||
@@ -43,7 +35,7 @@ impl TestimonialsSchema {
|
||||
pub fn from(dto: TestimonialsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id,
|
||||
user: dto.user.id,
|
||||
user_id: dto.user_id,
|
||||
role: dto.role,
|
||||
content: dto.content,
|
||||
is_deleted: dto.is_deleted,
|
||||
@@ -52,13 +44,10 @@ impl TestimonialsSchema {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self {
|
||||
pub fn create(payload: TestimonialsCreateRequestDto, user_id: &str) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Testimonials.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: user_id.clone(),
|
||||
id: Uuid::new_v4().to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
is_deleted: false,
|
||||
@@ -70,21 +59,14 @@ impl TestimonialsSchema {
|
||||
pub fn update(
|
||||
payload: TestimonialsUpdateRequestDto,
|
||||
id: String,
|
||||
user_id: &Thing,
|
||||
user_id: &str,
|
||||
) -> Self {
|
||||
// Normalize id: accept either raw id (uuid) or Thing-formatted id like "table:⟨id⟩"
|
||||
let raw_id = if id.contains(':') {
|
||||
id.split(':').last().unwrap().trim_matches(|c| c == '⟨' || c == '⟩').to_string()
|
||||
} else {
|
||||
id
|
||||
};
|
||||
|
||||
Self {
|
||||
id: make_thing(&ResourceEnum::Testimonials.to_string(), &raw_id),
|
||||
id,
|
||||
role: payload.role,
|
||||
content: payload.content,
|
||||
updated_at: get_iso_date(),
|
||||
user: user_id.clone(),
|
||||
user_id: user_id.to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +1,121 @@
|
||||
use super::{
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
testimonials_repository::TestimonialsRepository,
|
||||
testimonials_schema::TestimonialsSchema,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use imphnen_libs::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, success_created_response, validate_request,
|
||||
};
|
||||
|
||||
pub struct TestimonialsService;
|
||||
|
||||
impl TestimonialsService {
|
||||
pub async fn get_testimonial_list(
|
||||
state: &AppState,
|
||||
meta: MetaRequestDto,
|
||||
) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_testimonial_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<TestimonialsListItemDto> = data
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(|e| e.from())
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_testimonial_by_id(state: &AppState, id: String) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_testimonial_by_id(id).await {
|
||||
Ok(testimonial) if !testimonial.is_deleted => {
|
||||
success_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: testimonial.id.to_raw(),
|
||||
user_id: testimonial.user.id.to_raw(),
|
||||
user_fullname: testimonial.user.fullname,
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
created_at: testimonial.created_at,
|
||||
updated_at: testimonial.updated_at,
|
||||
},
|
||||
})
|
||||
}
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Testimonial not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_testimonial(
|
||||
state: &AppState,
|
||||
payload: TestimonialsCreateRequestDto,
|
||||
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
|
||||
match repo.query_create_testimonial(schema).await {
|
||||
Ok(created_testimonial) => {
|
||||
success_created_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: created_testimonial.id.to_raw(),
|
||||
user_id: created_testimonial.user.id.to_raw(),
|
||||
user_fullname: authenticated_user.fullname.clone(),
|
||||
role: created_testimonial.role,
|
||||
content: created_testimonial.content,
|
||||
created_at: created_testimonial.created_at,
|
||||
updated_at: created_testimonial.updated_at,
|
||||
},
|
||||
})
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_testimonial(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
payload: TestimonialsUpdateRequestDto,
|
||||
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
let schema = TestimonialsSchema::update(payload, id, &authenticated_user.id);
|
||||
match repo.query_update_testimonial(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_testimonial(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
_authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_delete_testimonial(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
use super::{
|
||||
testimonials_dto::{
|
||||
TestimonialsCreateRequestDto, TestimonialsDetailItemDto,
|
||||
TestimonialsListItemDto, TestimonialsUpdateRequestDto,
|
||||
},
|
||||
testimonials_repository::TestimonialsRepository,
|
||||
testimonials_schema::TestimonialsSchema,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use imphnen_libs::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
};
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, success_created_response, validate_request,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct TestimonialsService;
|
||||
|
||||
impl TestimonialsService {
|
||||
pub async fn get_testimonial_list(
|
||||
state: &AppState,
|
||||
meta: MetaRequestDto,
|
||||
) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_testimonial_list(meta).await {
|
||||
Ok(data) => {
|
||||
let items: Vec<TestimonialsListItemDto> = data
|
||||
.data
|
||||
.into_iter()
|
||||
.filter(|e| !e.is_deleted)
|
||||
.map(|e| e.from())
|
||||
.collect();
|
||||
let response = ResponseListSuccessDto {
|
||||
data: items,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_testimonial_by_id(state: &AppState, id: Uuid) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_testimonial_by_id(id).await {
|
||||
Ok(testimonial) if !testimonial.is_deleted => {
|
||||
success_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: testimonial.id,
|
||||
user_id: testimonial.user_id,
|
||||
user_fullname: testimonial.user_fullname,
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
created_at: testimonial.created_at,
|
||||
updated_at: testimonial.updated_at,
|
||||
},
|
||||
})
|
||||
}
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Testimonial not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_testimonial(
|
||||
state: &AppState,
|
||||
payload: TestimonialsCreateRequestDto,
|
||||
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
|
||||
match repo.query_create_testimonial(schema).await {
|
||||
Ok(created_testimonial) => {
|
||||
success_created_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: created_testimonial.id,
|
||||
user_id: created_testimonial.user_id,
|
||||
user_fullname: authenticated_user.fullname.clone(),
|
||||
role: created_testimonial.role,
|
||||
content: created_testimonial.content,
|
||||
created_at: created_testimonial.created_at,
|
||||
updated_at: created_testimonial.updated_at,
|
||||
},
|
||||
})
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_testimonial(
|
||||
state: &AppState,
|
||||
id: String,
|
||||
payload: TestimonialsUpdateRequestDto,
|
||||
authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
let schema = TestimonialsSchema::update(payload, id, &authenticated_user.id);
|
||||
match repo.query_update_testimonial(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_testimonial(
|
||||
state: &AppState,
|
||||
id: Uuid,
|
||||
_authenticated_user: &imphnen_iam::UsersDetailQueryDto,
|
||||
) -> Response {
|
||||
let repo = TestimonialsRepository::new(state);
|
||||
match repo.query_delete_testimonial(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pub mod landing;
|
||||
|
||||
pub use landing::events;
|
||||
pub use landing::testimonials;
|
||||
pub use landing::events::events_public_routes;
|
||||
pub use landing::events::events_protected_routes;
|
||||
pub use landing::testimonials::testimonials_public_routes;
|
||||
pub use landing::testimonials::testimonials_protected_routes;
|
||||
pub mod landing;
|
||||
|
||||
pub use landing::events;
|
||||
pub use landing::testimonials;
|
||||
pub use landing::events::events_public_routes;
|
||||
pub use landing::events::events_protected_routes;
|
||||
pub use landing::testimonials::testimonials_public_routes;
|
||||
pub use landing::testimonials::testimonials_protected_routes;
|
||||
|
||||
Reference in New Issue
Block a user