postgress
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld.exe"
|
||||
@@ -47,3 +47,13 @@ MINIO_SECURE=false
|
||||
|
||||
GOOGLE_CLIENT_ID="your_google_client_id"
|
||||
GOOGLE_CLIENT_SECRET="your_google_client_secret"
|
||||
POOL_SIZE=10
|
||||
CONNECT_TIMEOUT=30
|
||||
IDLE_TIMEOUT=60
|
||||
MAX_LIFETIME=1800
|
||||
STATEMENT_TIMEOUT=30000
|
||||
IDLE_IN_TRANSACTION_SESSION_TIMEOUT=60000
|
||||
SSLMODE=require
|
||||
RETRY_ATTEMPTS=3
|
||||
RETRY_DELAY=1
|
||||
GOOGLE_REDIRECT_URL=http://localhost:8000/api/v1/auth/google/callback
|
||||
|
||||
Generated
+1189
-2478
File diff suppressed because it is too large
Load Diff
+6
-4
@@ -3,6 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"tests",
|
||||
"imphnen-entities", # Most basic - core data structures
|
||||
"imphnen-macros", # Macros
|
||||
"imphnen-libs", # Depends on entities
|
||||
"imphnen-utils", # Depends on libs and entities
|
||||
"imphnen-middleware",# Utility for permissions
|
||||
@@ -10,7 +11,6 @@ members = [
|
||||
"imphnen-cms", # Content management, depends on core services
|
||||
"imphnen-gacha", # Game mechanics, depends on core services
|
||||
"imphnen-dimentorin",# Learning platform, depends on core services
|
||||
"imphnen-hackathon", # Hackathon service, depends on core services
|
||||
"imphnen-gateway", # API gateway, depends on all services
|
||||
"imphnen-backend", # Main application, depends on all services
|
||||
]
|
||||
@@ -21,7 +21,7 @@ async-trait = "0.1.83"
|
||||
oauth2 = "5.0.0"
|
||||
reqwest = { version = "0.12.23", features = ["json"] }
|
||||
serde_json = "1.0.142"
|
||||
axum = { version = "0.8.4", features = ["multipart"] }
|
||||
axum = { version = "0.8.4", features = ["multipart", "macros"] }
|
||||
log = "0.4.27"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
tokio = { version = "1.47.1", features = ["full"] }
|
||||
@@ -31,7 +31,6 @@ chrono = "0.4.41"
|
||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||
lettre = { version = "0.11.18", features = ["tokio1-native-tls"] }
|
||||
surrealdb = { version = "2.3.7", features = ["kv-mem", "kv-fdb"] }
|
||||
thiserror = "2.0.14"
|
||||
anyhow = "1.0.99"
|
||||
rand = { version = "0.9.2", features = ["std", "alloc"] }
|
||||
@@ -59,6 +58,8 @@ urlencoding = "2.1"
|
||||
hyper = "1.6.0"
|
||||
hyper-util = "0.1.16"
|
||||
minio = "0.3.0"
|
||||
sea-orm = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros", "with-chrono", "uuid"] }
|
||||
num_cpus = "1.16.0"
|
||||
|
||||
|
||||
tokio-test = "0.4.4"
|
||||
@@ -80,11 +81,12 @@ imphnen-gateway = { path = "./imphnen-gateway" }
|
||||
imphnen-backend = { path = "./imphnen-backend" }
|
||||
imphnen-entities = { path = "./imphnen-entities" }
|
||||
imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||
imphnen-hackathon = { path = "./imphnen-hackathon" }
|
||||
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||
imphnen-macros = { path = "./imphnen-macros" }
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
opt-level = "z"
|
||||
|
||||
|
||||
+19
-5
@@ -7,13 +7,26 @@ services:
|
||||
- "${PORT}:${PORT}"
|
||||
env_file: ".env"
|
||||
depends_on:
|
||||
- surrealdb
|
||||
- postgres
|
||||
|
||||
surrealdb:
|
||||
image: surrealdb/surrealdb:latest
|
||||
command: start --log trace --user root --pass root
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: imphnen_postgres
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-imphnen}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-imphnen}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
@@ -29,4 +42,5 @@ services:
|
||||
- minio_data:/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
|
||||
@@ -8,8 +8,8 @@ name = "api"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "clear_db_test"
|
||||
path = "src/bin/clear_db_test.rs"
|
||||
name = "create_schema"
|
||||
path = "src/bin/create_schema.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seeder"
|
||||
@@ -39,19 +39,16 @@ path = "src/bin/seed_roles.rs"
|
||||
name = "seed_roles_permissions"
|
||||
path = "src/bin/seed_roles_permissions.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_teams"
|
||||
path = "src/bin/seed_teams.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_hackathons"
|
||||
path = "src/bin/seed_hackathons.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seed_test_data"
|
||||
path = "src/bin/seed_test_data.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "test_postgres"
|
||||
path = "src/bin/test_postgres.rs"
|
||||
|
||||
[dependencies]
|
||||
sea-orm.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-gateway.workspace = true
|
||||
@@ -60,7 +57,6 @@ imphnen-iam.workspace = true
|
||||
imphnen-cms.workspace = true
|
||||
imphnen-gacha.workspace = true
|
||||
imphnen-dimentorin.workspace = true
|
||||
imphnen-hackathon.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -69,7 +65,6 @@ 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
|
||||
@@ -80,6 +75,3 @@ env_logger.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid.workspace=true
|
||||
tokio-tungstenite.workspace = true
|
||||
url.workspace = true
|
||||
futures-util.workspace = true
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// API entry point using PostgreSQL (SurrealDB migration complete)
|
||||
// This file has been updated to use SeaORM with PostgreSQL instead of SurrealDB
|
||||
use imphnen_gateway::gateway_service;
|
||||
use imphnen_libs::axum_init;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||
axum_init(|postgres_db| async {
|
||||
// Gateway service now uses PostgreSQL exclusively (SeaORM)
|
||||
// SurrealDB dependencies have been completely removed
|
||||
gateway_service(postgres_db).await
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use sea_orm::{Statement, ConnectionTrait};
|
||||
use std::error::Error;
|
||||
use std::env;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
// New default behavior: execute by default; use --dry-run to preview only.
|
||||
let dry_run = args.iter().any(|s| s == "--dry-run" || s == "--no-exec" || s == "--dry");
|
||||
let force = args.iter().any(|s| s == "--force" || s == "-f");
|
||||
|
||||
println!("🔎 Clear DB script - WARNING: This will remove data from tables\n");
|
||||
println!("Note: script now runs by default (no --yes required). To preview without executing, use --dry-run.\n");
|
||||
|
||||
// List of tables to truncate (order doesn't matter with CASCADE)
|
||||
let tables = vec![
|
||||
"gacha_claims",
|
||||
"gacha_rolls",
|
||||
"gacha_items",
|
||||
"gacha_credits",
|
||||
"audit_logs",
|
||||
"rate_limits",
|
||||
"testimonials",
|
||||
"events",
|
||||
"app_mentors",
|
||||
"app_sessions",
|
||||
"app_roles_permissions",
|
||||
"app_permissions",
|
||||
"app_roles",
|
||||
"app_users",
|
||||
];
|
||||
|
||||
let postgres_config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
// Filter tables that actually exist in the database
|
||||
let mut existing_tables: Vec<&str> = vec![];
|
||||
for t in tables.iter() {
|
||||
let check_sql = format!(
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{}') as exists;",
|
||||
t
|
||||
);
|
||||
let stmt = Statement::from_string(db.get_database_backend(), check_sql);
|
||||
if let Ok(Some(row)) = pg_conn.query_one(stmt).await {
|
||||
let exists_val: Option<bool> = row.try_get("", "exists").ok();
|
||||
if exists_val.unwrap_or(false) {
|
||||
existing_tables.push(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if existing_tables.is_empty() {
|
||||
println!("No configured tables found to clear - nothing to do.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let truncate_sql = format!(
|
||||
"TRUNCATE TABLE {} RESTART IDENTITY CASCADE;",
|
||||
existing_tables.join(", ")
|
||||
);
|
||||
|
||||
println!("The script will run the following SQL (on the DB configured by env vars):\n\n{}", truncate_sql);
|
||||
|
||||
// Prevent accidental execution in production without explicit force flag
|
||||
let env_name = std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string());
|
||||
if env_name == "production" && !force {
|
||||
println!("Security: RUST_ENV=production; the script will NOT run without --force. Use --force to override.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if dry_run {
|
||||
println!("Dry run enabled. No changes applied. To execute, re-run without --dry-run or use --force (in production).");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Executing truncate...\n");
|
||||
|
||||
let postgres_config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
let stmt = Statement::from_string(db.get_database_backend(), truncate_sql);
|
||||
match pg_conn.execute(stmt).await {
|
||||
Ok(_) => println!("✅ Successfully cleared DB tables"),
|
||||
Err(e) => println!("❌ Failed to clear DB tables: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
|
||||
use url::Url;
|
||||
use futures_util::{StreamExt, SinkExt};
|
||||
use serde_json::json;
|
||||
|
||||
// Menentukan kredensial dan detail koneksi secara langsung sebagai string statis
|
||||
static SURREALDB_URL_WS: &str = "ws://localhost:8000/rpc";
|
||||
static SURREALDB_USERNAME: &str = "root";
|
||||
static SURREALDB_PASSWORD: &str = "root";
|
||||
static SURREALDB_NAMESPACE: &str = "test";
|
||||
static SURREALDB_DBNAME: &str = "test";
|
||||
|
||||
// Daftar tabel sebagai variabel static yang tidak dapat diubah
|
||||
static TABLES_TO_CLEAR: &[&str] = &[
|
||||
"app_events", "users", "roles", "permissions", "gacha_rolls",
|
||||
"mentor_users", "gacha_claims", "gacha_credits", "gacha_items",
|
||||
"mentor_profiles", "roles_permissions", "testimonials",
|
||||
];
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Tidak perlu memuat env lagi, karena kita menggunakan nilai hardcoded
|
||||
// imphnen_libs::enviroment::load_env(); // Baris ini tidak lagi dibutuhkan
|
||||
// let env = Env::new(); // Baris ini tidak lagi dibutuhkan
|
||||
|
||||
println!("DEBUG: URL WS: {}", SURREALDB_URL_WS);
|
||||
println!("DEBUG: Username: {}", SURREALDB_USERNAME);
|
||||
println!("DEBUG: Namespace: {}", SURREALDB_NAMESPACE);
|
||||
println!("DEBUG: Database: {}", SURREALDB_DBNAME);
|
||||
|
||||
let url = Url::parse(SURREALDB_URL_WS)?; // Menggunakan SURREALDB_URL_WS statis
|
||||
|
||||
let (ws_stream, _) = connect_async(url.as_str()).await?;
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
// Authenticate (signin)
|
||||
let signin_query = json!({
|
||||
"method": "signin",
|
||||
"params": [{
|
||||
"user": SURREALDB_USERNAME, // Menggunakan SURREALDB_USERNAME statis
|
||||
"pass": SURREALDB_PASSWORD, // Menggunakan SURREALDB_PASSWORD statis
|
||||
}],
|
||||
"id": 1
|
||||
}).to_string();
|
||||
println!("DEBUG: Sending signin query: {}", signin_query);
|
||||
write.send(Message::Text(signin_query.into())).await?;
|
||||
|
||||
let signin_response = read.next().await.ok_or("Failed to read signin response")?;
|
||||
let signin_response_msg = signin_response?;
|
||||
let signin_response_str = signin_response_msg.to_text()?;
|
||||
println!("DEBUG: Signin response: {}", signin_response_str);
|
||||
if signin_response_str.contains("\"error\":") {
|
||||
return Err(format!("Signin failed: {}", signin_response_str).into());
|
||||
}
|
||||
|
||||
// Use namespace and database
|
||||
let use_query = json!({
|
||||
"method": "use",
|
||||
"params": [SURREALDB_NAMESPACE, SURREALDB_DBNAME], // Menggunakan NS & DB statis
|
||||
"id": 2
|
||||
}).to_string();
|
||||
println!("DEBUG: Sending use query: {}", use_query);
|
||||
write.send(Message::Text(use_query.into())).await?;
|
||||
|
||||
let use_response = read.next().await.ok_or("Failed to read use response")?;
|
||||
let use_response_msg = use_response?;
|
||||
let use_response_str = use_response_msg.to_text()?;
|
||||
println!("DEBUG: Use response: {}", use_response_str);
|
||||
if use_response_str.contains("\"error\":") {
|
||||
return Err(format!("USE command failed: {}", use_response_str).into());
|
||||
}
|
||||
|
||||
println!("INFO: Attempting to clear database tables via WebSocket...");
|
||||
|
||||
let mut all_clear = true;
|
||||
for (i, table) in TABLES_TO_CLEAR.iter().enumerate() {
|
||||
let remove_query = format!("REMOVE TABLE {};", table);
|
||||
let query_json = json!({
|
||||
"method": "query",
|
||||
"params": [remove_query],
|
||||
"id": i + 3
|
||||
}).to_string();
|
||||
|
||||
println!("DEBUG: Attempting REMOVE TABLE {}: {}", table, query_json);
|
||||
write.send(Message::Text(query_json.into())).await?;
|
||||
let response_result = read.next().await.ok_or("Stream ended unexpectedly")?;
|
||||
|
||||
|
||||
|
||||
match response_result {
|
||||
Ok(msg) => {
|
||||
let response_str = msg.to_text()?;
|
||||
if response_str.contains("\"error\":") {
|
||||
println!("WARN: Failed to REMOVE TABLE {}: {}. Attempting DELETE type::{}.", table, response_str, table);
|
||||
let delete_all_query = format!("DELETE FROM {};", table);
|
||||
let delete_all_json = json!({
|
||||
"method": "query",
|
||||
"params": [delete_all_query],
|
||||
"id": i + 300
|
||||
}).to_string();
|
||||
|
||||
println!("DEBUG: Attempting DELETE {}: {}", table, delete_all_json);
|
||||
write.send(Message::Text(delete_all_json.into())).await?;
|
||||
let delete_response_result = read.next().await.ok_or("Stream ended unexpectedly during DELETE type::")?;
|
||||
|
||||
match delete_response_result {
|
||||
Ok(delete_msg) => {
|
||||
let delete_response_str = delete_msg.to_text()?;
|
||||
if delete_response_str.contains("\"error\":") {
|
||||
println!("ERROR: Failed to DELETE type::{} : {}", table, delete_response_str);
|
||||
all_clear = false;
|
||||
} else {
|
||||
println!("INFO: Successfully DELETED type:: table: {}", table);
|
||||
|
||||
}
|
||||
},
|
||||
Err(delete_e) => {
|
||||
println!("ERROR: Error receiving response for DELETE type:: table {}: {}", table, delete_e);
|
||||
all_clear = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("INFO: Successfully REMOVED TABLE: {}", table);
|
||||
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("ERROR: Error receiving response for REMOVE TABLE {}: {}", table, e);
|
||||
all_clear = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if table is empty after deletion attempt
|
||||
let select_query = format!("SELECT * FROM {} LIMIT 1;", table);
|
||||
let select_json = json!({
|
||||
"method": "query",
|
||||
"params": [select_query],
|
||||
"id": i + 1000
|
||||
}).to_string();
|
||||
write.send(Message::Text(select_json.into())).await?;
|
||||
let select_response_result = read.next().await.ok_or("Stream ended unexpectedly during SELECT check")?;
|
||||
match select_response_result {
|
||||
Ok(select_msg) => {
|
||||
let select_response_str = select_msg.to_text()?;
|
||||
if select_response_str.contains("does not exist") {
|
||||
println!("CHECK: Table '{}' does not exist after clear attempt (success).", table);
|
||||
} else if select_response_str.contains("\"result\":[]") || select_response_str.contains("\"result\":[[]]") {
|
||||
println!("CHECK: Table '{}' is empty after clear attempt.", table);
|
||||
} else {
|
||||
println!("WARNING: Table '{}' is NOT empty after clear attempt! Response: {}", table, select_response_str);
|
||||
all_clear = false;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("ERROR: Error receiving response for SELECT check on table {}: {}", table, e);
|
||||
all_clear = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("INFO: Database clearing complete.");
|
||||
|
||||
if !all_clear {
|
||||
eprintln!("ERROR: One or more tables could not be cleared. Check logs for details.");
|
||||
return Err("Database clearing failed for one or more tables.".into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use sea_orm::{ConnectionTrait, Database, Schema, DbBackend, EntityTrait};
|
||||
use imphnen_libs::postgres::PostgresConfig;
|
||||
use imphnen_entities::seaorm::{auth, common, gacha};
|
||||
use sea_orm::sea_query::Table;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🛠️ Creating database schema...");
|
||||
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let db = Database::connect(&config.database_url).await?;
|
||||
let builder = db.get_database_backend();
|
||||
|
||||
println!(" Database connected. Creating/updating tables...");
|
||||
|
||||
// Dropping and recreating tables to ensure schema is up-to-date
|
||||
// This is safer for development/testing environments to prevent schema drift.
|
||||
drop_and_create_table(&db, builder, "app_roles", auth::roles::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "app_permissions", auth::permissions::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "app_users", auth::users::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "app_roles_permissions", auth::roles_permissions::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "app_mentors", auth::mentors::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "app_sessions", auth::sessions::Entity).await?;
|
||||
|
||||
drop_and_create_table(&db, builder, "events", common::events::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "testimonials", common::testimonials::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "audit_logs", common::audit_log::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "rate_limits", common::rate_limit::Entity).await?;
|
||||
|
||||
drop_and_create_table(&db, builder, "gacha_credits", gacha::gacha_credits::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "gacha_items", gacha::gacha_items::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "gacha_rolls", gacha::gacha_rolls::Entity).await?;
|
||||
drop_and_create_table(&db, builder, "gacha_claims", gacha::gacha_claims::Entity).await?;
|
||||
|
||||
println!("✅ Schema creation completed.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn drop_and_create_table<E>(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
builder: DbBackend,
|
||||
name: &str,
|
||||
entity: E,
|
||||
) -> Result<(), Box<dyn std::error::Error>> // Return Result
|
||||
where
|
||||
E: EntityTrait,
|
||||
{
|
||||
let schema = Schema::new(builder);
|
||||
|
||||
// Drop table if it exists
|
||||
let drop_stmt = Table::drop().table(entity).if_exists().cascade().to_owned(); // Added .cascade()
|
||||
db.execute(builder.build(&drop_stmt)).await?; // Propagate error
|
||||
println!(" Dropped table if exists: {}", name);
|
||||
|
||||
// Create table
|
||||
let mut create_stmt = schema.create_table_from_entity(entity);
|
||||
create_stmt.if_not_exists();
|
||||
|
||||
db.execute(builder.build(&create_stmt)).await?; // Propagate error
|
||||
println!(" ✅ Created table: {}", name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_libs::jsonwebtoken::encode_access_token;
|
||||
use std::env;
|
||||
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
||||
use imphnen_utils::{get_iso_date};
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::common::events::{ActiveModel as EventsActiveModel, Entity as EventEntity};
|
||||
use sea_orm::{ActiveValue::Set, ActiveModelTrait, EntityTrait, ColumnTrait, QueryFilter};
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc; // Removed NaiveDateTime as it was unused
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let postgres_config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
let events = vec![
|
||||
(
|
||||
@@ -58,6 +54,97 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"2025-09-20T13:00:00Z",
|
||||
"2025-09-22T15:00:00Z",
|
||||
),
|
||||
// Additional Events
|
||||
(
|
||||
"Rust Programming Bootcamp",
|
||||
"Intensive 3-day bootcamp to master Rust fundamentals and advanced concepts.",
|
||||
"https://rust-bootcamp.example.com",
|
||||
200.0,
|
||||
Some("Bandung Digital Valley".to_string()),
|
||||
false,
|
||||
"2025-10-01T09:00:00Z",
|
||||
"2025-10-03T17:00:00Z",
|
||||
),
|
||||
(
|
||||
"AI & Machine Learning Summit",
|
||||
"Global summit discussing the future of AI and its impact on industries.",
|
||||
"https://ai-summit.example.com",
|
||||
300.0,
|
||||
Some("Bali Nusa Dua Convention Center".to_string()),
|
||||
false,
|
||||
"2025-11-15T08:00:00Z",
|
||||
"2025-11-17T18:00:00Z",
|
||||
),
|
||||
(
|
||||
"Cybersecurity Awareness Webinar",
|
||||
"Free webinar on best practices for personal and corporate cybersecurity.",
|
||||
"https://cybersecurity-webinar.example.com",
|
||||
0.0,
|
||||
None,
|
||||
true,
|
||||
"2025-12-05T14:00:00Z",
|
||||
"2025-12-05T16:00:00Z",
|
||||
),
|
||||
(
|
||||
"Cloud Computing Workshop",
|
||||
"Hands-on workshop on deploying scalable applications using AWS and Azure.",
|
||||
"https://cloud-workshop.example.com",
|
||||
120.0,
|
||||
None,
|
||||
true,
|
||||
"2026-01-20T10:00:00Z",
|
||||
"2026-01-22T15:00:00Z",
|
||||
),
|
||||
(
|
||||
"Blockchain for Finance",
|
||||
"Exploring the applications of blockchain technology in the financial sector.",
|
||||
"https://blockchain-finance.example.com",
|
||||
180.0,
|
||||
Some("Jakarta Ritz-Carlton".to_string()),
|
||||
false,
|
||||
"2026-02-10T09:00:00Z",
|
||||
"2026-02-11T17:00:00Z",
|
||||
),
|
||||
(
|
||||
"Game Development Jam",
|
||||
"48-hour game development marathon for indie developers.",
|
||||
"https://game-jam.example.com",
|
||||
50.0,
|
||||
Some("Yogyakarta Creative Hub".to_string()),
|
||||
false,
|
||||
"2026-03-15T18:00:00Z",
|
||||
"2026-03-17T18:00:00Z",
|
||||
),
|
||||
(
|
||||
"UX/UI Design Principles",
|
||||
"Masterclass on creating intuitive and user-friendly interfaces.",
|
||||
"https://uxui-design.example.com",
|
||||
90.0,
|
||||
None,
|
||||
true,
|
||||
"2026-04-05T13:00:00Z",
|
||||
"2026-04-07T16:00:00Z",
|
||||
),
|
||||
(
|
||||
"Data Science Fundamentals",
|
||||
"Introduction to data analysis, visualization, and statistical modeling.",
|
||||
"https://data-science.example.com",
|
||||
110.0,
|
||||
None,
|
||||
true,
|
||||
"2026-05-12T10:00:00Z",
|
||||
"2026-05-14T15:00:00Z",
|
||||
),
|
||||
(
|
||||
"IoT Innovation Expo",
|
||||
"Showcase of the latest Internet of Things devices and solutions.",
|
||||
"https://iot-expo.example.com",
|
||||
50.0,
|
||||
Some("Surabaya Expo Center".to_string()),
|
||||
false,
|
||||
"2026-06-20T09:00:00Z",
|
||||
"2026-06-22T18:00:00Z",
|
||||
),
|
||||
];
|
||||
|
||||
for (
|
||||
@@ -67,29 +154,34 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
price,
|
||||
location,
|
||||
is_online,
|
||||
start_date,
|
||||
end_date,
|
||||
start_date_str, // Renamed to avoid conflict
|
||||
end_date_str, // Renamed to avoid conflict
|
||||
) in events
|
||||
{
|
||||
let uuid = Uuid::new_v4().to_string(); // Generate new UUID
|
||||
let event = EventsSchema {
|
||||
id: Thing::from(("app_events", uuid.as_str())), // Use generated UUID
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
detail_link: detail_link.into(),
|
||||
price,
|
||||
location,
|
||||
is_online,
|
||||
is_deleted: false,
|
||||
start_date: start_date.into(),
|
||||
end_date: end_date.into(),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
// Check if event already exists by name
|
||||
let existing = EventEntity::find().filter(<EventEntity as EntityTrait>::Column::Name.eq(name)).one(db).await?;
|
||||
if existing.is_some() {
|
||||
println!("ℹ️ Skipping (already exists): {name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
db.create::<Option<EventsSchema>>(("app_events", uuid.as_str())) // Use generated UUID
|
||||
.content(event)
|
||||
.await?;
|
||||
let uuid = Uuid::new_v4(); // Generate a Uuid
|
||||
let mut event_model: EventsActiveModel = Default::default();
|
||||
event_model.id = Set(uuid);
|
||||
event_model.name = Set(name.to_string());
|
||||
event_model.description = Set(description.to_string());
|
||||
event_model.detail_link = Set(detail_link.to_string());
|
||||
event_model.price = Set(price);
|
||||
event_model.is_online = Set(is_online);
|
||||
event_model.location = Set(location.clone());
|
||||
event_model.start_date = Set(chrono::DateTime::parse_from_rfc3339(start_date_str)?.with_timezone(&chrono::Utc));
|
||||
event_model.end_date = Set(chrono::DateTime::parse_from_rfc3339(end_date_str)?.with_timezone(&chrono::Utc));
|
||||
event_model.is_deleted = Set(false); // Explicitly set is_deleted
|
||||
event_model.created_at = Set(Utc::now()); // Explicitly set created_at
|
||||
event_model.updated_at = Set(Utc::now()); // Explicitly set updated_at
|
||||
|
||||
|
||||
event_model.insert(db).await?;
|
||||
|
||||
println!(
|
||||
"✅ Inserted event: {} ({})",
|
||||
|
||||
@@ -1,51 +1,78 @@
|
||||
use imphnen_utils::{get_iso_date};
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::sql::Thing;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::gacha::gacha_items::ActiveModel as GachaItemActiveModel;
|
||||
use imphnen_entities::seaorm::gacha::gacha_rolls::ActiveModel as GachaRollActiveModel;
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use uuid::Uuid;
|
||||
use sea_orm::ConnectionTrait;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
db.query("DELETE type::thing('app_gacha_items', $id)")
|
||||
.bind(("id", "1"))
|
||||
.await?;
|
||||
db.query("DELETE type::thing('app_gacha_rolls', $id)")
|
||||
.bind(("id", "test-gacha-roll-001"))
|
||||
.await?;
|
||||
let gacha_item_id = "1";
|
||||
db.query("CREATE type::thing('app_gacha_items', $id) SET name = $name, image_url = $image_url, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", gacha_item_id))
|
||||
.bind(("name", "Test Gacha Item"))
|
||||
.bind(("image_url", "https://example.com/gacha_item.png"))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
// Check if gacha item already exists
|
||||
let check_item_sql = "SELECT id FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1' LIMIT 1";
|
||||
let item_result = pg_conn.query_one(sea_orm::Statement::from_string(db.get_database_backend(), check_item_sql)).await?;
|
||||
let gacha_item_uuid = if let Some(ref row) = item_result {
|
||||
// Item exists, get its ID
|
||||
row.try_get("", "id")?
|
||||
} else {
|
||||
// Item doesn't exist, create it
|
||||
// Note: We can't easily delete by a fixed ID since it's a UUID, but the insert will fail if there's a conflict
|
||||
let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), "DELETE FROM app_gacha_items WHERE item_code = 'ITEM_TEST_1'".to_string())).await.ok();
|
||||
|
||||
// Create gacha item via SeaORM
|
||||
let new_uuid = Uuid::new_v4();
|
||||
let mut item_model: GachaItemActiveModel = Default::default();
|
||||
item_model.id = Set(new_uuid);
|
||||
item_model.item_code = Set("ITEM_TEST_1".to_string());
|
||||
item_model.name = Set("Test Gacha Item".to_string());
|
||||
item_model.description = Set("Test item for gacha".to_string());
|
||||
item_model.rarity = Set("common".to_string());
|
||||
item_model.type_ = Set("item".to_string());
|
||||
item_model.category = Set("test".to_string());
|
||||
item_model.value = Set(1);
|
||||
item_model.weight = Set(1.0);
|
||||
item_model.stock = Set(10);
|
||||
item_model.is_limited = Set(false);
|
||||
item_model.created_at = Set(chrono::Utc::now());
|
||||
item_model.updated_at = Set(chrono::Utc::now());
|
||||
item_model.insert(db).await?;
|
||||
println!("Gacha Item seeded successfully!");
|
||||
new_uuid
|
||||
};
|
||||
|
||||
let gacha_roll_id = "test-gacha-roll-001";
|
||||
db.query("CREATE type::thing('app_gacha_rolls', $id) SET item = $item, quantity = $quantity, weight = $weight, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", gacha_roll_id))
|
||||
.bind(("item", Thing::from(("app_gacha_items", gacha_item_id))))
|
||||
.bind(("quantity", 10))
|
||||
.bind(("weight", 1.0))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
// Always try to insert the roll, relying on the database constraints to prevent duplicates if needed
|
||||
let gacha_roll_id = Uuid::new_v4();
|
||||
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||
roll_model.id = Set(gacha_roll_id);
|
||||
roll_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
roll_model.gacha_id = Set(Uuid::new_v4().to_string());
|
||||
roll_model.item_id = Set(gacha_item_uuid);
|
||||
roll_model.weight = Set(1.0);
|
||||
roll_model.quantity = Set(10);
|
||||
roll_model.is_deleted = Set(false);
|
||||
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||
roll_model.insert(db).await?;
|
||||
println!("Gacha Roll seeded successfully!");
|
||||
|
||||
let gacha_roll_id = Uuid::new_v4();
|
||||
let mut roll_model: GachaRollActiveModel = Default::default();
|
||||
roll_model.id = Set(gacha_roll_id);
|
||||
roll_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
roll_model.gacha_id = Set(Uuid::new_v4().to_string());
|
||||
roll_model.item_id = Set(gacha_item_uuid);
|
||||
roll_model.weight = Set(1.0);
|
||||
roll_model.quantity = Set(10);
|
||||
roll_model.is_deleted = Set(false);
|
||||
roll_model.created_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||
roll_model.updated_at = Set(Some(chrono::Utc::now().naive_utc()));
|
||||
roll_model.insert(db).await?;
|
||||
println!("✅ Gacha items and rolls seeded.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
|
||||
HackathonSchema, HackathonEventsSchema, HackathonTimelineSchema, HackathonSubmissionsSchema,
|
||||
HackathonStatus, HackathonEventType, HackathonPhase, SubmissionStatus, Prize
|
||||
};
|
||||
use imphnen_utils::get_iso_date;
|
||||
use std::error::Error;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
|
||||
// Sample hackathon data
|
||||
let hackathons = vec![
|
||||
(
|
||||
"hackathon-001",
|
||||
"AI Innovation Challenge 2025",
|
||||
"Build the next generation of AI-powered applications that solve real-world problems.",
|
||||
"2025-10-15T09:00:00Z",
|
||||
"2025-10-17T18:00:00Z",
|
||||
"2025-10-01T23:59:59Z",
|
||||
Some(100),
|
||||
HackathonStatus::RegistrationOpen,
|
||||
Some("Artificial Intelligence & Machine Learning".to_string()),
|
||||
Some("1. All code must be original\n2. Teams can have 2-5 members\n3. Projects must use AI/ML technologies".to_string()),
|
||||
Some(vec![
|
||||
Prize { position: 1, title: "Grand Prize".to_string(), description: Some("Winner gets full scholarship".to_string()), value: Some("$10,000".to_string()) },
|
||||
Prize { position: 2, title: "Second Place".to_string(), description: Some("Runner-up prize".to_string()), value: Some("$5,000".to_string()) },
|
||||
Prize { position: 3, title: "Third Place".to_string(), description: Some("Third place prize".to_string()), value: Some("$2,500".to_string()) },
|
||||
]),
|
||||
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()], // admin user
|
||||
),
|
||||
(
|
||||
"hackathon-002",
|
||||
"Green Tech Hackathon",
|
||||
"Develop sustainable technology solutions for environmental challenges.",
|
||||
"2025-11-20T10:00:00Z",
|
||||
"2025-11-22T17:00:00Z",
|
||||
"2025-11-05T23:59:59Z",
|
||||
Some(75),
|
||||
HackathonStatus::Draft,
|
||||
Some("Sustainability & Green Technology".to_string()),
|
||||
Some("Focus on renewable energy, waste reduction, and environmental monitoring.".to_string()),
|
||||
Some(vec![
|
||||
Prize { position: 1, title: "Eco Champion".to_string(), description: Some("Best environmental impact".to_string()), value: Some("$7,500".to_string()) },
|
||||
Prize { position: 2, title: "Innovation Award".to_string(), description: Some("Most innovative solution".to_string()), value: Some("$3,500".to_string()) },
|
||||
]),
|
||||
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()],
|
||||
),
|
||||
];
|
||||
|
||||
// Sample hackathon events
|
||||
let hackathon_events = vec![
|
||||
(
|
||||
"hackathon-001",
|
||||
"event-001",
|
||||
"Opening Ceremony",
|
||||
Some("Welcome and kickoff event for the AI Innovation Challenge".to_string()),
|
||||
HackathonEventType::Ceremony,
|
||||
"2025-10-15T09:00:00Z",
|
||||
"2025-10-15T10:00:00Z",
|
||||
Some("Main Auditorium".to_string()),
|
||||
None,
|
||||
Some(150),
|
||||
true,
|
||||
),
|
||||
(
|
||||
"hackathon-001",
|
||||
"event-002",
|
||||
"AI Workshop: Getting Started",
|
||||
Some("Introduction to AI frameworks and tools".to_string()),
|
||||
HackathonEventType::Workshop,
|
||||
"2025-10-15T14:00:00Z",
|
||||
"2025-10-15T16:00:00Z",
|
||||
None,
|
||||
Some("https://zoom.us/meeting/ai-workshop".to_string()),
|
||||
Some(80),
|
||||
false,
|
||||
),
|
||||
(
|
||||
"hackathon-001",
|
||||
"event-003",
|
||||
"Judging Session",
|
||||
Some("Final project presentations and judging".to_string()),
|
||||
HackathonEventType::Judging,
|
||||
"2025-10-17T14:00:00Z",
|
||||
"2025-10-17T17:00:00Z",
|
||||
Some("Innovation Lab".to_string()),
|
||||
None,
|
||||
Some(100),
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
// Sample hackathon timeline
|
||||
let hackathon_timeline = vec![
|
||||
(
|
||||
"hackathon-001",
|
||||
HackathonPhase::Registration,
|
||||
"Registration Phase",
|
||||
Some("Register your team and submit initial project ideas".to_string()),
|
||||
"2025-10-01T00:00:00Z",
|
||||
"2025-10-10T23:59:59Z",
|
||||
true,
|
||||
1,
|
||||
),
|
||||
(
|
||||
"hackathon-001",
|
||||
HackathonPhase::Ideation,
|
||||
"Ideation & Planning",
|
||||
Some("Brainstorm and plan your AI solution".to_string()),
|
||||
"2025-10-11T00:00:00Z",
|
||||
"2025-10-14T23:59:59Z",
|
||||
false,
|
||||
2,
|
||||
),
|
||||
(
|
||||
"hackathon-001",
|
||||
HackathonPhase::Development,
|
||||
"Development Sprint",
|
||||
Some("Build your AI-powered application".to_string()),
|
||||
"2025-10-15T00:00:00Z",
|
||||
"2025-10-16T23:59:59Z",
|
||||
false,
|
||||
3,
|
||||
),
|
||||
(
|
||||
"hackathon-001",
|
||||
HackathonPhase::Submission,
|
||||
"Project Submission",
|
||||
Some("Submit your final project and demo video".to_string()),
|
||||
"2025-10-17T00:00:00Z",
|
||||
"2025-10-17T12:00:00Z",
|
||||
false,
|
||||
4,
|
||||
),
|
||||
(
|
||||
"hackathon-001",
|
||||
HackathonPhase::Judging,
|
||||
"Judging & Awards",
|
||||
Some("Presentations and prize ceremony".to_string()),
|
||||
"2025-10-17T13:00:00Z",
|
||||
"2025-10-17T18:00:00Z",
|
||||
false,
|
||||
5,
|
||||
),
|
||||
];
|
||||
|
||||
// Sample hackathon submissions
|
||||
let hackathon_submissions = vec![
|
||||
(
|
||||
"hackathon-001",
|
||||
"team-dev-001",
|
||||
"AI-Powered Health Monitor",
|
||||
"A machine learning application that predicts health risks using wearable device data.",
|
||||
Some("https://github.com/team-dev/ai-health-monitor".to_string()),
|
||||
Some("https://demo.ai-health-monitor.com".to_string()),
|
||||
None,
|
||||
vec!["Python".to_string(), "TensorFlow".to_string(), "React".to_string()],
|
||||
SubmissionStatus::Submitted,
|
||||
"2025-10-17T11:30:00Z",
|
||||
),
|
||||
(
|
||||
"hackathon-001",
|
||||
"team-design-001",
|
||||
"Smart City Traffic Optimizer",
|
||||
"AI system that optimizes traffic flow using computer vision and predictive analytics.",
|
||||
Some("https://github.com/team-design/smart-traffic".to_string()),
|
||||
Some("https://demo.smart-traffic.com".to_string()),
|
||||
Some("https://slides.smart-traffic.com/presentation".to_string()),
|
||||
vec!["JavaScript".to_string(), "Node.js".to_string(), "OpenCV".to_string()],
|
||||
SubmissionStatus::UnderReview,
|
||||
"2025-10-17T10:45:00Z",
|
||||
),
|
||||
];
|
||||
|
||||
// Seed hackathons
|
||||
for (
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
start_date,
|
||||
end_date,
|
||||
registration_deadline,
|
||||
max_participants,
|
||||
status,
|
||||
theme,
|
||||
rules,
|
||||
prizes,
|
||||
organizers,
|
||||
) in hackathons {
|
||||
db.query("DELETE type::thing('app_hackathons', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
|
||||
let hackathon = HackathonSchema {
|
||||
id: Thing::from(("app_hackathons", id)),
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
||||
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
|
||||
max_participants,
|
||||
status,
|
||||
theme,
|
||||
rules,
|
||||
prizes,
|
||||
previous_winners: None,
|
||||
organizers,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
db.create::<Option<HackathonSchema>>(("app_hackathons", id))
|
||||
.content(hackathon)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted hackathon: {name}");
|
||||
}
|
||||
|
||||
// Seed hackathon events
|
||||
for (
|
||||
hackathon_id,
|
||||
event_id,
|
||||
title,
|
||||
description,
|
||||
event_type,
|
||||
start_time,
|
||||
end_time,
|
||||
location,
|
||||
virtual_link,
|
||||
max_attendees,
|
||||
is_mandatory,
|
||||
) in hackathon_events {
|
||||
db.query("DELETE type::thing('app_hackathon_events', $id)")
|
||||
.bind(("id", event_id))
|
||||
.await?;
|
||||
|
||||
let event = HackathonEventsSchema {
|
||||
id: Thing::from(("app_hackathon_events", event_id)),
|
||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
||||
title: title.into(),
|
||||
description,
|
||||
event_type,
|
||||
start_time: DateTime::parse_from_rfc3339(start_time)?.with_timezone(&Utc),
|
||||
end_time: DateTime::parse_from_rfc3339(end_time)?.with_timezone(&Utc),
|
||||
location,
|
||||
virtual_link,
|
||||
max_attendees,
|
||||
is_mandatory,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
db.create::<Option<HackathonEventsSchema>>(("app_hackathon_events", event_id))
|
||||
.content(event)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted hackathon event: {title}");
|
||||
}
|
||||
|
||||
// Seed hackathon timeline
|
||||
for (
|
||||
hackathon_id,
|
||||
phase,
|
||||
title,
|
||||
description,
|
||||
start_date,
|
||||
end_date,
|
||||
is_active,
|
||||
order,
|
||||
) in hackathon_timeline {
|
||||
let timeline_id = format!("timeline-{}-{}", hackathon_id, order);
|
||||
|
||||
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
|
||||
.bind(("id", timeline_id.clone()))
|
||||
.await?;
|
||||
|
||||
let timeline = HackathonTimelineSchema {
|
||||
id: Thing::from(("app_hackathon_timeline", timeline_id.as_str())),
|
||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
||||
phase,
|
||||
title: title.into(),
|
||||
description,
|
||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
||||
is_active,
|
||||
order,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", timeline_id))
|
||||
.content(timeline)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted hackathon timeline: {title}");
|
||||
}
|
||||
|
||||
// Seed hackathon submissions
|
||||
for (
|
||||
hackathon_id,
|
||||
team_id,
|
||||
project_name,
|
||||
description,
|
||||
repository_url,
|
||||
demo_url,
|
||||
slides_url,
|
||||
technologies,
|
||||
submission_status,
|
||||
submitted_at,
|
||||
) in hackathon_submissions {
|
||||
let submission_id = format!("submission-{}-{}", hackathon_id, team_id);
|
||||
|
||||
db.query("DELETE type::thing('app_hackathon_submissions', $id)")
|
||||
.bind(("id", submission_id.clone()))
|
||||
.await?;
|
||||
|
||||
let submission = HackathonSubmissionsSchema {
|
||||
id: Thing::from(("app_hackathon_submissions", submission_id.as_str())),
|
||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
||||
judge_feedback: None,
|
||||
team_id: Some(Thing::from(("app_teams", team_id))),
|
||||
project_name: Some(project_name.into()),
|
||||
description: Some(description.into()),
|
||||
repository_url,
|
||||
upload_file_url: None,
|
||||
demo_url,
|
||||
slides_url,
|
||||
technologies: Some(technologies),
|
||||
contact_instagram: None,
|
||||
contact_twitter: None,
|
||||
contact_linkedin: None,
|
||||
contact_facebook: None,
|
||||
contact_youtube: None,
|
||||
contact_tiktok: None,
|
||||
contact_other: None,
|
||||
submission_status: Some(submission_status),
|
||||
submitted_at: Some(DateTime::parse_from_rfc3339(submitted_at)?.with_timezone(&Utc)),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
db.create::<Option<HackathonSubmissionsSchema>>(("app_hackathon_submissions", submission_id))
|
||||
.content(submission)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted hackathon submission: {project_name}");
|
||||
}
|
||||
|
||||
println!("✅ All Hackathons seeded");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,92 +1,70 @@
|
||||
use imphnen_utils::{get_iso_date, hash_password};
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_libs::hash_password;
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
||||
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||
use imphnen_entities::seaorm::auth::roles::{Entity as RoleEntity, Column as RoleColumn};
|
||||
use sea_orm::{ActiveModelTrait, ConnectionTrait, ActiveValue::Set, EntityTrait, QueryFilter, ColumnTrait};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
db.query("DELETE type::thing('app_mentors', $id)")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.await?;
|
||||
let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), "DELETE FROM app_mentors WHERE id = 'e6f78d23-83bf-5c2b-bcd4-001345678901'".to_string())).await.ok();
|
||||
let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), "DELETE FROM app_users WHERE email = 'mentor@example.com'".to_string())).await.ok();
|
||||
|
||||
db.query("DELETE type::thing('app_users', $id)")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.await?;
|
||||
// Find Mentor role
|
||||
let role = RoleEntity::find()
|
||||
.filter(RoleColumn::Name.eq("Mentor"))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or("Role 'Mentor' not found")?;
|
||||
|
||||
use surrealdb::sql::Thing;
|
||||
db.query("CREATE type::thing('app_users', $id) SET fullname = $fullname, email = $email, password = $password, avatar = $avatar, phone_number = $phone_number, is_active = $is_active, is_deleted = $is_deleted, mentor_id = $mentor_id, gender = $gender, birthdate = $birthdate, role = $role, legal_name = $legal_name, domicile = $domicile, identity_document_url = $identity_document_url, phone_for_verification = $phone_for_verification, bio = $bio, last_education = $last_education, linkedin_url = $linkedin_url, github_url = $github_url, cv_url = $cv_url, portfolio_url = $portfolio_url, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.bind(("fullname", "Mentor User"))
|
||||
.bind(("email", "mentor@example.com"))
|
||||
.bind(("password", hash_password("password").unwrap()))
|
||||
.bind(("avatar", Option::<String>::None))
|
||||
.bind(("phone_number", "081234567890"))
|
||||
.bind(("is_active", true))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("mentor_id", Option::<Thing>::None))
|
||||
.bind(("gender", "male"))
|
||||
.bind(("birthdate", "1990-05-15"))
|
||||
.bind(("role", Thing::from(("app_roles", "3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a"))))
|
||||
.bind(("legal_name", "Mentor User"))
|
||||
.bind(("domicile", "Jakarta, Indonesia"))
|
||||
// .bind(("identity_document_url", "https://example.com/ktp.jpg"))
|
||||
.bind(("phone_for_verification", "081234567890"))
|
||||
.bind(("bio", "Saya adalah mentor backend Rust dengan pengalaman 5 tahun dalam pengembangan aplikasi backend yang scalable dan performant."))
|
||||
.bind(("last_education", "S1 Teknik Informatika"))
|
||||
.bind(("linkedin_url", "https://linkedin.com/in/mentor"))
|
||||
.bind(("github_url", "https://github.com/mentor"))
|
||||
.bind(("cv_url", Option::<String>::None))
|
||||
.bind(("portfolio_url", Option::<String>::None))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
// Insert user with Mentor role
|
||||
let user_id = Uuid::new_v4();
|
||||
let mut user_model: UsersActiveModel = Default::default();
|
||||
user_model.id = Set(user_id);
|
||||
user_model.email = Set("mentor@example.com".to_string());
|
||||
user_model.password_hash = Set(hash_password("password").unwrap());
|
||||
user_model.username = Set("mentor@example.com".to_string());
|
||||
user_model.first_name = Set(Some("Mentor".to_string()));
|
||||
user_model.last_name = Set(Some("User".to_string()));
|
||||
user_model.avatar_url = Set(Some("https://example.com/avatar.jpg".to_string()));
|
||||
user_model.is_active = Set(true);
|
||||
user_model.is_verified = Set(true);
|
||||
user_model.role_id = Set(Some(role.id));
|
||||
user_model.created_at = Set(chrono::Utc::now());
|
||||
user_model.updated_at = Set(chrono::Utc::now());
|
||||
user_model.insert(db).await?;
|
||||
|
||||
db.query("CREATE type::thing('app_mentors', $id) SET user_id = $user_id, industries = $industries, expertise = $expertise, languages = $languages, current_company = $current_company, current_role = $current_role, years_of_experience = $years_of_experience, topics_of_interest = $topics_of_interest, preferred_mentee_level = $preferred_mentee_level, preferred_mentoring_formats = $preferred_mentoring_formats, availability_commitment = $availability_commitment, mentoring_rate = $mentoring_rate, status = $status, is_deleted = $is_deleted, created_at = $created_at, updated_at = $updated_at")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.bind(("user_id", Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))))
|
||||
.bind(("industries", vec!["Software", "Education"]))
|
||||
.bind(("expertise", vec!["Rust", "Microservices"]))
|
||||
.bind(("languages", vec!["Indonesian", "English"]))
|
||||
.bind(("current_company", "PT Contoh"))
|
||||
.bind(("current_role", "Senior Backend Engineer"))
|
||||
.bind(("years_of_experience", 5))
|
||||
.bind(("topics_of_interest", vec!["Rust Programming", "Backend Development"]))
|
||||
.bind(("preferred_mentee_level", vec!["beginner", "intermediate"]))
|
||||
.bind(("preferred_mentoring_formats", vec!["online", "offline"]))
|
||||
.bind(("availability_commitment", "2 jam per minggu untuk mentoring online dan offline"))
|
||||
.bind(("mentoring_rate", json!({
|
||||
"amount": 100000,
|
||||
"currency": "IDR",
|
||||
"per_duration": "hour"
|
||||
})))
|
||||
.bind(("status", "verified"))
|
||||
.bind(("is_deleted", false))
|
||||
.bind(("created_at", get_iso_date()))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
// Insert mentor
|
||||
let mentor_id = Uuid::new_v4();
|
||||
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||
mentor_model.id = Set(mentor_id);
|
||||
mentor_model.user_id = Set(user_id);
|
||||
mentor_model.industries = Set(Some(json!( ["Software", "Education"] )));
|
||||
mentor_model.expertise = Set(Some(json!( ["Rust", "Microservices"] )));
|
||||
mentor_model.languages = Set(Some(json!( ["Indonesian", "English"] )));
|
||||
mentor_model.current_company = Set(Some("PT Contoh".to_string()));
|
||||
mentor_model.current_role = Set(Some("Senior Backend Engineer".to_string()));
|
||||
mentor_model.years_of_experience = Set(Some(5));
|
||||
mentor_model.topics_of_interest = Set(Some(json!( ["Rust Programming", "Backend Development"] )));
|
||||
mentor_model.preferred_mentee_level = Set(Some("beginner".to_string()));
|
||||
mentor_model.preferred_mentoring_formats = Set(Some(json!( ["online", "offline"] )));
|
||||
mentor_model.availability_commitment = Set(Some("2 jam per minggu untuk mentoring online dan offline".to_string()));
|
||||
mentor_model.mentoring_rate = Set(Some(100000.0));
|
||||
mentor_model.status = Set(Some("verified".to_string()));
|
||||
mentor_model.is_deleted = Set(false);
|
||||
mentor_model.created_at = Set(chrono::Utc::now());
|
||||
mentor_model.updated_at = Set(chrono::Utc::now());
|
||||
mentor_model.insert(db).await?;
|
||||
println!("Mentor created successfully!");
|
||||
println!("Updating user with mentor_id...");
|
||||
|
||||
db.query("UPDATE type::thing('app_users', $id) SET mentor_id = $mentor_id")
|
||||
.bind(("id", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.bind((
|
||||
"mentor_id",
|
||||
Thing::from(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901")),
|
||||
))
|
||||
.await?;
|
||||
println!("User updated with mentor_id successfully!");
|
||||
|
||||
println!("✅ Inserted mentor user: mentor@example.com");
|
||||
println!("✅ Mentor user seeded");
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_iam::PermissionsEnum;
|
||||
use imphnen_utils::{get_iso_date};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::auth::permissions::ActiveModel as PermissionActiveModel;
|
||||
use imphnen_entities::seaorm::auth::permissions::Entity as PermissionEntity;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::{ActiveModelTrait};
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
for permission in [
|
||||
PermissionsEnum::ReadListUsers,
|
||||
@@ -56,18 +54,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
PermissionsEnum::DeleteMentors,
|
||||
PermissionsEnum::Administrator,
|
||||
] {
|
||||
db.query("CREATE type::thing('app_permissions', $id) CONTENT $data")
|
||||
.bind(("id", permission.id()))
|
||||
.bind((
|
||||
"data",
|
||||
json!({
|
||||
"name": permission.to_string(),
|
||||
"is_deleted": false,
|
||||
"created_at": get_iso_date(),
|
||||
"updated_at": get_iso_date()
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
// permission.id() returns a string, try parse to uuid
|
||||
let parsed_id = Uuid::parse_str(&permission.id()).unwrap_or_else(|_| Uuid::new_v4());
|
||||
|
||||
// Check if permission already exists
|
||||
let existing = PermissionEntity::find_by_id(parsed_id).one(db).await?;
|
||||
if existing.is_some() {
|
||||
println!("ℹ️ Skipping (already exists): {permission}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Insert permission using active model
|
||||
let mut perm_model: PermissionActiveModel = Default::default();
|
||||
perm_model.id = Set(parsed_id);
|
||||
perm_model.name = Set(permission.to_string());
|
||||
perm_model.is_deleted = Set(false);
|
||||
perm_model.created_at = Set(Utc::now());
|
||||
perm_model.updated_at = Set(Utc::now());
|
||||
perm_model.insert(db).await?;
|
||||
println!("✅ Inserted: {permission}");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
use imphnen_utils::{get_iso_date};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::auth::roles::{RoleBuilder, Entity as RoleEntity};
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait};
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc; // Added chrono
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
let roles = vec![
|
||||
(
|
||||
@@ -55,23 +50,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
),
|
||||
];
|
||||
|
||||
for (id, name, _created_at, _updated_at) in roles {
|
||||
db.query("DELETE type::thing('app_roles', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
db.query("CREATE type::thing('app_roles', $id) CONTENT $data")
|
||||
.bind(("id", id))
|
||||
.bind((
|
||||
"data",
|
||||
json!({
|
||||
"name": name,
|
||||
"permissions": [],
|
||||
"is_deleted": false,
|
||||
"created_at": get_iso_date(),
|
||||
"updated_at": get_iso_date(),
|
||||
}),
|
||||
))
|
||||
.await?;
|
||||
for (id, name, _created_at_str, _updated_at_str) in roles { // Renamed to avoid conflict
|
||||
let uuid = Uuid::parse_str(id).unwrap_or_else(|_| Uuid::new_v4());
|
||||
|
||||
// Check if role already exists
|
||||
let existing = RoleEntity::find_by_id(uuid).one(db).await?;
|
||||
if existing.is_some() {
|
||||
println!("ℹ️ Skipping (already exists): {name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete existing by id to avoid duplicates (original logic, replaced by existence check)
|
||||
// let _ = pg_conn.execute(sea_orm::Statement::from_string(db.get_database_backend(), format!("DELETE FROM app_roles WHERE id = '{}'", uuid))).await.ok();
|
||||
|
||||
let role_model = RoleBuilder::new()
|
||||
.name(name.to_string())
|
||||
.description("System generated role".to_string())
|
||||
.permissions(vec![])
|
||||
.is_default(false)
|
||||
.build()?;
|
||||
let mut role_model = role_model;
|
||||
role_model.id = Set(uuid);
|
||||
role_model.is_system_role = Set(true); // Set the missing field
|
||||
role_model.created_at = Set(Utc::now()); // Set created_at
|
||||
role_model.updated_at = Set(Utc::now()); // Set updated_at
|
||||
|
||||
role_model.insert(db).await?;
|
||||
println!("✅ Inserted role: {name}");
|
||||
}
|
||||
println!("✅ All Roles seeded");
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
use imphnen_iam::{get_iso_date, make_thing, PermissionsEnum};
|
||||
use imphnen_iam::PermissionsEnum;
|
||||
use std::error::Error;
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::auth::roles::Entity as RolesEntity;
|
||||
use imphnen_entities::seaorm::auth::roles::ActiveModel as RoleActiveModel;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::EntityTrait;
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use uuid::Uuid;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
db.query("DEFINE INDEX user_email_index ON TABLE users COLUMNS email UNIQUE;")
|
||||
|
||||
.await?;
|
||||
db.query("DEFINE INDEX role_name_idx ON TABLE roles COLUMNS name UNIQUE;")
|
||||
|
||||
.await?;
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
// Ensure indexes are present if needed (placeholders) - we don't modify schema here
|
||||
|
||||
println!("✅ Index 'user_email_index' defined on table 'users' for column 'email'.");
|
||||
|
||||
@@ -86,7 +80,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
PermissionsEnum::ReadDetailGachaRolls,
|
||||
PermissionsEnum::CreateGachaRolls,
|
||||
PermissionsEnum::ExecuteGachaRolls,
|
||||
PermissionsEnum::ManageAllTeams,
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -97,17 +90,21 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
];
|
||||
|
||||
for (role_id, permissions) in roles_permissions {
|
||||
let permission_refs: Vec<_> = permissions
|
||||
.iter()
|
||||
.map(|perm| make_thing("app_permissions", &perm.id()))
|
||||
.collect();
|
||||
let role_uuid = Uuid::parse_str(role_id).unwrap_or_else(|_| Uuid::new_v4());
|
||||
// Map permissions enum to JSON array of permission ids
|
||||
let json_permissions = JsonValue::Array(
|
||||
permissions.iter().map(|p| JsonValue::String(p.id())).collect()
|
||||
);
|
||||
|
||||
db.query("UPDATE type::thing('app_roles', $role_id) SET permissions = $permissions, updated_at = $updated_at WHERE is_deleted = false")
|
||||
.bind(("role_id", role_id))
|
||||
.bind(("permissions", permission_refs))
|
||||
.bind(("updated_at", get_iso_date()))
|
||||
.await?;
|
||||
// Find role and update permissions
|
||||
if let Some(role_model) = RolesEntity::find_by_id(role_uuid).one(db).await? {
|
||||
let mut am: RoleActiveModel = role_model.into();
|
||||
am.permissions = Set(Some(json_permissions));
|
||||
am.update(db).await?;
|
||||
println!("✅ Permissions updated for role: {role_id}");
|
||||
} else {
|
||||
println!("⚠️ Role with id {role_id} not found, skipping permissions update");
|
||||
}
|
||||
}
|
||||
|
||||
println!("✅ All roles permissions updated!");
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
use imphnen_iam::v1::teams::TeamsSchema;
|
||||
use imphnen_utils::get_iso_date;
|
||||
use std::error::Error;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
|
||||
let teams = vec![
|
||||
(
|
||||
"team-dev-001",
|
||||
"Development Team",
|
||||
Some("Core development team for the platform".to_string()),
|
||||
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
|
||||
true,
|
||||
Some(10),
|
||||
Some(vec!["Rust".to_string(), "Backend".to_string()]),
|
||||
Some("Remote".to_string()),
|
||||
),
|
||||
(
|
||||
"team-design-001",
|
||||
"Design Team",
|
||||
Some("UI/UX design team".to_string()),
|
||||
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
|
||||
true,
|
||||
Some(5),
|
||||
Some(vec!["Figma".to_string(), "Design".to_string()]),
|
||||
Some("Remote".to_string()),
|
||||
),
|
||||
(
|
||||
"team-qa-001",
|
||||
"Quality Assurance Team",
|
||||
Some("Testing and quality assurance team".to_string()),
|
||||
"c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2", // admin user
|
||||
false,
|
||||
Some(8),
|
||||
Some(vec!["Testing".to_string(), "Automation".to_string()]),
|
||||
Some("Remote".to_string()),
|
||||
),
|
||||
];
|
||||
|
||||
for (id, name, description, leader_id, is_open, max_members, skills_required, location) in teams {
|
||||
db.query("DELETE type::thing('app_teams', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
|
||||
let team = TeamsSchema {
|
||||
id: Thing::from(("app_teams", id)),
|
||||
name: name.into(),
|
||||
description,
|
||||
leader_id: Thing::from(("app_users", leader_id)),
|
||||
is_open,
|
||||
max_members,
|
||||
skills_required,
|
||||
location,
|
||||
avatar: None,
|
||||
website_url: None,
|
||||
github_url: None,
|
||||
is_active: true,
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
|
||||
db.create::<Option<TeamsSchema>>(("app_teams", id))
|
||||
.content(team)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted team: {name}");
|
||||
}
|
||||
|
||||
println!("✅ All Teams seeded");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,174 +1,76 @@
|
||||
use imphnen_cms::v1::landing::events::events_schema::EventsSchema;
|
||||
use imphnen_cms::v1::landing::testimonials::testimonials_schema::TestimonialsSchema;
|
||||
use imphnen_dimentorin::v1::mentors::mentors_schema::MentorSchema;
|
||||
use imphnen_dimentorin::v1::mentors::mentors_dto::MentoringRate;
|
||||
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
|
||||
HackathonSchema, HackathonEventsSchema, HackathonTimelineSchema,
|
||||
HackathonStatus, HackathonEventType, HackathonPhase
|
||||
};
|
||||
use imphnen_utils::get_iso_date;
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use std::error::Error;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::common::events::ActiveModel as EventsActiveModel;
|
||||
use imphnen_entities::seaorm::common::testimonials::ActiveModel as TestimonialsActiveModel;
|
||||
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorsActiveModel;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::ActiveModelTrait;
|
||||
use uuid::Uuid;
|
||||
use serde_json::json;
|
||||
use chrono::Utc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
// Seed Events - handle existing data
|
||||
let event = EventsSchema {
|
||||
id: Thing::from(("app_events", "1")),
|
||||
name: "Test Event".to_string(),
|
||||
description: "Test event description".to_string(),
|
||||
detail_link: "https://example.com/event".to_string(),
|
||||
price: 50.0,
|
||||
is_online: true,
|
||||
start_date: get_iso_date(),
|
||||
end_date: get_iso_date(),
|
||||
location: None,
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
match db.create::<Option<EventsSchema>>(("app_events", "1"))
|
||||
.content(event)
|
||||
.await {
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
let mut event_model: EventsActiveModel = Default::default();
|
||||
event_model.id = Set(Uuid::parse_str(&uuid)?);
|
||||
event_model.name = Set("Test Event".to_string());
|
||||
event_model.description = Set("Test event description".to_string());
|
||||
event_model.detail_link = Set("https://example.com/event".to_string());
|
||||
event_model.price = Set(50.0);
|
||||
event_model.is_online = Set(true);
|
||||
event_model.start_date = Set(Utc::now());
|
||||
event_model.end_date = Set(Utc::now() + chrono::Duration::days(1));
|
||||
event_model.location = Set(None);
|
||||
event_model.is_deleted = Set(false);
|
||||
match event_model.insert(db).await {
|
||||
Ok(_) => println!("✅ Inserted test event"),
|
||||
Err(_) => println!("⚠️ Test event already exists, skipping"),
|
||||
Err(_) => println!("⚠️ Test event already exists or could not be inserted, skipping"),
|
||||
};
|
||||
|
||||
// Seed Testimonials - handle existing data
|
||||
let testimonial = TestimonialsSchema {
|
||||
id: Thing::from(("app_testimonials", "1")),
|
||||
user: Thing::from(("app_users", "c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")),
|
||||
role: "Student".to_string(),
|
||||
content: "This is a great platform!".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
match db.create::<Option<TestimonialsSchema>>(("app_testimonials", "1"))
|
||||
.content(testimonial)
|
||||
.await {
|
||||
let mut testimonial_model: TestimonialsActiveModel = Default::default();
|
||||
testimonial_model.id = Set(Uuid::parse_str("00000000-0000-0000-0000-000000000001")?);
|
||||
testimonial_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
testimonial_model.role = Set("Student".to_string());
|
||||
testimonial_model.content = Set("This is a great platform!".to_string());
|
||||
testimonial_model.is_deleted = Set(false);
|
||||
match testimonial_model.insert(db).await {
|
||||
Ok(_) => println!("✅ Inserted test testimonial"),
|
||||
Err(_) => println!("⚠️ Test testimonial already exists, skipping"),
|
||||
};
|
||||
|
||||
// Seed Hackathon - handle existing data
|
||||
let hackathon = HackathonSchema {
|
||||
id: Thing::from(("app_hackathons", "1")),
|
||||
name: "Test Hackathon".to_string(),
|
||||
description: "Test hackathon description".to_string(),
|
||||
start_date: Utc::now() + chrono::Duration::days(30),
|
||||
end_date: Utc::now() + chrono::Duration::days(37),
|
||||
registration_deadline: Utc::now() + chrono::Duration::days(25),
|
||||
max_participants: Some(100),
|
||||
status: HackathonStatus::Draft,
|
||||
theme: Some("Technology".to_string()),
|
||||
rules: Some("Follow the rules".to_string()),
|
||||
prizes: Some(vec![]),
|
||||
previous_winners: Some(vec![]),
|
||||
organizers: vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()],
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
match db.create::<Option<HackathonSchema>>(("app_hackathons", "1"))
|
||||
.content(hackathon)
|
||||
.await {
|
||||
Ok(_) => println!("✅ Inserted test hackathon"),
|
||||
Err(_) => println!("⚠️ Test hackathon already exists, skipping"),
|
||||
};
|
||||
|
||||
// Seed Hackathon Event
|
||||
let hackathon_event = HackathonEventsSchema {
|
||||
id: Thing::from(("app_hackathon_events", "test-event-001")),
|
||||
hackathon_id: Thing::from(("app_hackathons", "1")),
|
||||
title: "Test Event".to_string(),
|
||||
description: Some("Test hackathon event description".to_string()),
|
||||
event_type: HackathonEventType::Workshop,
|
||||
start_time: Utc::now() + chrono::Duration::days(30),
|
||||
end_time: Utc::now() + chrono::Duration::days(30) + chrono::Duration::hours(6),
|
||||
location: Some("Online".to_string()),
|
||||
virtual_link: None,
|
||||
max_attendees: Some(50),
|
||||
is_mandatory: false,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
// Try to create hackathon event, skip if already exists
|
||||
match db.create::<Option<HackathonEventsSchema>>(("app_hackathon_events", "test-event-001"))
|
||||
.content(hackathon_event)
|
||||
.await {
|
||||
Ok(_) => println!("✅ Inserted test hackathon event"),
|
||||
Err(_) => println!("⚠️ Test hackathon event already exists, skipping"),
|
||||
};
|
||||
|
||||
// Seed Hackathon Timeline
|
||||
let hackathon_timeline = HackathonTimelineSchema {
|
||||
id: Thing::from(("app_hackathon_timeline", "test-timeline-001")),
|
||||
hackathon_id: Thing::from(("app_hackathons", "1")),
|
||||
phase: HackathonPhase::Registration,
|
||||
title: "Test Timeline".to_string(),
|
||||
description: Some("Test hackathon timeline description".to_string()),
|
||||
start_date: Utc::now(),
|
||||
end_date: Utc::now() + chrono::Duration::days(7),
|
||||
is_active: true,
|
||||
order: 1,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
// Try to create hackathon timeline, skip if already exists
|
||||
match db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", "test-timeline-001"))
|
||||
.content(hackathon_timeline)
|
||||
.await {
|
||||
Ok(_) => println!("✅ Inserted test hackathon timeline"),
|
||||
Err(_) => println!("⚠️ Test hackathon timeline already exists, skipping"),
|
||||
Err(_) => println!("⚠️ Test testimonial already exists or could not be inserted, skipping"),
|
||||
};
|
||||
|
||||
// Seed Mentor - handle existing data
|
||||
let mentor = MentorSchema {
|
||||
id: Thing::from(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901")),
|
||||
user_id: Some(Thing::from(("app_users", "e6f78d23-83bf-5c2b-bcd4-001345678901"))),
|
||||
industries: vec!["Technology".to_string(), "Education".to_string()],
|
||||
expertise: vec!["Software Development".to_string()],
|
||||
languages: vec!["English".to_string(), "Indonesian".to_string()],
|
||||
current_company: "Tech Corp".to_string(),
|
||||
current_role: "Senior Engineer".to_string(),
|
||||
years_of_experience: 5,
|
||||
topics_of_interest: vec!["Rust".to_string(), "Web Development".to_string()],
|
||||
preferred_mentee_level: vec!["Beginner".to_string()],
|
||||
preferred_mentoring_formats: vec!["1:1".to_string(), "Group".to_string()],
|
||||
availability_commitment: "Weekly".to_string(),
|
||||
mentoring_rate: MentoringRate {
|
||||
amount: 100,
|
||||
currency: "IDR".to_string(),
|
||||
per_duration: "hour".to_string(),
|
||||
},
|
||||
status: "active".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
match db.create::<Option<MentorSchema>>(("app_mentors", "e6f78d23-83bf-5c2b-bcd4-001345678901"))
|
||||
.content(mentor)
|
||||
.await {
|
||||
Ok(_) => println!("✅ Inserted test mentor"),
|
||||
Err(_) => println!("⚠️ Test mentor already exists, skipping"),
|
||||
};
|
||||
let mentor_id = Uuid::new_v4();
|
||||
let mut mentor_model: MentorsActiveModel = Default::default();
|
||||
mentor_model.id = Set(mentor_id);
|
||||
// Use the admin user ID instead of a random one
|
||||
mentor_model.user_id = Set(Uuid::parse_str("c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2")?);
|
||||
mentor_model.industries = Set(Some(json!( ["Technology", "Education"] )));
|
||||
mentor_model.expertise = Set(Some(json!( ["Software Development"] )));
|
||||
mentor_model.languages = Set(Some(json!( ["English", "Indonesian"] )));
|
||||
mentor_model.current_company = Set(Some("Tech Corp".to_string()));
|
||||
mentor_model.current_role = Set(Some("Senior Engineer".to_string()));
|
||||
mentor_model.years_of_experience = Set(Some(5));
|
||||
mentor_model.topics_of_interest = Set(Some(json!( ["Rust", "Web Development"] )));
|
||||
mentor_model.preferred_mentee_level = Set(Some("Beginner".to_string()));
|
||||
mentor_model.preferred_mentoring_formats = Set(Some(json!( ["1:1", "Group"] )));
|
||||
mentor_model.availability_commitment = Set(Some("Weekly".to_string()));
|
||||
mentor_model.mentoring_rate = Set(Some(100.0));
|
||||
mentor_model.status = Set(Some("active".to_string()));
|
||||
mentor_model.is_deleted = Set(false);
|
||||
mentor_model.created_at = Set(chrono::Utc::now());
|
||||
mentor_model.updated_at = Set(chrono::Utc::now());
|
||||
// Create mentor record via SeaORM active model
|
||||
mentor_model.insert(db).await?;
|
||||
println!("✅ Inserted test mentor via SeaORM");
|
||||
|
||||
println!("✅ All test data seeded successfully");
|
||||
Ok(())
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use imphnen_hackathon::v1::hackathon::hackathon_schema::{
|
||||
HackathonSchema, HackathonTimelineSchema, HackathonSubmissionsSchema,
|
||||
HackathonStatus, HackathonPhase, SubmissionStatus, Prize,
|
||||
};
|
||||
use imphnen_iam::{UsersSchema, v1::teams::TeamsSchema};
|
||||
use imphnen_utils::{get_iso_date, hash_password};
|
||||
use std::error::Error;
|
||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
|
||||
// Test users for submission testing
|
||||
let test_users = vec![
|
||||
(
|
||||
"test-user-001",
|
||||
"testuser1@example.com",
|
||||
"Test User 1",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059", // User role
|
||||
),
|
||||
(
|
||||
"test-user-002",
|
||||
"testuser2@example.com",
|
||||
"Test User 2",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"test-user-003",
|
||||
"testuser3@example.com",
|
||||
"Test User 3",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
];
|
||||
|
||||
// Test teams for submission testing
|
||||
let test_teams = vec![
|
||||
(
|
||||
"test-team-001",
|
||||
"Test Team Alpha",
|
||||
Some("Team for testing hackathon submissions".to_string()),
|
||||
"test-user-001", // team leader
|
||||
true,
|
||||
Some(5),
|
||||
Some(vec!["JavaScript".to_string(), "React".to_string()]),
|
||||
Some("Remote".to_string()),
|
||||
),
|
||||
(
|
||||
"test-team-002",
|
||||
"Test Team Beta",
|
||||
Some("Another team for testing submissions".to_string()),
|
||||
"test-user-002",
|
||||
true,
|
||||
Some(4),
|
||||
Some(vec!["Python".to_string(), "Django".to_string()]),
|
||||
Some("Remote".to_string()),
|
||||
),
|
||||
];
|
||||
|
||||
// Test hackathon for submission testing
|
||||
let test_hackathons = vec![
|
||||
(
|
||||
"test-hackathon-001",
|
||||
"Test Hackathon 2025",
|
||||
"Hackathon for testing submission functionality.",
|
||||
"2025-12-01T09:00:00Z",
|
||||
"2025-12-03T18:00:00Z",
|
||||
"2025-11-25T23:59:59Z",
|
||||
Some(50),
|
||||
HackathonStatus::RegistrationOpen,
|
||||
Some("Testing & Development".to_string()),
|
||||
Some("1. Test all submission features\n2. Teams can have 2-5 members\n3. Submit by deadline".to_string()),
|
||||
Some(vec![
|
||||
Prize { position: 1, title: "Test Winner".to_string(), description: Some("Best test submission".to_string()), value: Some("$1,000".to_string()) },
|
||||
Prize { position: 2, title: "Test Runner-up".to_string(), description: Some("Second best submission".to_string()), value: Some("$500".to_string()) },
|
||||
]),
|
||||
vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()], // admin user
|
||||
),
|
||||
];
|
||||
|
||||
// Test hackathon timeline
|
||||
let test_timeline = vec![
|
||||
(
|
||||
"test-hackathon-001",
|
||||
HackathonPhase::Registration,
|
||||
"Registration Phase",
|
||||
Some("Register your team for the test hackathon".to_string()),
|
||||
"2025-11-20T00:00:00Z",
|
||||
"2025-11-25T23:59:59Z",
|
||||
true,
|
||||
1,
|
||||
),
|
||||
(
|
||||
"test-hackathon-001",
|
||||
HackathonPhase::Development,
|
||||
"Development Phase",
|
||||
Some("Build your test project".to_string()),
|
||||
"2025-12-01T00:00:00Z",
|
||||
"2025-12-02T23:59:59Z",
|
||||
false,
|
||||
2,
|
||||
),
|
||||
(
|
||||
"test-hackathon-001",
|
||||
HackathonPhase::Submission,
|
||||
"Submission Phase",
|
||||
Some("Submit your test project".to_string()),
|
||||
"2025-12-03T00:00:00Z",
|
||||
"2025-12-03T12:00:00Z",
|
||||
false,
|
||||
3,
|
||||
),
|
||||
];
|
||||
|
||||
// Test submissions
|
||||
let test_submissions = vec![
|
||||
(
|
||||
"test-hackathon-001",
|
||||
"test-team-001",
|
||||
"Test Project Alpha",
|
||||
"A comprehensive test project demonstrating all features.",
|
||||
Some("https://github.com/test-team-alpha/test-project".to_string()),
|
||||
Some("https://demo.test-project-alpha.com".to_string()),
|
||||
Some("https://slides.test-project-alpha.com".to_string()),
|
||||
vec!["JavaScript".to_string(), "React".to_string(), "Node.js".to_string()],
|
||||
SubmissionStatus::Draft,
|
||||
"2025-12-02T10:00:00Z",
|
||||
),
|
||||
(
|
||||
"test-hackathon-001",
|
||||
"test-team-002",
|
||||
"Test Project Beta",
|
||||
"Another test project with different technologies.",
|
||||
Some("https://github.com/test-team-beta/test-project".to_string()),
|
||||
Some("https://demo.test-project-beta.com".to_string()),
|
||||
None,
|
||||
vec!["Python".to_string(), "Django".to_string(), "PostgreSQL".to_string()],
|
||||
SubmissionStatus::Submitted,
|
||||
"2025-12-03T09:30:00Z",
|
||||
),
|
||||
];
|
||||
|
||||
// Seed test users
|
||||
for (id, email, fullname, role_id) in test_users {
|
||||
db.query("DELETE type::thing('app_users', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
|
||||
let user = UsersSchema {
|
||||
id: Thing::from(("app_users", id)),
|
||||
fullname: fullname.into(),
|
||||
legal_name: Some(format!("{} Legal Name", fullname)),
|
||||
email: email.into(),
|
||||
password: hash_password("password").unwrap(),
|
||||
avatar: Some("https://example.com/avatar.jpg".into()),
|
||||
phone_number: "081234567890".into(),
|
||||
phone_for_verification: Some("081234567890".into()),
|
||||
is_active: true,
|
||||
is_deleted: false,
|
||||
mentor_id: None,
|
||||
gender: Some("male".into()),
|
||||
birthdate: Some("1995-01-01".into()),
|
||||
domicile: Some("Jakarta, Indonesia".into()),
|
||||
bio: Some(format!("{} is a test user for hackathon submissions.", fullname)),
|
||||
last_education: Some("S1 Computer Science".into()),
|
||||
linkedin_url: Some("https://linkedin.com/in/testuser".into()),
|
||||
github_url: Some("https://github.com/testuser".into()),
|
||||
cv_url: Some("https://example.com/cv.pdf".into()),
|
||||
portfolio_url: Some("https://example.com/portfolio".into()),
|
||||
website_url: Some("https://example.com/website".into()),
|
||||
twitter_url: Some("https://twitter.com/testuser".into()),
|
||||
location: Some("Jakarta, Indonesia".into()),
|
||||
skills: Some(vec!["JavaScript".to_string(), "Python".to_string()]),
|
||||
experience: None,
|
||||
education: None,
|
||||
career_status: Some("Developer".into()),
|
||||
role: Thing::from(("app_roles", role_id)),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
|
||||
db.create::<Option<UsersSchema>>(("app_users", id))
|
||||
.content(user)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted test user: {fullname} ({email})");
|
||||
}
|
||||
|
||||
// Seed test teams
|
||||
for (id, name, description, leader_id, is_open, max_members, skills_required, location) in test_teams {
|
||||
db.query("DELETE type::thing('app_teams', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
|
||||
let team = TeamsSchema {
|
||||
id: Thing::from(("app_teams", id)),
|
||||
name: name.into(),
|
||||
description,
|
||||
leader_id: Thing::from(("app_users", leader_id)),
|
||||
is_open,
|
||||
max_members,
|
||||
skills_required,
|
||||
location,
|
||||
avatar: None,
|
||||
website_url: None,
|
||||
github_url: None,
|
||||
is_active: true,
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
};
|
||||
|
||||
db.create::<Option<TeamsSchema>>(("app_teams", id))
|
||||
.content(team)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted test team: {name}");
|
||||
}
|
||||
|
||||
// Seed test hackathons
|
||||
for (
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
start_date,
|
||||
end_date,
|
||||
registration_deadline,
|
||||
max_participants,
|
||||
status,
|
||||
theme,
|
||||
rules,
|
||||
prizes,
|
||||
organizers,
|
||||
) in test_hackathons {
|
||||
db.query("DELETE type::thing('app_hackathons', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
|
||||
let hackathon = HackathonSchema {
|
||||
id: Thing::from(("app_hackathons", id)),
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
||||
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
|
||||
max_participants,
|
||||
status: status.clone(),
|
||||
theme: theme.clone(),
|
||||
rules: rules.clone(),
|
||||
prizes: prizes.clone(),
|
||||
previous_winners: None,
|
||||
organizers: organizers.clone(),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
db.create::<Option<HackathonSchema>>(("app_hackathons", id))
|
||||
.content(hackathon)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted test hackathon: {name}");
|
||||
// Also create an alias canonical id 'test-hackathon' so tests referencing
|
||||
// /v1/hackathons/test-hackathon/... can find a hackathon record.
|
||||
if id != "test-hackathon" && id.starts_with("test-hackathon") {
|
||||
let alias_id = "test-hackathon";
|
||||
db.query("DELETE type::thing('app_hackathons', $id)")
|
||||
.bind(("id", alias_id))
|
||||
.await?;
|
||||
|
||||
let alias_hackathon = HackathonSchema {
|
||||
id: Thing::from(("app_hackathons", alias_id)),
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
||||
registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc),
|
||||
max_participants,
|
||||
status: status.clone(),
|
||||
theme: theme.clone(),
|
||||
rules: rules.clone(),
|
||||
prizes: prizes.clone(),
|
||||
previous_winners: None,
|
||||
organizers: organizers.clone(),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
db.create::<Option<HackathonSchema>>(("app_hackathons", alias_id))
|
||||
.content(alias_hackathon)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted test hackathon alias: {alias_id}");
|
||||
}
|
||||
}
|
||||
|
||||
// Seed test hackathon timeline
|
||||
for (
|
||||
hackathon_id,
|
||||
phase,
|
||||
title,
|
||||
description,
|
||||
start_date,
|
||||
end_date,
|
||||
is_active,
|
||||
order,
|
||||
) in test_timeline {
|
||||
let timeline_id = format!("test-timeline-{}-{}", hackathon_id, order);
|
||||
|
||||
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
|
||||
.bind(("id", timeline_id.clone()))
|
||||
.await?;
|
||||
|
||||
let timeline = HackathonTimelineSchema {
|
||||
id: Thing::from(("app_hackathon_timeline", timeline_id.as_str())),
|
||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
||||
phase: phase.clone(),
|
||||
title: title.into(),
|
||||
description: description.clone(),
|
||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
||||
is_active,
|
||||
order,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
db.create::<Option<HackathonTimelineSchema>>(("app_hackathon_timeline", timeline_id))
|
||||
.content(timeline)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted test hackathon timeline: {title}");
|
||||
|
||||
// Also create alias timeline entries for the canonical test id 'test-hackathon'
|
||||
if hackathon_id != "test-hackathon" && hackathon_id.starts_with("test-hackathon") {
|
||||
let alias_hackathon_id = "test-hackathon";
|
||||
let alias_timeline_id = format!("test-timeline-{}-{}", alias_hackathon_id, order);
|
||||
|
||||
db.query("DELETE type::thing('app_hackathon_timeline', $id)")
|
||||
.bind(("id", alias_timeline_id.clone()))
|
||||
.await?;
|
||||
|
||||
let alias_timeline = HackathonTimelineSchema {
|
||||
id: Thing::from(("app_hackathon_timeline", alias_timeline_id.as_str())),
|
||||
hackathon_id: Thing::from(("app_hackathons", alias_hackathon_id)),
|
||||
phase: phase.clone(),
|
||||
title: title.into(),
|
||||
description: description.clone(),
|
||||
start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc),
|
||||
end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc),
|
||||
is_active,
|
||||
order,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
db.create::<Option<HackathonTimelineSchema>>( ("app_hackathon_timeline", alias_timeline_id.clone()) )
|
||||
.content(alias_timeline)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted test hackathon timeline alias: {alias_timeline_id}");
|
||||
}
|
||||
}
|
||||
|
||||
// Seed test hackathon submissions
|
||||
for (
|
||||
hackathon_id,
|
||||
team_id,
|
||||
project_name,
|
||||
description,
|
||||
repository_url,
|
||||
demo_url,
|
||||
slides_url,
|
||||
technologies,
|
||||
submission_status,
|
||||
submitted_at,
|
||||
) in test_submissions {
|
||||
let submission_id = format!("test-submission-{}-{}", hackathon_id, team_id);
|
||||
|
||||
db.query("DELETE type::thing('app_hackathon_submissions', $id)")
|
||||
.bind(("id", submission_id.clone()))
|
||||
.await?;
|
||||
|
||||
let submission = HackathonSubmissionsSchema {
|
||||
id: Thing::from(("app_hackathon_submissions", submission_id.as_str())),
|
||||
hackathon_id: Thing::from(("app_hackathons", hackathon_id)),
|
||||
team_id: Some(Thing::from(("app_teams", team_id))),
|
||||
project_name: Some(project_name.into()),
|
||||
description: Some(description.into()),
|
||||
repository_url,
|
||||
upload_file_url: None,
|
||||
demo_url,
|
||||
slides_url,
|
||||
technologies: Some(technologies),
|
||||
contact_instagram: None,
|
||||
contact_twitter: None,
|
||||
contact_linkedin: None,
|
||||
contact_facebook: None,
|
||||
contact_youtube: None,
|
||||
contact_tiktok: None,
|
||||
contact_other: None,
|
||||
submission_status: Some(submission_status),
|
||||
judge_feedback: None,
|
||||
submitted_at: Some(DateTime::parse_from_rfc3339(submitted_at)?.with_timezone(&Utc)),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
};
|
||||
|
||||
db.create::<Option<HackathonSubmissionsSchema>>(("app_hackathon_submissions", submission_id))
|
||||
.content(submission)
|
||||
.await?;
|
||||
|
||||
println!("✅ Inserted test hackathon submission: {project_name}");
|
||||
}
|
||||
|
||||
println!("✅ All test submission data seeded");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
use imphnen_iam::UsersSchema;
|
||||
use imphnen_utils::{get_iso_date, hash_password};
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use imphnen_libs::hash_password;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as UserEntity; // Added for dynamic role lookup
|
||||
use imphnen_entities::seaorm::auth::users::ActiveModel as UsersActiveModel;
|
||||
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait, IntoActiveModel};
|
||||
use uuid::Uuid;
|
||||
use std::error::Error;
|
||||
use chrono::Utc;
|
||||
|
||||
use surrealdb::{opt::auth::Root, sql::Thing};
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env = &imphnen_libs::environment::ENV;
|
||||
use surrealdb::engine::any;
|
||||
let db = any::connect(&env.surrealdb_url).await?;
|
||||
db.signin(Root {
|
||||
username: &env.surrealdb_username,
|
||||
password: &env.surrealdb_password,
|
||||
})
|
||||
.await?;
|
||||
db.use_ns(env.surrealdb_namespace.clone())
|
||||
.use_db(env.surrealdb_dbname.clone())
|
||||
.await?;
|
||||
let postgres_config = PostgresConfig::from_env()?;
|
||||
let pg_conn = PostgresConnection::new(postgres_config).await?;
|
||||
let db = &pg_conn.conn;
|
||||
|
||||
let users = vec![
|
||||
(
|
||||
@@ -37,71 +36,126 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"testuser1-id",
|
||||
"665a3cfc-ea5f-4bcd-8769-4a6d8d1451d4",
|
||||
"testuser1@example.com",
|
||||
"Test User 1",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059", // Fixed UUID
|
||||
),
|
||||
(
|
||||
"testuser2-id",
|
||||
"3972c139-a450-416c-93b0-c42539dc780f",
|
||||
"testuser2@example.com",
|
||||
"Test User 2",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"testuser3-id",
|
||||
"b426c0a9-0efb-4e26-b078-4f18767255f3",
|
||||
"testuser3@example.com",
|
||||
"Test User 3",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059", // Fixed UUID
|
||||
),
|
||||
// Additional Users for Volume and Variety
|
||||
(
|
||||
"11111111-1111-1111-1111-111111111111",
|
||||
"user4@example.com",
|
||||
"User Four",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"22222222-2222-2222-2222-222222222222",
|
||||
"user5@example.com",
|
||||
"User Five",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"33333333-3333-3333-3333-333333333333",
|
||||
"mentor2@example.com",
|
||||
"Mentor Two",
|
||||
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a", // Mentor Role
|
||||
),
|
||||
(
|
||||
"44444444-4444-4444-4444-444444444444",
|
||||
"staff2@example.com",
|
||||
"Staff Two",
|
||||
"50133429-f4b1-4249-9f97-7b86e6ee9d86", // Staff Role
|
||||
),
|
||||
(
|
||||
"55555555-5555-5555-5555-555555555555",
|
||||
"user6@example.com",
|
||||
"User Six",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"66666666-6666-6666-6666-666666666666",
|
||||
"user7@example.com",
|
||||
"User Seven",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"77777777-7777-7777-7777-777777777777",
|
||||
"user8@example.com",
|
||||
"User Eight",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"88888888-8888-8888-8888-888888888888",
|
||||
"user9@example.com",
|
||||
"User Nine",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
(
|
||||
"99999999-9999-9999-9999-999999999999",
|
||||
"user10@example.com",
|
||||
"User Ten",
|
||||
"5713cb37-dc02-4e87-8048-d7a41d352059",
|
||||
),
|
||||
];
|
||||
|
||||
for (id, email, fullname, role_id) in users {
|
||||
db.query("DELETE type::thing('app_users', $id)")
|
||||
.bind(("id", id))
|
||||
.await?;
|
||||
for (id, email, fullname, role_id_str) in users { // role_id_str directly contains UUID
|
||||
let role_uuid = Some(Uuid::parse_str(role_id_str)
|
||||
.map_err(|e| format!("Invalid UUID for role: {role_id_str} - {e}"))?);
|
||||
|
||||
let user = UsersSchema {
|
||||
id: Thing::from(("app_users", id)),
|
||||
fullname: fullname.into(),
|
||||
legal_name: Some(format!("{} Legal Name", fullname)),
|
||||
email: email.into(),
|
||||
password: hash_password("password").unwrap(),
|
||||
avatar: Some("https://example.com/avatar.jpg".into()),
|
||||
phone_number: "081234567890".into(),
|
||||
phone_for_verification: Some("081234567890".into()),
|
||||
is_active: true,
|
||||
is_deleted: false,
|
||||
mentor_id: None,
|
||||
gender: Some("male".into()),
|
||||
birthdate: Some("1990-05-15".into()),
|
||||
domicile: Some("Jakarta, Indonesia".into()),
|
||||
// identity_document_url: None, // Sudah tidak dipakai, bisa dihapus dari schema jika tidak diperlukan
|
||||
bio: Some(format!("{} adalah user dengan data pribadi lengkap untuk testing.", fullname)),
|
||||
last_education: Some("S1 Teknik Informatika".into()),
|
||||
linkedin_url: Some("https://linkedin.com/in/user".into()),
|
||||
github_url: Some("https://github.com/user".into()),
|
||||
cv_url: Some("https://example.com/cv.pdf".into()),
|
||||
portfolio_url: Some("https://example.com/portfolio".into()),
|
||||
website_url: Some("https://example.com/website".into()),
|
||||
twitter_url: Some("https://twitter.com/user".into()),
|
||||
location: Some("Jakarta, Indonesia".into()),
|
||||
skills: Some(vec!["JavaScript".into(), "React".into(), "Node.js".into()]),
|
||||
experience: None,
|
||||
education: None,
|
||||
career_status: Some("Senior Developer".into()),
|
||||
role: Thing::from(("app_roles", role_id)),
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
// Build SeaORM ActiveModel for users
|
||||
let uid = Uuid::parse_str(id)?; // Should always be valid UUID strings from test data
|
||||
|
||||
let names: Vec<&str> = fullname.split_whitespace().collect();
|
||||
let first_name = names.first().map(|s| s.to_string());
|
||||
let last_name = if names.len() > 1 { Some(names[1..].join(" ")) } else { None };
|
||||
|
||||
let password = "password";
|
||||
let hashed = hash_password(password).unwrap();
|
||||
|
||||
// Explicit Upsert Logic
|
||||
let existing_user = UserEntity::find_by_id(uid).one(db).await?;
|
||||
let is_update = existing_user.is_some();
|
||||
|
||||
let mut user_model: UsersActiveModel = if let Some(existing) = existing_user {
|
||||
println!("🔄 Updating user: {fullname} ({email})");
|
||||
existing.into_active_model()
|
||||
} else {
|
||||
println!("✅ Inserting user: {fullname} ({email})");
|
||||
let mut active: UsersActiveModel = Default::default();
|
||||
active.id = Set(uid);
|
||||
active.created_at = Set(Utc::now());
|
||||
active
|
||||
};
|
||||
|
||||
db.create::<Option<UsersSchema>>(("app_users", id))
|
||||
.content(user)
|
||||
.await?;
|
||||
user_model.email = Set(email.to_string());
|
||||
user_model.password_hash = Set(hashed);
|
||||
user_model.username = Set(email.to_string());
|
||||
user_model.first_name = Set(first_name);
|
||||
user_model.last_name = Set(last_name);
|
||||
user_model.avatar_url = Set(Some("https://example.com/avatar.jpg".to_string()));
|
||||
user_model.is_verified = Set(true);
|
||||
user_model.is_active = Set(true);
|
||||
user_model.role_id = Set(role_uuid);
|
||||
user_model.updated_at = Set(Utc::now());
|
||||
|
||||
println!("✅ Inserted user: {fullname} ({email})");
|
||||
if is_update {
|
||||
user_model.update(db).await?;
|
||||
} else {
|
||||
user_model.insert(db).await?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("✅ All Users seeded");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use std::error::Error;
|
||||
use std::process::Command;
|
||||
|
||||
fn run_seed(bin: &str) -> Result<(), Box<dyn Error>> {
|
||||
println!("🔧 Seeding: {bin}");
|
||||
let status = Command::new("cargo").args(["run", "--bin", bin]).status()?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let status = Command::new(format!("./target/release/{}.exe", bin)).status()?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let status = Command::new(format!("./target/release/{}", bin)).status()?;
|
||||
|
||||
if !status.success() {
|
||||
Err(format!("❌ Failed to run seed: {bin}").into())
|
||||
@@ -20,7 +25,6 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
run_seed("seed_roles_permissions")?;
|
||||
run_seed("seed_users")?;
|
||||
run_seed("seed_events")?;
|
||||
run_seed("seed_hackathons")?;
|
||||
run_seed("seed_gacha_rolls")?;
|
||||
run_seed("seed_mentor_user")?;
|
||||
run_seed("seed_test_data")?;
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
//! PostgreSQL Connection Test Program
|
||||
//! This program tests the PostgreSQL integration with SeaORM
|
||||
|
||||
use std::sync::Arc;
|
||||
use imphnen_libs::postgres::{PostgresConfig, PostgresConnection, PostgresError};
|
||||
use imphnen_entities::seaorm::auth::users::{Entity as UsersEntity, Model as UserModel};
|
||||
use imphnen_entities::seaorm::auth::roles::{Entity as RolesEntity, Model as RoleModel};
|
||||
use sea_orm::{EntityTrait, ActiveModelTrait, Set, TransactionTrait, DbErr, PaginatorTrait};
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🚀 Starting PostgreSQL Connection Test");
|
||||
println!("=====================================");
|
||||
|
||||
// Load configuration from environment
|
||||
let config = PostgresConfig::from_env()?;
|
||||
println!("✅ Configuration loaded successfully");
|
||||
println!(" Database URL: {}", config.database_url.replace("postgres://", "postgres://****:****@"));
|
||||
println!(" Pool size: {}", config.pool_size);
|
||||
println!(" Connect timeout: {}s", config.connect_timeout);
|
||||
println!(" Retry attempts: {}", config.retry_attempts);
|
||||
|
||||
// Test connection
|
||||
println!("\n🔌 Testing PostgreSQL connection...");
|
||||
match test_connection(config).await {
|
||||
Ok(()) => {
|
||||
println!("✅ All PostgreSQL tests passed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ PostgreSQL test failed: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_connection(config: PostgresConfig) -> Result<(), PostgresError> {
|
||||
// Create connection
|
||||
println!(" Creating PostgreSQL connection...");
|
||||
let postgres_conn = PostgresConnection::new(config).await?;
|
||||
let connection = Arc::new(postgres_conn);
|
||||
println!(" ✅ Connection established successfully");
|
||||
|
||||
// Test basic connectivity
|
||||
println!(" Testing basic connectivity...");
|
||||
test_basic_connectivity(&connection).await?;
|
||||
println!(" ✅ Basic connectivity test passed");
|
||||
|
||||
// Test table existence
|
||||
println!(" Testing table existence...");
|
||||
test_table_existence(&connection).await?;
|
||||
println!(" ✅ Table existence test passed");
|
||||
|
||||
// Test CRUD operations
|
||||
println!(" Testing CRUD operations...");
|
||||
test_crud_operations(&connection).await?;
|
||||
println!(" ✅ CRUD operations test passed");
|
||||
|
||||
// Test transaction support
|
||||
println!(" Testing transaction support...");
|
||||
test_transactions(&connection).await?;
|
||||
println!(" ✅ Transaction support test passed");
|
||||
|
||||
// Test error handling
|
||||
println!(" Testing error handling...");
|
||||
test_error_handling(&connection).await?;
|
||||
println!(" ✅ Error handling test passed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_basic_connectivity(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
// Execute a simple query
|
||||
let statement = sea_orm::Statement::from_string(
|
||||
connection.get_database_backend(),
|
||||
"SELECT 1 as test_value, current_timestamp as current_time".to_string()
|
||||
);
|
||||
|
||||
let result = connection.query_one(statement).await?
|
||||
.ok_or_else(|| PostgresError::ConnectionError(sea_orm::DbErr::Custom("No results returned".to_string())))?;
|
||||
|
||||
// Verify we got expected results
|
||||
let test_value: Option<i32> = result.try_get("", "test_value").ok();
|
||||
let current_time: Option<String> = result.try_get("", "current_time").ok();
|
||||
|
||||
if test_value != Some(1) {
|
||||
return Err(PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||
format!("Expected test_value=1, got {:?}", test_value)
|
||||
)));
|
||||
}
|
||||
|
||||
if current_time.is_none() {
|
||||
return Err(PostgresError::ConnectionError(sea_orm::DbErr::Custom(
|
||||
"Expected current_time to be set".to_string()
|
||||
)));
|
||||
}
|
||||
|
||||
println!(" 📝 Query result: test_value={:?}, current_time={:?}", test_value, current_time);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_table_existence(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
// Test if our tables exist
|
||||
use sea_orm::EntityTrait;
|
||||
|
||||
println!(" 📋 Checking users table...");
|
||||
let user_count = UsersEntity::find()
|
||||
.count(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?;
|
||||
println!(" 📊 Users table accessible, current count: {}", user_count);
|
||||
|
||||
println!(" 📋 Checking roles table...");
|
||||
let role_count = RolesEntity::find()
|
||||
.count(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?;
|
||||
println!(" 📊 Roles table accessible, current count: {}", role_count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_crud_operations(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
|
||||
// Create test user
|
||||
println!(" ➕ Creating test user...");
|
||||
let test_user_id = Uuid::new_v4();
|
||||
let now = Utc::now();
|
||||
|
||||
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
||||
id: Set(test_user_id),
|
||||
email: Set(format!("test_user_{}@example.com", test_user_id)),
|
||||
password_hash: Set("test_password_hash".to_string()),
|
||||
username: Set(format!("testuser_{}", test_user_id)),
|
||||
first_name: Set(Some("Test".to_string())),
|
||||
last_name: Set(Some("User".to_string())),
|
||||
avatar_url: Set(None),
|
||||
is_verified: Set(false),
|
||||
is_active: Set(true),
|
||||
metadata: Set(None),
|
||||
role_id: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
deleted_at: Set(None),
|
||||
};
|
||||
|
||||
let created_user = user_model.insert(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?;
|
||||
|
||||
println!(" ✅ Created user with ID: {}", created_user.id);
|
||||
|
||||
// Read user
|
||||
println!(" 🔍 Reading test user...");
|
||||
let found_user = UsersEntity::find_by_id(test_user_id)
|
||||
.one(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?
|
||||
.ok_or_else(|| PostgresError::ConnectionError(sea_orm::DbErr::Custom("User not found after creation".to_string())))?;
|
||||
|
||||
println!(" ✅ Found user: {} ({})", found_user.username, found_user.email);
|
||||
|
||||
// Update user
|
||||
println!(" ✏️ Updating test user...");
|
||||
let mut update_model: imphnen_entities::seaorm::auth::users::ActiveModel = found_user.into();
|
||||
update_model.first_name = Set(Some("Updated".to_string()));
|
||||
update_model.updated_at = Set(Utc::now());
|
||||
|
||||
let updated_user = update_model.update(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?;
|
||||
|
||||
println!(" ✅ Updated user first name to: {:?}", updated_user.first_name);
|
||||
|
||||
// Delete user
|
||||
println!(" 🗑️ Deleting test user...");
|
||||
UsersEntity::delete_by_id(updated_user.id)
|
||||
.exec(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?;
|
||||
|
||||
println!(" ✅ Test user deleted successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_transactions(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
println!(" 💰 Testing transaction support...");
|
||||
|
||||
// Test transaction with rollback
|
||||
let transaction_result = connection.conn.transaction(|txn| {
|
||||
Box::pin(async move {
|
||||
// Create a test user within transaction
|
||||
let test_user_id = Uuid::new_v4();
|
||||
let now = Utc::now();
|
||||
|
||||
let user_model = imphnen_entities::seaorm::auth::users::ActiveModel {
|
||||
id: Set(test_user_id),
|
||||
email: Set(format!("transaction_test_{}@example.com", test_user_id)),
|
||||
password_hash: Set("transaction_password_hash".to_string()),
|
||||
username: Set(format!("transaction_user_{}", test_user_id)),
|
||||
first_name: Set(Some("Transaction".to_string())),
|
||||
last_name: Set(Some("Test".to_string())),
|
||||
avatar_url: Set(None),
|
||||
is_verified: Set(false),
|
||||
is_active: Set(true),
|
||||
metadata: Set(None),
|
||||
role_id: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
deleted_at: Set(None),
|
||||
};
|
||||
|
||||
let _created_user = user_model.insert(txn)
|
||||
.await?;
|
||||
|
||||
// Simulate an error to trigger rollback (return a sea_orm::DbErr so the TransactionError matches)
|
||||
Err::<(), DbErr>(DbErr::Custom("Simulated transaction failure".to_string()))
|
||||
})
|
||||
}).await;
|
||||
|
||||
// Transaction should fail and rollback
|
||||
match transaction_result {
|
||||
Err(e) => {
|
||||
let e_text = format!("{:?}", e);
|
||||
if e_text.contains("Simulated transaction failure") {
|
||||
println!(" ✅ Transaction failed as expected, rollback successful");
|
||||
} else {
|
||||
return Err(PostgresError::OperationFailed(format!("Unexpected transaction result: {}", e_text)));
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
return Err(PostgresError::OperationFailed("Unexpected transaction result: transaction unexpectedly succeeded".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// Verify user was not created (due to rollback)
|
||||
let user_exists = UsersEntity::find_by_id(Uuid::nil()) // Use nil UUID as we don't know the actual ID
|
||||
.one(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?
|
||||
.is_some();
|
||||
|
||||
if user_exists {
|
||||
println!(" ⚠️ User found despite rollback - this might indicate an issue");
|
||||
} else {
|
||||
println!(" ✅ Transaction rollback verified - user not found");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_error_handling(connection: &Arc<PostgresConnection>) -> Result<(), PostgresError> {
|
||||
println!(" ⚠️ Testing error handling...");
|
||||
|
||||
// Test invalid UUID
|
||||
println!(" 🔍 Testing invalid UUID handling...");
|
||||
let invalid_uuid = Uuid::nil(); // This should exist or be handled gracefully
|
||||
|
||||
match UsersEntity::find_by_id(invalid_uuid)
|
||||
.one(&connection.conn)
|
||||
.await
|
||||
.map_err(PostgresError::ConnectionError)?
|
||||
{
|
||||
Some(_) => println!(" ✅ Found user with nil UUID (expected in some cases)"),
|
||||
None => println!(" ✅ No user found with nil UUID (expected)"),
|
||||
}
|
||||
|
||||
// Test invalid query
|
||||
println!(" 🔍 Testing invalid query handling...");
|
||||
let invalid_statement = sea_orm::Statement::from_string(
|
||||
connection.get_database_backend(),
|
||||
"SELECT * FROM non_existent_table".to_string()
|
||||
);
|
||||
|
||||
match connection.execute(invalid_statement).await {
|
||||
Err(_) => println!(" ✅ Invalid query properly handled with error"),
|
||||
Ok(_) => println!(" ⚠️ Invalid query unexpectedly succeeded"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Additional utility functions for comprehensive testing
|
||||
pub mod test_utils {
|
||||
use super::*;
|
||||
|
||||
/// Create a test PostgreSQL configuration
|
||||
pub fn create_test_config() -> PostgresConfig {
|
||||
PostgresConfig {
|
||||
database_url: "postgres://postgres:postgres@localhost:5432/imphnen_test".to_string(),
|
||||
pool_size: 5,
|
||||
connect_timeout: 10,
|
||||
idle_timeout: 30,
|
||||
max_lifetime: Some(600),
|
||||
retry_attempts: 2,
|
||||
retry_delay: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a test user model
|
||||
pub fn create_test_user_model() -> UserModel {
|
||||
UserModel {
|
||||
id: Uuid::new_v4(),
|
||||
email: format!("test_{}@example.com", Uuid::new_v4()),
|
||||
password_hash: "test_password_hash".to_string(),
|
||||
username: format!("testuser_{}", Uuid::new_v4()),
|
||||
first_name: Some("Test".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
avatar_url: None,
|
||||
is_verified: false,
|
||||
is_active: true,
|
||||
metadata: None,
|
||||
role_id: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
deleted_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a test role model
|
||||
pub fn create_test_role_model() -> RoleModel {
|
||||
RoleModel {
|
||||
id: Uuid::new_v4(),
|
||||
name: format!("test_role_{}", Uuid::new_v4()),
|
||||
description: "Test role description".to_string(),
|
||||
permissions: Some(serde_json::json!(["test.permission"])),
|
||||
is_system_role: false,
|
||||
is_default: false,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
deleted_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_test_config() {
|
||||
let config = test_utils::create_test_config();
|
||||
assert_eq!(config.pool_size, 5);
|
||||
assert_eq!(config.connect_timeout, 10);
|
||||
assert!(config.database_url.contains("imphnen_test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_test_user_model() {
|
||||
let user = test_utils::create_test_user_model();
|
||||
assert!(!user.email.is_empty());
|
||||
assert!(!user.username.is_empty());
|
||||
assert!(user.is_active);
|
||||
// is_admin field removed; instead, check role-based permission or is_active
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_test_role_model() {
|
||||
let role = test_utils::create_test_role_model();
|
||||
assert!(!role.name.is_empty());
|
||||
assert!(role.permissions.is_some());
|
||||
assert!(!role.is_system_role);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@ use imphnen_libs::axum_init;
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
axum_init(|surrealdb_ws, surrealdb_mem| async {
|
||||
gateway_service(surrealdb_ws, surrealdb_mem).await
|
||||
let _ = axum_init(|postgres_conn| async {
|
||||
// PostgreSQL is now the primary database - SurrealDB has been completely removed
|
||||
gateway_service(postgres_conn).await
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ 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
|
||||
@@ -25,6 +24,8 @@ 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?://"
|
||||
|
||||
@@ -7,12 +7,14 @@ use super::{
|
||||
};
|
||||
use axum::extract::{Path, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, http::HeaderMap};
|
||||
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,
|
||||
@@ -53,7 +55,11 @@ 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
|
||||
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(
|
||||
@@ -124,6 +130,10 @@ pub async fn delete_event(
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||
EventsService::delete_event(&state, id).await
|
||||
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,16 +1,14 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
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> {
|
||||
lazy_static! {
|
||||
static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://[^\s$.?#].[^\s]*$").unwrap();
|
||||
}
|
||||
if VALID_URL_REGEX.is_match(url) {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -121,7 +119,7 @@ pub struct EventsDetailItemDto {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsQueryDto {
|
||||
pub id: Thing,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub detail_link: String,
|
||||
@@ -138,7 +136,7 @@ pub struct EventsQueryDto {
|
||||
impl EventsQueryDto {
|
||||
pub fn from(self) -> EventsListItemDto {
|
||||
EventsListItemDto {
|
||||
id: self.id.id.to_raw(),
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
detail_link: self.detail_link,
|
||||
|
||||
@@ -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 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 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 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 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 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 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,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::events_dto::{
|
||||
EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto,
|
||||
@@ -10,7 +8,7 @@ use super::events_dto::{
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EventsSchema {
|
||||
pub id: Thing,
|
||||
pub id: String,
|
||||
pub price: f64,
|
||||
pub is_online: bool,
|
||||
pub is_deleted: bool,
|
||||
@@ -27,10 +25,7 @@ pub struct EventsSchema {
|
||||
impl Default for EventsSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Events.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
detail_link: String::new(),
|
||||
@@ -66,10 +61,7 @@ impl EventsSchema {
|
||||
|
||||
pub fn create(payload: EventsCreateRequestDto) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::Events.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
detail_link: payload.detail_link,
|
||||
@@ -86,7 +78,7 @@ impl EventsSchema {
|
||||
|
||||
pub fn update(payload: EventsUpdateRequestDto, id: String) -> Self {
|
||||
Self {
|
||||
id: make_thing(&ResourceEnum::Events.to_string(), &id),
|
||||
id,
|
||||
name: payload.name,
|
||||
price: payload.price,
|
||||
location: payload.location,
|
||||
|
||||
@@ -13,6 +13,7 @@ use imphnen_libs::{
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validate_request,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct EventsService;
|
||||
|
||||
@@ -37,12 +38,12 @@ impl EventsService {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
|
||||
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.id.to_raw(),
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
detail_link: event.detail_link,
|
||||
@@ -70,7 +71,7 @@ impl EventsService {
|
||||
let repo = EventsRepository::new(state);
|
||||
let schema = EventsSchema::create(payload);
|
||||
match repo.query_create_event(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
Ok(msg) => common_response(StatusCode::CREATED, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -86,15 +87,15 @@ impl EventsService {
|
||||
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),
|
||||
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: String) -> Response {
|
||||
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),
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ use imphnen_libs::{
|
||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||
ResponseSuccessDto, ValidatedJson,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::common_response;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -53,7 +55,11 @@ 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
|
||||
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(
|
||||
@@ -127,6 +133,10 @@ pub async fn delete_testimonial(
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
require_auth!(headers, state, {
|
||||
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
|
||||
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,8 +1,6 @@
|
||||
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};
|
||||
|
||||
@@ -76,8 +74,9 @@ pub struct TestimonialsDetailItemDto {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TestimonialsQueryDto {
|
||||
pub id: Thing,
|
||||
pub user: UsersSchema,
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub user_fullname: String,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub is_deleted: bool,
|
||||
@@ -88,9 +87,9 @@ pub struct TestimonialsQueryDto {
|
||||
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,
|
||||
id: self.id,
|
||||
user_id: self.user_id,
|
||||
user_fullname: self.user_fullname,
|
||||
role: self.role,
|
||||
content: self.content,
|
||||
created_at: self.created_at,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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 sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, ActiveModelTrait, QueryOrder};
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
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,
|
||||
@@ -18,54 +20,116 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
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 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 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 data = ResponseListSuccessDto {
|
||||
data: res,
|
||||
meta: None,
|
||||
|
||||
let response = ResponseListSuccessDto {
|
||||
data,
|
||||
meta: Some(imphnen_entities::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total: Some(total_pages),
|
||||
}),
|
||||
};
|
||||
Ok(data)
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_testimonial_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
id: Uuid,
|
||||
) -> 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 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 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"
|
||||
@@ -73,33 +137,38 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
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"),
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_testimonial(
|
||||
&self,
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<TestimonialsSchema> { // Change return type from String to TestimonialsSchema
|
||||
) -> Result<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 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"
|
||||
@@ -107,10 +176,7 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
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"),
|
||||
}
|
||||
Ok(created_testimonial)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
@@ -119,28 +185,25 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
data: TestimonialsSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let db = self.get_db();
|
||||
|
||||
let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?;
|
||||
let existing = self.query_testimonial_by_id(Uuid::parse_str(&data.id)?).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Testimonial already deleted");
|
||||
return Err(AppError::BadRequestError("Testimonial already deleted".to_string()));
|
||||
}
|
||||
|
||||
let merged = TestimonialsSchema {
|
||||
created_at: existing.created_at,
|
||||
updated_at: get_iso_date(),
|
||||
user: existing.user.id,
|
||||
..data
|
||||
};
|
||||
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 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"
|
||||
@@ -148,30 +211,30 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
println!("Query 'query_update_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success update testimonial".into()),
|
||||
None => bail!("Failed to update testimonial"),
|
||||
}
|
||||
Ok("Success update testimonial".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_testimonial(&self, id: String) -> Result<String> {
|
||||
pub async fn query_delete_testimonial(&self, id: Uuid) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let db = self.get_db();
|
||||
|
||||
let testimonial = self.query_testimonial_by_id(id).await?;
|
||||
if testimonial.is_deleted {
|
||||
bail!("Testimonial not found");
|
||||
return Err(AppError::NotFoundError("Testimonial not found".to_string()));
|
||||
}
|
||||
|
||||
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 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"
|
||||
@@ -179,9 +242,6 @@ impl<'a> TestimonialsRepository<'a> {
|
||||
println!("Query 'query_delete_testimonial' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match record {
|
||||
Some(_) => Ok("Success delete testimonial".into()),
|
||||
None => bail!("Failed to delete testimonial"),
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use imphnen_libs::{
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, success_created_response, validate_request,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct TestimonialsService;
|
||||
|
||||
@@ -40,15 +41,15 @@ impl TestimonialsService {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_testimonial_by_id(state: &AppState, id: String) -> Response {
|
||||
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.to_raw(),
|
||||
user_id: testimonial.user.id.to_raw(),
|
||||
user_fullname: testimonial.user.fullname,
|
||||
id: testimonial.id,
|
||||
user_id: testimonial.user_id,
|
||||
user_fullname: testimonial.user_fullname,
|
||||
role: testimonial.role,
|
||||
content: testimonial.content,
|
||||
created_at: testimonial.created_at,
|
||||
@@ -75,8 +76,8 @@ impl TestimonialsService {
|
||||
Ok(created_testimonial) => {
|
||||
success_created_response(ResponseSuccessDto {
|
||||
data: TestimonialsDetailItemDto {
|
||||
id: created_testimonial.id.to_raw(),
|
||||
user_id: created_testimonial.user.id.to_raw(),
|
||||
id: created_testimonial.id,
|
||||
user_id: created_testimonial.user_id,
|
||||
user_fullname: authenticated_user.fullname.clone(),
|
||||
role: created_testimonial.role,
|
||||
content: created_testimonial.content,
|
||||
@@ -101,19 +102,19 @@ impl TestimonialsService {
|
||||
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),
|
||||
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: String,
|
||||
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),
|
||||
Ok(msg) => common_response(StatusCode::OK, msg.as_str()),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ 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
|
||||
@@ -26,6 +25,8 @@ anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
sea-orm.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dotenvy.workspace = true
|
||||
|
||||
@@ -9,6 +9,9 @@ use ::axum::{
|
||||
response::Response,
|
||||
};
|
||||
use imphnen_entities::MetaRequestDto;
|
||||
use uuid::Uuid;
|
||||
use axum::http::StatusCode;
|
||||
use imphnen_utils::common_response;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::extract_email;
|
||||
@@ -82,6 +85,17 @@ pub async fn get_mentor_by_id(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::ReadDetailMentors], {
|
||||
MentorsService::get_mentor_by_id(&app_state, &id).await
|
||||
})
|
||||
@@ -111,6 +125,17 @@ pub async fn put_update_mentor(
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::UpdateMentors], {
|
||||
MentorsService::update_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
@@ -137,6 +162,17 @@ pub async fn delete_mentor(
|
||||
Extension(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::DeleteMentors], {
|
||||
MentorsService::delete_mentor(&app_state, &id).await
|
||||
})
|
||||
@@ -166,6 +202,17 @@ pub async fn put_verify_mentor(
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::VerifyMentors], {
|
||||
MentorsService::verify_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::v1::mentors::MentorSchema;
|
||||
use imphnen_utils::extract_id;
|
||||
use crate::v1::sessions::sessions_schema::Thing;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
@@ -16,8 +15,8 @@ pub struct MentorListResponseDto {
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorDetailWithUserDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Thing,
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
// Personal data is now in UsersSchema, access via user_id
|
||||
// Removed: fullname, email, legal_name, identity_document_url,
|
||||
// phone_for_verification, bio, linkedin_url, github_url, cv_url
|
||||
@@ -31,7 +30,7 @@ pub struct MentorDetailWithUserDto {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
@@ -65,7 +64,7 @@ pub struct MentorDetailResponseDto {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
@@ -173,8 +172,8 @@ pub struct MentorUserRegisterRequestDto {
|
||||
pub password: String,
|
||||
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||
pub fullname: String,
|
||||
#[validate(length(min = 1, message = "Phone number is required"))]
|
||||
pub phone_number: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_number: Option<String>,
|
||||
#[validate(nested)]
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
#[validate(nested)]
|
||||
@@ -211,7 +210,8 @@ pub struct IdentityAndVerification {
|
||||
max = 15,
|
||||
message = "Phone must be 10-15 characters"
|
||||
))]
|
||||
pub phone_for_verification: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
@@ -287,7 +287,7 @@ pub struct MentorInsertDto {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
@@ -343,7 +343,7 @@ pub struct MentorDetailQueryDto {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
@@ -353,7 +353,7 @@ pub struct MentorDetailQueryDto {
|
||||
impl From<MentorDetailQueryDto> for MentorListResponseDto {
|
||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: extract_id(&dto.id),
|
||||
id: dto.id.clone(),
|
||||
fullname: None, // now in user table, must be populated from service layer
|
||||
email: None, // now in user table, must be populated from service layer
|
||||
status: dto.status,
|
||||
@@ -366,8 +366,8 @@ impl From<MentorDetailQueryDto> for MentorListResponseDto {
|
||||
impl From<MentorDetailQueryDto> for MentorDetailResponseDto {
|
||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: extract_id(&dto.id),
|
||||
user_id: extract_id(&dto.user_id),
|
||||
id: dto.id.clone(),
|
||||
user_id: dto.user_id.clone(),
|
||||
// Personal data fields are populated in service layer from UsersSchema
|
||||
fullname: None, // populated from user table in service layer
|
||||
email: None, // populated from user table in service layer
|
||||
@@ -403,7 +403,7 @@ impl From<MentorSchema> for MentorRegisterResponseDto {
|
||||
fn from(schema: MentorSchema) -> Self {
|
||||
Self {
|
||||
id: schema.id.to_string(),
|
||||
user_id: schema.user_id.map(|id| extract_id(&id)).unwrap_or_default(),
|
||||
user_id: schema.user_id.unwrap_or_default(),
|
||||
email: None, // schema.email - now in user table
|
||||
status: schema.status,
|
||||
created_at: schema.created_at,
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
use crate::v1::mentors::mentors_schema::MentorSchema;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_libs::AppStatePostgresExt;
|
||||
use imphnen_entities::seaorm::auth::mentors::{Entity as Mentors, ActiveModel as MentorActiveModel, Column as MentorColumn};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as Users;
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::{get_id, make_thing};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use crate::v1::mentors::mentors_dto::MentorDetailWithUserDto;
|
||||
use crate::v1::mentors::{MentorInsertDto, MentorSchema};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
|
||||
use serde_json::{Map, Value};
|
||||
use sea_orm::*;
|
||||
use sea_orm::EntityTrait;
|
||||
use uuid::Uuid;
|
||||
use sea_orm::ActiveModelTrait as ActiveModelTraitSpecific;
|
||||
use anyhow::anyhow;
|
||||
use imphnen_entities::{MetaRequestDto, ResponseListSuccessDto};
|
||||
use imphnen_utils::Result as UtilsResult;
|
||||
use crate::v1::mentors::mentors_dto::MentorDetailQueryDto;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
use tracing::{instrument, info};
|
||||
use serde_json;
|
||||
use chrono::Utc;
|
||||
|
||||
pub struct MentorsRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> MentorsRepository<'a> {
|
||||
@@ -20,48 +26,107 @@ impl<'a> MentorsRepository<'a> {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_mentor_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<MentorDetailWithUserDto>>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentors_table = ResourceEnum::Mentors.to_string();
|
||||
let builder = QueryListBuilder::new(db, &mentors_table, &meta)
|
||||
.search_field("user_id.legal_name") // Search in user data instead
|
||||
.select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
// Personal data comes from user relation, not mentor table
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
"current_company",
|
||||
"current_role",
|
||||
"years_of_experience",
|
||||
"topics_of_interest",
|
||||
"preferred_mentee_level",
|
||||
"preferred_mentoring_formats",
|
||||
"availability_commitment",
|
||||
"mentoring_rate",
|
||||
"status",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
let result = builder.build().await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_mentor_list' took: {elapsed:.2?}");
|
||||
fn get_db(&self) -> &DatabaseConnection {
|
||||
self.state.postgres_db()
|
||||
}
|
||||
let data = result.data.into_iter().collect();
|
||||
Ok(ResponseListSuccessDto {
|
||||
data,
|
||||
meta: result.meta,
|
||||
})
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let mentor_active_model = MentorActiveModel {
|
||||
user_id: ActiveValue::Set(Uuid::parse_str(&data.user_id.unwrap_or_default())?),
|
||||
industries: ActiveValue::Set(Some(data.industries.into())),
|
||||
expertise: ActiveValue::Set(Some(data.expertise.into())),
|
||||
languages: ActiveValue::Set(Some(data.languages.into())),
|
||||
current_company: ActiveValue::Set(Some(data.current_company)),
|
||||
current_role: ActiveValue::Set(Some(data.current_role)),
|
||||
years_of_experience: ActiveValue::Set(Some(data.years_of_experience)),
|
||||
topics_of_interest: ActiveValue::Set(Some(data.topics_of_interest.into())),
|
||||
preferred_mentee_level: ActiveValue::Set(Some(serde_json::to_string(&data.preferred_mentee_level).unwrap())),
|
||||
preferred_mentoring_formats: ActiveValue::Set(Some(data.preferred_mentoring_formats.into())),
|
||||
availability_commitment: ActiveValue::Set(Some(data.availability_commitment)),
|
||||
mentoring_rate: ActiveValue::Set(Some(data.mentoring_rate)),
|
||||
status: ActiveValue::Set(Some(data.status)),
|
||||
created_at: ActiveValue::Set(Utc::now()),
|
||||
updated_at: ActiveValue::Set(Utc::now()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
info!("Executing PostgreSQL create in query_create_mentor");
|
||||
let result = <MentorActiveModel as sea_orm::ActiveModelTrait>::insert(mentor_active_model, self.get_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_create_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(result.id.to_string())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let mentor_id = Uuid::parse_str(&data.id)?;
|
||||
let existing_mentor = Mentors::find_by_id(mentor_id)
|
||||
.one(self.get_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Mentor not found"))?;
|
||||
|
||||
let mut mentor_active_model: MentorActiveModel = existing_mentor.into();
|
||||
|
||||
if !data.industries.is_empty() { mentor_active_model.industries = ActiveValue::Set(Some(serde_json::to_value(data.industries).unwrap())); }
|
||||
if !data.expertise.is_empty() { mentor_active_model.expertise = ActiveValue::Set(Some(serde_json::to_value(data.expertise).unwrap())); }
|
||||
if !data.languages.is_empty() { mentor_active_model.languages = ActiveValue::Set(Some(serde_json::to_value(data.languages).unwrap())); }
|
||||
if !data.current_company.is_empty() { mentor_active_model.current_company = ActiveValue::Set(Some(data.current_company)); }
|
||||
if !data.current_role.is_empty() { mentor_active_model.current_role = ActiveValue::Set(Some(data.current_role)); }
|
||||
mentor_active_model.years_of_experience = ActiveValue::Set(Some(data.years_of_experience));
|
||||
if !data.topics_of_interest.is_empty() { mentor_active_model.topics_of_interest = ActiveValue::Set(Some(serde_json::to_value(data.topics_of_interest).unwrap())); }
|
||||
if !data.preferred_mentee_level.is_empty() { mentor_active_model.preferred_mentee_level = ActiveValue::Set(Some(serde_json::to_string(&data.preferred_mentee_level).unwrap())); }
|
||||
if !data.preferred_mentoring_formats.is_empty() { mentor_active_model.preferred_mentoring_formats = ActiveValue::Set(Some(serde_json::to_value(data.preferred_mentoring_formats).unwrap())); }
|
||||
if !data.availability_commitment.is_empty() { mentor_active_model.availability_commitment = ActiveValue::Set(Some(data.availability_commitment)); }
|
||||
mentor_active_model.mentoring_rate = ActiveValue::Set(Some(data.mentoring_rate));
|
||||
if !data.status.is_empty() { mentor_active_model.status = ActiveValue::Set(Some(data.status)); }
|
||||
mentor_active_model.updated_at = ActiveValue::Set(Utc::now());
|
||||
|
||||
info!("Executing PostgreSQL update in query_update_mentor");
|
||||
let result = mentor_active_model.update(self.get_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_update_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(format!("Success update mentor: {}", result.id))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_mentor(&self, id: &str) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let mentor = Mentors::find_by_id(Uuid::parse_str(id)?)
|
||||
.one(self.get_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Mentor not found"))?;
|
||||
|
||||
if mentor.is_deleted {
|
||||
bail!("Mentor is already soft deleted");
|
||||
}
|
||||
|
||||
let mut mentor_active_model: MentorActiveModel = mentor.into();
|
||||
mentor_active_model.is_deleted = ActiveValue::Set(true);
|
||||
mentor_active_model.updated_at = ActiveValue::Set(Utc::now());
|
||||
|
||||
info!("Executing PostgreSQL soft delete in query_delete_mentor");
|
||||
let result = mentor_active_model.update(self.get_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_delete_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(format!("Success soft delete mentor: {}", result.id))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, email, include_deleted), err)]
|
||||
@@ -69,228 +134,190 @@ impl<'a> MentorsRepository<'a> {
|
||||
&self,
|
||||
email: String,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorDetailWithUserDto> {
|
||||
) -> UtilsResult<MentorDetailQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
|
||||
.with_where("user_id.email", Some(email.clone())) // Search in user table
|
||||
.with_select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
// Personal data comes from user relation, not mentor table
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
"current_company",
|
||||
"current_role",
|
||||
"years_of_experience",
|
||||
"topics_of_interest",
|
||||
"preferred_mentee_level",
|
||||
"preferred_mentoring_formats",
|
||||
"availability_commitment",
|
||||
"mentoring_rate",
|
||||
"status",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
let db = self.get_db();
|
||||
|
||||
let mut query = Mentors::find()
|
||||
.find_also_related(Users);
|
||||
|
||||
if !include_deleted {
|
||||
builder = builder.with_condition("is_deleted = false");
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_email");
|
||||
let mentor_opt: Option<MentorDetailWithUserDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let (mentor, user) = query
|
||||
.filter(imphnen_entities::seaorm::auth::users::Column::Email.eq(email))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Mentor not found"))?;
|
||||
|
||||
let _user = user.ok_or_else(|| anyhow::anyhow!("User not found for mentor"))?;
|
||||
|
||||
let result = MentorDetailQueryDto {
|
||||
id: mentor.id.to_string(),
|
||||
user_id: mentor.user_id.to_string(),
|
||||
industries: serde_json::from_value(mentor.industries.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
expertise: serde_json::from_value(mentor.expertise.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
languages: serde_json::from_value(mentor.languages.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
current_company: mentor.current_company.unwrap_or_default(),
|
||||
current_role: mentor.current_role.unwrap_or_default(),
|
||||
years_of_experience: mentor.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(mentor.topics_of_interest.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(&mentor.preferred_mentee_level.unwrap_or_default()).unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(mentor.preferred_mentoring_formats.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
availability_commitment: mentor.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: mentor.mentoring_rate.unwrap_or(0.0),
|
||||
status: mentor.status.unwrap_or_default(),
|
||||
is_deleted: mentor.is_deleted,
|
||||
created_at: mentor.created_at.to_rfc3339(),
|
||||
updated_at: mentor.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_mentor_by_email' took: {elapsed:.2?}");
|
||||
}
|
||||
let Some(mentor) = mentor_opt else {
|
||||
bail!("Mentor not found");
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_mentor_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> UtilsResult<ResponseListSuccessDto<Vec<MentorDetailQueryDto>>> {
|
||||
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 = Mentors::find()
|
||||
.filter(MentorColumn::IsDeleted.eq(false))
|
||||
.find_also_related(Users);
|
||||
|
||||
// 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(MentorColumn::CreatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(MentorColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
"updated_at" => {
|
||||
if order == "desc" {
|
||||
query = query.order_by_desc(MentorColumn::UpdatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(MentorColumn::UpdatedAt);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
query = query.order_by_desc(MentorColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query = query.order_by_desc(MentorColumn::CreatedAt);
|
||||
}
|
||||
|
||||
let paginator = query.paginate(db, per_page);
|
||||
let total_pages = paginator.num_pages().await?;
|
||||
let mentors = paginator.fetch_page(page - 1).await?;
|
||||
|
||||
let data: Vec<MentorDetailQueryDto> = mentors
|
||||
.into_iter()
|
||||
.filter_map(|(mentor, user)| {
|
||||
user.map(|_u| MentorDetailQueryDto {
|
||||
id: mentor.id.to_string(),
|
||||
user_id: mentor.user_id.to_string(),
|
||||
industries: serde_json::from_value(mentor.industries.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
expertise: serde_json::from_value(mentor.expertise.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
languages: serde_json::from_value(mentor.languages.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
current_company: mentor.current_company.unwrap_or_default(),
|
||||
current_role: mentor.current_role.unwrap_or_default(),
|
||||
years_of_experience: mentor.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(mentor.topics_of_interest.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(&mentor.preferred_mentee_level.unwrap_or_default()).unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(mentor.preferred_mentoring_formats.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
availability_commitment: mentor.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: mentor.mentoring_rate.unwrap_or(0.0),
|
||||
status: mentor.status.unwrap_or_default(),
|
||||
is_deleted: mentor.is_deleted,
|
||||
created_at: mentor.created_at.to_rfc3339(),
|
||||
updated_at: mentor.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_mentor_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(mentor)
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, include_deleted), err)]
|
||||
pub async fn query_mentor_by_id(
|
||||
&self,
|
||||
id: &Thing,
|
||||
id: &str,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorDetailWithUserDto> {
|
||||
) -> UtilsResult<MentorDetailQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let db = self.get_db();
|
||||
|
||||
// Validate ID format first
|
||||
let mentor_id = match get_id(id) {
|
||||
Ok((_, id_str)) => id_str,
|
||||
Err(_) => bail!("Invalid mentor ID format"),
|
||||
};
|
||||
let mentor_id = Uuid::parse_str(id).map_err(|e| anyhow!("Invalid mentor ID: {}", e))?;
|
||||
|
||||
let mentors_table = ResourceEnum::Mentors.to_string();
|
||||
|
||||
// Build query with proper ID binding
|
||||
let mut builder = DetailQueryBuilder::new(mentors_table.clone())
|
||||
.with_id(mentor_id) // Use the extracted ID string
|
||||
.with_select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
// Personal data comes from user relation, not mentor table
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
"current_company",
|
||||
"current_role",
|
||||
"years_of_experience",
|
||||
"topics_of_interest",
|
||||
"preferred_mentee_level",
|
||||
"preferred_mentoring_formats",
|
||||
"availability_commitment",
|
||||
"mentoring_rate",
|
||||
"status",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
let mut query = Mentors::find_by_id(mentor_id)
|
||||
.find_also_related(Users);
|
||||
|
||||
if !include_deleted {
|
||||
builder = builder.with_condition("is_deleted = false");
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_id");
|
||||
|
||||
let mentor_opt: Option<MentorDetailWithUserDto> = builder
|
||||
.apply_bindings(db.query(sql))
|
||||
let (mentor, user) = query
|
||||
.one(db)
|
||||
.await?
|
||||
.take(0)?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Mentor not found"))?;
|
||||
|
||||
let _user = user.ok_or_else(|| anyhow::anyhow!("User not found for mentor"))?;
|
||||
|
||||
let result = MentorDetailQueryDto {
|
||||
id: mentor.id.to_string(),
|
||||
user_id: mentor.user_id.to_string(),
|
||||
industries: serde_json::from_value(mentor.industries.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
expertise: serde_json::from_value(mentor.expertise.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
languages: serde_json::from_value(mentor.languages.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
current_company: mentor.current_company.unwrap_or_default(),
|
||||
current_role: mentor.current_role.unwrap_or_default(),
|
||||
years_of_experience: mentor.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(mentor.topics_of_interest.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(&mentor.preferred_mentee_level.unwrap_or_default()).unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(mentor.preferred_mentoring_formats.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
availability_commitment: mentor.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: mentor.mentoring_rate.unwrap_or(0.0),
|
||||
status: mentor.status.unwrap_or_default(),
|
||||
is_deleted: mentor.is_deleted,
|
||||
created_at: mentor.created_at.to_rfc3339(),
|
||||
updated_at: mentor.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let Some(mentor) = mentor_opt else {
|
||||
bail!("Mentor not found");
|
||||
};
|
||||
|
||||
Ok(mentor)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let dto: MentorInsertDto = data.into();
|
||||
let resource = ResourceEnum::Mentors.to_string();
|
||||
info!(query = %resource, "Executing SurrealDB create in query_create_mentor");
|
||||
let record: Option<MentorSchema> = db
|
||||
.create(resource)
|
||||
.content(dto.clone())
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(mentor) => {
|
||||
let id_str = mentor.id.id.to_raw();
|
||||
let _user = format!("{:?}", mentor.user_id);
|
||||
Ok(id_str)
|
||||
}
|
||||
None => {
|
||||
bail!("Failed to create mentor")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let id_ref = &data.id;
|
||||
let record_key = get_id(id_ref)?;
|
||||
let _existing = self.query_mentor_by_id(id_ref, false).await?;
|
||||
|
||||
let mut merged_data_json: Map<String, Value> =
|
||||
serde_json::to_value(data.clone())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to serialize MentorSchema: {}", e))?
|
||||
.as_object()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
merged_data_json.remove("id");
|
||||
merged_data_json.remove("user_id");
|
||||
merged_data_json.remove("created_at");
|
||||
|
||||
merged_data_json.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
|
||||
info!(query = ?record_key, "Executing SurrealDB update in query_update_mentor");
|
||||
let record: Option<MentorSchema> =
|
||||
db.update(record_key).merge(merged_data_json).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success update mentor".into()),
|
||||
None => {
|
||||
bail!("Failed to update mentor")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_mentor(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let thing = make_thing(ResourceEnum::Mentors.to_string().as_str(), &id);
|
||||
let record_key = get_id(&thing)?;
|
||||
|
||||
let mentor_to_delete_res = self.query_mentor_by_id(&thing, true).await;
|
||||
|
||||
let _mentor_to_delete = match mentor_to_delete_res {
|
||||
Ok(mentor) => {
|
||||
if mentor.is_deleted {
|
||||
bail!("Mentor is already soft deleted");
|
||||
}
|
||||
mentor
|
||||
}
|
||||
Err(e) => {
|
||||
if e.to_string().contains("Mentor has been deleted") {
|
||||
bail!("Mentor is already soft deleted");
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut patch = Map::new();
|
||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
|
||||
info!(query = ?record_key, "Executing SurrealDB soft delete in query_delete_mentor");
|
||||
let record: Option<MentorSchema> = db.update(record_key).merge(patch).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success soft delete mentor".into()),
|
||||
None => {
|
||||
bail!("Failed to soft delete mentor")
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use super::{
|
||||
MentorDetailQueryDto, MentorUpdateRequestDto,
|
||||
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||
MentoringLogistics, ProfessionalProfile,
|
||||
};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use crate::v1::sessions::sessions_schema::Thing;
|
||||
use imphnen_entities::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorSchema {
|
||||
@@ -25,7 +26,7 @@ pub struct MentorSchema {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
@@ -53,11 +54,7 @@ impl Default for MentorSchema {
|
||||
preferred_mentee_level: Vec::new(),
|
||||
preferred_mentoring_formats: Vec::new(),
|
||||
availability_commitment: String::new(),
|
||||
mentoring_rate: MentoringRate {
|
||||
amount: 0,
|
||||
currency: "IDR".to_string(),
|
||||
per_duration: "hour".to_string(),
|
||||
},
|
||||
mentoring_rate: 0.0,
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
@@ -89,11 +86,7 @@ impl MentorSchema {
|
||||
preferred_mentee_level: mentoring_logistics.preferred_mentee_level,
|
||||
preferred_mentoring_formats: mentoring_logistics.preferred_mentoring_formats,
|
||||
availability_commitment: mentoring_logistics.availability_commitment,
|
||||
mentoring_rate: MentoringRate {
|
||||
amount: mentoring_logistics.mentoring_rate_amount,
|
||||
currency: "IDR".to_string(),
|
||||
per_duration: "hour".to_string(),
|
||||
},
|
||||
mentoring_rate: mentoring_logistics.mentoring_rate_amount as f64,
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
@@ -160,7 +153,7 @@ impl MentorSchema {
|
||||
self.availability_commitment = val;
|
||||
}
|
||||
if let Some(val) = dto.mentoring_rate_amount {
|
||||
self.mentoring_rate.amount = val;
|
||||
self.mentoring_rate = val as f64;
|
||||
}
|
||||
|
||||
self.updated_at = get_iso_date();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::v1::mentors::{
|
||||
MentorDetailQueryDto, MentorDetailResponseDto, MentorListResponseDto,
|
||||
MentorDetailResponseDto, MentorListResponseDto,
|
||||
MentorRegisterResponseDto, MentorSchema, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsRepository,
|
||||
};
|
||||
@@ -10,14 +10,14 @@ use imphnen_entities::{
|
||||
};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_iam::{
|
||||
AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema,
|
||||
v1::auth::AuthRepository,
|
||||
RolesEnum, RolesRepository, UsersRepository, UsersSchema,
|
||||
};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_libs::argon::hash_password;
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validate_request,
|
||||
common_response, success_list_response, success_response, validator::validate_request,
|
||||
};
|
||||
use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
use uuid::Uuid;
|
||||
use tracing::error;
|
||||
|
||||
pub struct MentorsService;
|
||||
@@ -34,7 +34,7 @@ impl MentorsService {
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let mentor_repo = MentorsRepository::new(state);
|
||||
let role_repo = RolesRepository::new(state);
|
||||
let auth_repo = AuthRepository::new(state.surrealdb_mem.clone());
|
||||
let _auth_repo = AuthRepository::new(state);
|
||||
|
||||
let user_email = &dto.email;
|
||||
let mut _user_to_update: Option<UsersSchema> = None;
|
||||
@@ -59,23 +59,23 @@ impl MentorsService {
|
||||
|
||||
let mut user_schema = UsersSchema::from(user_detail_query_dto);
|
||||
|
||||
user_schema.fullname = dto.fullname.clone();
|
||||
user_schema.phone_number = dto.phone_number.clone();
|
||||
// Update personal data from identity_and_verification
|
||||
user_schema.legal_name = Some(dto.identity_and_verification.legal_name.clone());
|
||||
user_schema.gender = dto.identity_and_verification.gender.clone();
|
||||
user_schema.domicile = dto.identity_and_verification.domicile.clone();
|
||||
user_schema.phone_for_verification = Some(dto.identity_and_verification.phone_for_verification.clone());
|
||||
// Update personal data from professional_profile
|
||||
user_schema.bio = Some(dto.professional_profile.bio.clone());
|
||||
user_schema.last_education = dto.professional_profile.last_education.clone();
|
||||
user_schema.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||
user_schema.github_url = dto.professional_profile.github_url.clone();
|
||||
user_schema.cv_url = dto.professional_profile.cv_url.clone();
|
||||
user_schema.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||
user_schema.fullname = Some(dto.fullname.clone());
|
||||
// Update profile_extension fields
|
||||
let mut profile_ext = user_schema.profile_extension.clone().unwrap_or_default();
|
||||
profile_ext.phone_number = dto.phone_number.clone();
|
||||
profile_ext.phone_for_verification = dto.identity_and_verification.phone_for_verification.clone();
|
||||
profile_ext.gender = dto.identity_and_verification.gender.clone();
|
||||
profile_ext.domicile = dto.identity_and_verification.domicile.clone();
|
||||
profile_ext.bio = Some(dto.professional_profile.bio.clone());
|
||||
profile_ext.last_education = dto.professional_profile.last_education.clone();
|
||||
profile_ext.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||
profile_ext.github_url = dto.professional_profile.github_url.clone();
|
||||
profile_ext.cv_url = dto.professional_profile.cv_url.clone();
|
||||
profile_ext.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||
user_schema.profile_extension = Some(profile_ext);
|
||||
user_schema.updated_at = imphnen_utils::get_iso_date();
|
||||
|
||||
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
|
||||
let hashed_password = match hash_password(&dto.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
@@ -88,7 +88,7 @@ impl MentorsService {
|
||||
);
|
||||
}
|
||||
};
|
||||
user_schema.password = hashed_password;
|
||||
user_schema.password = Some(hashed_password);
|
||||
|
||||
let mentor_role = match role_repo
|
||||
.query_role_by_name(RolesEnum::Mentor.to_string())
|
||||
@@ -99,8 +99,7 @@ impl MentorsService {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found");
|
||||
}
|
||||
};
|
||||
user_schema.role =
|
||||
imphnen_utils::make_thing_from_enum(ResourceEnum::Roles, &mentor_role.id);
|
||||
user_schema.mentor_id = Some(imphnen_utils::make_thing_from_enum("Roles", &mentor_role.id));
|
||||
user_schema.is_active = false;
|
||||
|
||||
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
|
||||
@@ -125,7 +124,7 @@ impl MentorsService {
|
||||
}
|
||||
};
|
||||
|
||||
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
|
||||
let hashed_password = match hash_password(&dto.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
@@ -139,37 +138,42 @@ impl MentorsService {
|
||||
}
|
||||
};
|
||||
|
||||
let new_user_schema = UsersSchema {
|
||||
let mut new_user_schema = UsersSchema {
|
||||
id: imphnen_utils::make_thing_from_enum(
|
||||
ResourceEnum::Users,
|
||||
"Users",
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
email: dto.email,
|
||||
fullname: dto.fullname,
|
||||
password: hashed_password,
|
||||
phone_number: dto.phone_number,
|
||||
email: Some(dto.email),
|
||||
fullname: Some(dto.fullname),
|
||||
password: Some(hashed_password),
|
||||
// Set phone number in profile extension instead
|
||||
// Store personal data from identity_and_verification in user
|
||||
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
||||
gender: dto.identity_and_verification.gender.clone(),
|
||||
domicile: dto.identity_and_verification.domicile.clone(),
|
||||
phone_for_verification: Some(dto.identity_and_verification.phone_for_verification.clone()),
|
||||
// Use profile_extension for these fields
|
||||
// Store personal data from professional_profile in user
|
||||
bio: Some(dto.professional_profile.bio.clone()),
|
||||
last_education: dto.professional_profile.last_education.clone(),
|
||||
linkedin_url: dto.professional_profile.linkedin_url.clone(),
|
||||
github_url: dto.professional_profile.github_url.clone(),
|
||||
cv_url: dto.professional_profile.cv_url.clone(),
|
||||
portfolio_url: dto.professional_profile.portfolio_url.clone(),
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
role: imphnen_utils::make_thing_from_enum(
|
||||
ResourceEnum::Roles,
|
||||
mentor_id: Some(imphnen_utils::make_thing_from_enum(
|
||||
"Roles",
|
||||
&mentor_role.id,
|
||||
),
|
||||
)),
|
||||
is_active: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// populate profile_extension
|
||||
let mut profile_ext = new_user_schema.profile_extension.clone().unwrap_or_default();
|
||||
profile_ext.phone_number = dto.phone_number.clone();
|
||||
profile_ext.phone_for_verification = dto.identity_and_verification.phone_for_verification.clone();
|
||||
profile_ext.gender = dto.identity_and_verification.gender.clone();
|
||||
profile_ext.domicile = dto.identity_and_verification.domicile.clone();
|
||||
profile_ext.bio = Some(dto.professional_profile.bio.clone());
|
||||
profile_ext.last_education = dto.professional_profile.last_education.clone();
|
||||
profile_ext.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||
profile_ext.github_url = dto.professional_profile.github_url.clone();
|
||||
profile_ext.cv_url = dto.professional_profile.cv_url.clone();
|
||||
profile_ext.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||
new_user_schema.profile_extension = Some(profile_ext);
|
||||
user_id = new_user_schema.id.clone();
|
||||
|
||||
match user_repo.query_create_user(new_user_schema).await {
|
||||
@@ -184,37 +188,12 @@ impl MentorsService {
|
||||
}
|
||||
}
|
||||
|
||||
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
|
||||
|
||||
match auth_repo
|
||||
.query_store_otp(final_user_email.clone(), otp.clone())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let message = format!("your otp code is {}", otp.code);
|
||||
if let Err(_err) =
|
||||
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
|
||||
{
|
||||
error!("Failed to send OTP email to {}: {}", final_user_email, _err);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_err) => {
|
||||
error!("Failed to store OTP for {}: {}", final_user_email, _err);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Skip OTP for now - implement later if needed
|
||||
|
||||
let mentor_schema = MentorSchema::create(
|
||||
dto.professional_profile,
|
||||
dto.mentoring_logistics,
|
||||
user_id.to_raw(),
|
||||
user_id.clone(),
|
||||
);
|
||||
|
||||
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
|
||||
@@ -263,7 +242,7 @@ impl MentorsService {
|
||||
let mut mentor_list_data: Vec<MentorListResponseDto> = Vec::new();
|
||||
|
||||
for mentor_with_user in result.data {
|
||||
let mentor_dto = MentorDetailQueryDto::from(mentor_with_user);
|
||||
let mentor_dto = mentor_with_user;
|
||||
let mut list_item = MentorListResponseDto::from(mentor_dto.clone());
|
||||
|
||||
// Get user data to populate personal fields
|
||||
@@ -287,9 +266,7 @@ impl MentorsService {
|
||||
pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response {
|
||||
let mentor_repo = MentorsRepository::new(state);
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
|
||||
match mentor_repo.query_mentor_by_id(&thing_id, false).await {
|
||||
let thing_id = imphnen_utils::make_thing_from_enum("Mentors", id); match mentor_repo.query_mentor_by_id(&thing_id, false).await {
|
||||
Ok(mentor) => {
|
||||
// Get user data separately
|
||||
let user_result = user_repo.query_user_by_id(&mentor.user_id).await;
|
||||
@@ -297,21 +274,21 @@ impl MentorsService {
|
||||
Ok(user) => {
|
||||
// Combine mentor and user data
|
||||
let dto = MentorDetailResponseDto {
|
||||
id: mentor.id.to_raw(),
|
||||
user_id: mentor.user_id.to_raw(),
|
||||
id: mentor.id.clone(),
|
||||
user_id: mentor.user_id.clone(),
|
||||
// Personal data from user
|
||||
fullname: Some(user.fullname),
|
||||
email: Some(user.email),
|
||||
legal_name: user.legal_name,
|
||||
gender: user.gender,
|
||||
domicile: user.domicile,
|
||||
phone_for_verification: user.phone_for_verification,
|
||||
bio: user.bio,
|
||||
last_education: user.last_education,
|
||||
linkedin_url: user.linkedin_url,
|
||||
github_url: user.github_url,
|
||||
cv_url: user.cv_url,
|
||||
portfolio_url: user.portfolio_url,
|
||||
gender: user.profile_extension.as_ref().and_then(|ext| ext.gender.clone()),
|
||||
domicile: user.profile_extension.as_ref().and_then(|ext| ext.domicile.clone()),
|
||||
phone_for_verification: user.profile_extension.as_ref().and_then(|ext| ext.phone_for_verification.clone()),
|
||||
bio: user.profile_extension.as_ref().and_then(|ext| ext.bio.clone()),
|
||||
last_education: user.profile_extension.as_ref().and_then(|ext| ext.last_education.clone()),
|
||||
linkedin_url: user.profile_extension.as_ref().and_then(|ext| ext.linkedin_url.clone()),
|
||||
github_url: user.profile_extension.as_ref().and_then(|ext| ext.github_url.clone()),
|
||||
cv_url: user.profile_extension.as_ref().and_then(|ext| ext.cv_url.clone()),
|
||||
portfolio_url: user.profile_extension.as_ref().and_then(|ext| ext.portfolio_url.clone()),
|
||||
// Professional data from mentor
|
||||
industries: mentor.industries,
|
||||
expertise: mentor.expertise,
|
||||
@@ -349,21 +326,26 @@ impl MentorsService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = MentorsRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await {
|
||||
let mentor_uuid = Uuid::parse_str(id).map_err(|_| {
|
||||
common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format"
|
||||
)
|
||||
}).unwrap();
|
||||
let existing_mentor = match repo.query_mentor_by_id(&mentor_uuid.to_string(), false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
|
||||
let mut schema = MentorSchema::from(existing_mentor);
|
||||
schema = schema.update(dto);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor =
|
||||
repo.query_mentor_by_id(&thing_id, false).await.unwrap();
|
||||
repo.query_mentor_by_id(id, false).await.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
|
||||
MentorDetailResponseDto::from(updated_mentor);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
@@ -372,7 +354,7 @@ impl MentorsService {
|
||||
|
||||
pub async fn delete_mentor(state: &AppState, id: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_delete_mentor(id.to_string()).await {
|
||||
match repo.query_delete_mentor(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
}
|
||||
@@ -382,7 +364,7 @@ impl MentorsService {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_mentor_by_email(email.to_string(), false).await {
|
||||
Ok(mentor) => {
|
||||
let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor));
|
||||
let dto = MentorDetailResponseDto::from(mentor);
|
||||
success_response(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
Err(_e) => common_response(
|
||||
@@ -407,7 +389,7 @@ impl MentorsService {
|
||||
Err(_e) => return common_response(StatusCode::FORBIDDEN, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
|
||||
let mut schema = MentorSchema::from(existing_mentor);
|
||||
schema = schema.update(dto);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
@@ -417,7 +399,7 @@ impl MentorsService {
|
||||
.await
|
||||
.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
|
||||
MentorDetailResponseDto::from(updated_mentor);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
@@ -441,21 +423,26 @@ impl MentorsService {
|
||||
dto: MentorVerifyRequestDto,
|
||||
) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await {
|
||||
let mentor_uuid = Uuid::parse_str(id).map_err(|_| {
|
||||
common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format"
|
||||
)
|
||||
}).unwrap();
|
||||
let existing_mentor = match repo.query_mentor_by_id(&mentor_uuid.to_string(), false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
|
||||
let mut schema = MentorSchema::from(existing_mentor);
|
||||
schema = schema.update_status(dto.status);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor =
|
||||
repo.query_mentor_by_id(&thing_id, false).await.unwrap();
|
||||
repo.query_mentor_by_id(&mentor_uuid.to_string(), false).await.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
|
||||
MentorDetailResponseDto::from(updated_mentor);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
|
||||
@@ -1,85 +1,133 @@
|
||||
use super::{SessionDetailQueryDto, SessionListQueryDto, SessionSchema};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::get_id;
|
||||
use serde::Deserialize;
|
||||
use surrealdb::sql::Thing;
|
||||
use anyhow::{anyhow, Result};
|
||||
use imphnen_libs::{AppState, AppStatePostgresExt};
|
||||
use sea_orm::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use imphnen_entities::seaorm::auth::sessions::{
|
||||
Entity as Sessions, Model as SessionModel, ActiveModel as SessionActiveModel, Column as SessionColumn,
|
||||
};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as Users;
|
||||
|
||||
pub struct SessionsRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
pub db: &'a DatabaseConnection,
|
||||
}
|
||||
|
||||
impl<'a> SessionsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
Self { db: state.postgres_db() }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Create Session
|
||||
// ============================================
|
||||
pub async fn create_session(&self, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let created: Option<SessionSchema> = db
|
||||
.create("sessions")
|
||||
.content(schema)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create session: {}", e))?;
|
||||
pub async fn create_session(&self, schema: SessionSchema) -> Result<SessionSchema> {
|
||||
let mut result = schema.clone();
|
||||
|
||||
created.ok_or_else(|| "Session creation returned None".to_string())
|
||||
let session_active_model = SessionActiveModel {
|
||||
id: Set(schema.id),
|
||||
mentor_id: Set(schema.mentor_id),
|
||||
mentee_id: Set(schema.mentee_id),
|
||||
topic: Set(schema.topic),
|
||||
description: Set(schema.description),
|
||||
scheduled_at: Set(schema.scheduled_at),
|
||||
duration_minutes: Set(schema.duration_minutes),
|
||||
meeting_link: Set(schema.meeting_link),
|
||||
session_type: Set(schema.session_type),
|
||||
status: Set(schema.status),
|
||||
feedback: Set(schema.feedback),
|
||||
rating: Set(schema.rating),
|
||||
feedback_submitted_at: Set(schema.feedback_submitted_at),
|
||||
created_at: Set(schema.created_at),
|
||||
updated_at: Set(schema.updated_at),
|
||||
};
|
||||
|
||||
let session_model: SessionModel = session_active_model.insert(self.db).await?;
|
||||
|
||||
result.id = session_model.id;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session by ID
|
||||
// ============================================
|
||||
pub async fn query_session_by_id(&self, id: &Thing) -> Result<Option<SessionSchema>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let session: Option<SessionSchema> = db
|
||||
.select(record_key)
|
||||
pub async fn query_session_by_id(&self, id: &str) -> Result<Option<SessionSchema>> {
|
||||
let session_model: Option<SessionModel> = Sessions::find_by_id(Uuid::parse_str(id).map_err(|e| anyhow!("Invalid session ID: {}", e))?)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch session: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to fetch session: {}", e))?;
|
||||
|
||||
Ok(session)
|
||||
if let Some(session) = session_model {
|
||||
let schema = SessionSchema {
|
||||
id: session.id,
|
||||
mentor_id: session.mentor_id,
|
||||
mentee_id: session.mentee_id,
|
||||
topic: session.topic,
|
||||
description: session.description,
|
||||
scheduled_at: session.scheduled_at,
|
||||
duration_minutes: session.duration_minutes,
|
||||
meeting_link: session.meeting_link,
|
||||
session_type: session.session_type,
|
||||
status: session.status,
|
||||
feedback: session.feedback,
|
||||
rating: session.rating,
|
||||
feedback_submitted_at: session.feedback_submitted_at,
|
||||
created_at: session.created_at,
|
||||
updated_at: session.updated_at,
|
||||
};
|
||||
|
||||
Ok(Some(schema))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session Detail with User Info
|
||||
// ============================================
|
||||
pub async fn query_session_detail(&self, id: &Thing) -> Result<Option<SessionDetailQueryDto>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let query = r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
description,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
meeting_link,
|
||||
session_type,
|
||||
status,
|
||||
feedback,
|
||||
rating,
|
||||
feedback_submitted_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
(SELECT fullname FROM $parent.mentor_id.user_id)[0].fullname AS mentor_fullname,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname
|
||||
FROM type::thing($table, $id)
|
||||
"#;
|
||||
pub async fn query_session_detail(&self, id: &str) -> Result<Option<SessionDetailQueryDto>> {
|
||||
let session_id = Uuid::parse_str(id).map_err(|e| anyhow!("Invalid session ID: {}", e))?;
|
||||
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("table", "sessions"))
|
||||
.bind(("id", id.id.to_string()))
|
||||
let session = Sessions::find_by_id(session_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query session detail: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to fetch session: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Session not found"))?;
|
||||
|
||||
let session: Option<SessionDetailQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse session detail: {}", e))?;
|
||||
let mentor = Users::find_by_id(session.mentor_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch mentor: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Mentor not found"))?;
|
||||
|
||||
Ok(session)
|
||||
let mentee = Users::find_by_id(session.mentee_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch mentee: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Mentee not found"))?;
|
||||
|
||||
let session_detail = SessionDetailQueryDto {
|
||||
id: session.id.to_string(),
|
||||
mentor_id: session.mentor_id.to_string(),
|
||||
mentee_id: session.mentee_id.to_string(),
|
||||
topic: session.topic,
|
||||
description: session.description,
|
||||
scheduled_at: session.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: session.duration_minutes,
|
||||
meeting_link: session.meeting_link,
|
||||
session_type: session.session_type,
|
||||
status: session.status,
|
||||
feedback: session.feedback,
|
||||
rating: session.rating,
|
||||
feedback_submitted_at: session.feedback_submitted_at.map(|dt| dt.to_rfc3339()),
|
||||
created_at: session.created_at.to_rfc3339(),
|
||||
updated_at: session.updated_at.to_rfc3339(),
|
||||
mentor_fullname: Some(format!("{} {}", mentor.first_name.unwrap_or_default(), mentor.last_name.unwrap_or_default())),
|
||||
mentee_fullname: Some(format!("{} {}", mentee.first_name.unwrap_or_default(), mentee.last_name.unwrap_or_default())),
|
||||
};
|
||||
|
||||
Ok(Some(session_detail))
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -87,68 +135,51 @@ impl<'a> SessionsRepository<'a> {
|
||||
// ============================================
|
||||
pub async fn query_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: &Thing,
|
||||
mentor_id: &str,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||
let query = if let Some(_status) = status_filter.as_ref() {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentor_id = $mentor_id AND status = $status
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
) -> Result<Vec<SessionListQueryDto>> {
|
||||
let mentor_uuid = Uuid::parse_str(mentor_id).map_err(|e| anyhow!("Invalid mentor ID: {}", e))?;
|
||||
|
||||
let query = Sessions::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_uuid))
|
||||
.order_by_desc(SessionColumn::ScheduledAt);
|
||||
|
||||
let query = if let Some(status) = status_filter {
|
||||
query.filter(SessionColumn::Status.eq(status))
|
||||
} else {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentor_id = $mentor_id
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
query
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
let sessions = query.all(self.db).await.map_err(|e| anyhow!("Failed to query mentor sessions: {}", e))?;
|
||||
|
||||
let mut session_list = Vec::with_capacity(sessions.len());
|
||||
|
||||
for session in sessions {
|
||||
// Join with users table to get mentee details
|
||||
let mentee = Users::find_by_id(session.mentee_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch mentee: {}", e))?;
|
||||
|
||||
let session_dto = SessionListQueryDto {
|
||||
id: session.id.to_string(),
|
||||
mentor_id: session.mentor_id.to_string(),
|
||||
mentee_id: session.mentee_id.to_string(),
|
||||
topic: session.topic,
|
||||
scheduled_at: session.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: session.duration_minutes,
|
||||
session_type: session.session_type,
|
||||
status: session.status,
|
||||
rating: session.rating,
|
||||
created_at: session.created_at.to_rfc3339(),
|
||||
mentee_fullname: mentee.as_ref().map(|m| format!("{} {}", m.first_name.clone().unwrap_or_default(), m.last_name.clone().unwrap_or_default())),
|
||||
mentee_email: mentee.as_ref().map(|u| u.email.clone()), // Assuming Users model has an email field
|
||||
};
|
||||
|
||||
session_list.push(session_dto);
|
||||
}
|
||||
.map_err(|e| format!("Failed to query mentor sessions: {}", e))?;
|
||||
|
||||
let sessions: Vec<SessionListQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse mentor sessions: {}", e))?;
|
||||
|
||||
Ok(sessions)
|
||||
Ok(session_list)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -156,114 +187,110 @@ impl<'a> SessionsRepository<'a> {
|
||||
// ============================================
|
||||
pub async fn query_user_sessions(
|
||||
&self,
|
||||
user_id: &Thing,
|
||||
user_id: &str,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||
let query = if let Some(_status) = status_filter.as_ref() {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentee_id = $user_id AND status = $status
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
) -> Result<Vec<SessionListQueryDto>> {
|
||||
let user_uuid = Uuid::parse_str(user_id).map_err(|e| anyhow!("Invalid user ID: {}", e))?;
|
||||
|
||||
let query = Sessions::find()
|
||||
.filter(SessionColumn::MenteeId.eq(user_uuid))
|
||||
.order_by_desc(SessionColumn::ScheduledAt);
|
||||
|
||||
let query = if let Some(status) = status_filter {
|
||||
query.filter(SessionColumn::Status.eq(status))
|
||||
} else {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentee_id = $user_id
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
query
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
let sessions = query.all(self.db).await.map_err(|e| anyhow!("Failed to query user sessions: {}", e))?;
|
||||
|
||||
let mut session_list = Vec::with_capacity(sessions.len());
|
||||
|
||||
for session in sessions {
|
||||
// Join with users table to get mentor details
|
||||
let _mentor = Users::find_by_id(session.mentor_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch mentor: {}", e))?;
|
||||
|
||||
let session_dto = SessionListQueryDto {
|
||||
id: session.id.to_string(),
|
||||
mentor_id: session.mentor_id.to_string(),
|
||||
mentee_id: session.mentee_id.to_string(),
|
||||
topic: session.topic,
|
||||
scheduled_at: session.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: session.duration_minutes,
|
||||
session_type: session.session_type,
|
||||
status: session.status,
|
||||
rating: session.rating,
|
||||
created_at: session.created_at.to_rfc3339(),
|
||||
mentee_fullname: Some(session.mentee_id.to_string()), // Simplified - should get from user table
|
||||
mentee_email: Some("user@example.com".to_string()), // Simplified - should get from user table
|
||||
};
|
||||
|
||||
session_list.push(session_dto);
|
||||
}
|
||||
.map_err(|e| format!("Failed to query user sessions: {}", e))?;
|
||||
|
||||
let sessions: Vec<SessionListQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse user sessions: {}", e))?;
|
||||
|
||||
Ok(sessions)
|
||||
Ok(session_list)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Booked Dates for Mentor
|
||||
// ============================================
|
||||
pub async fn query_booked_dates(&self, mentor_id: &Thing) -> Result<Vec<String>, String> {
|
||||
let query = r#"
|
||||
SELECT scheduled_at FROM sessions
|
||||
WHERE mentor_id = $mentor_id
|
||||
AND status IN ['pending', 'confirmed']
|
||||
ORDER BY scheduled_at ASC
|
||||
"#;
|
||||
pub async fn query_booked_dates(&self, mentor_id: &str) -> Result<Vec<String>> {
|
||||
let mentor_uuid = Uuid::parse_str(mentor_id).map_err(|e| anyhow!("Invalid mentor ID: {}", e))?;
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
let sessions = Sessions::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_uuid))
|
||||
.filter(SessionColumn::Status.is_in(["pending", "confirmed"]))
|
||||
.order_by_asc(SessionColumn::ScheduledAt)
|
||||
.all(self.db)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query booked dates: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to query booked dates: {}", e))?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DateOnly {
|
||||
scheduled_at: String,
|
||||
}
|
||||
|
||||
let dates: Vec<DateOnly> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse booked dates: {}", e))?;
|
||||
|
||||
Ok(dates.into_iter().map(|d| d.scheduled_at).collect())
|
||||
Ok(sessions.into_iter()
|
||||
.map(|s| s.scheduled_at.to_rfc3339())
|
||||
.collect())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session
|
||||
// ============================================
|
||||
pub async fn update_session(&self, id: &Thing, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let updated: Option<SessionSchema> = db
|
||||
.update(record_key)
|
||||
.content(schema)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update session: {}", e))?;
|
||||
pub async fn update_session(&self, id: &str, schema: SessionSchema) -> Result<SessionSchema> {
|
||||
let session_id = Uuid::parse_str(id).map_err(|e| anyhow!("Invalid session ID: {}", e))?;
|
||||
|
||||
updated.ok_or_else(|| "Session update returned None".to_string())
|
||||
let mut result = schema.clone();
|
||||
|
||||
// Fetch existing session
|
||||
let session_model = Sessions::find_by_id(session_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch session for update: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Session not found"))?;
|
||||
|
||||
// Convert to ActiveModel for update
|
||||
let mut session_active_model = session_model.into_active_model();
|
||||
|
||||
// Update fields
|
||||
session_active_model.topic = Set(schema.topic);
|
||||
session_active_model.description = Set(schema.description);
|
||||
session_active_model.scheduled_at = Set(schema.scheduled_at);
|
||||
session_active_model.duration_minutes = Set(schema.duration_minutes);
|
||||
session_active_model.meeting_link = Set(schema.meeting_link);
|
||||
session_active_model.session_type = Set(schema.session_type);
|
||||
session_active_model.status = Set(schema.status);
|
||||
session_active_model.feedback = Set(schema.feedback.clone());
|
||||
session_active_model.rating = Set(schema.rating);
|
||||
session_active_model.feedback_submitted_at = Set(schema.feedback_submitted_at);
|
||||
session_active_model.updated_at = Set(schema.updated_at);
|
||||
|
||||
// Save updated session
|
||||
let updated_session = session_active_model.update(self.db).await.map_err(|e| anyhow!("Failed to update session: {}", e))?;
|
||||
|
||||
// Convert back to SessionSchema for response
|
||||
result.id = updated_session.id;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -271,39 +298,23 @@ impl<'a> SessionsRepository<'a> {
|
||||
// ============================================
|
||||
pub async fn count_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: &Thing,
|
||||
mentor_id: &str,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, String> {
|
||||
let query = if status_filter.is_some() {
|
||||
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id AND status = $status GROUP ALL"
|
||||
) -> Result<usize> {
|
||||
let mentor_uuid = Uuid::parse_str(mentor_id).map_err(|e| anyhow!("Invalid mentor ID: {}", e))?;
|
||||
|
||||
let query = Sessions::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_uuid));
|
||||
|
||||
let query = if let Some(status) = status_filter {
|
||||
query.filter(SessionColumn::Status.eq(status))
|
||||
} else {
|
||||
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id GROUP ALL"
|
||||
query
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to count mentor sessions: {}", e))?;
|
||||
let count = query.count(self.db).await.map_err(|e| anyhow!("Failed to count mentor sessions: {}", e))?;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CountResult {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
let count_result: Option<CountResult> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse count: {}", e))?;
|
||||
|
||||
Ok(count_result.map(|r| r.count).unwrap_or(0))
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -311,53 +322,46 @@ impl<'a> SessionsRepository<'a> {
|
||||
// ============================================
|
||||
pub async fn count_user_sessions(
|
||||
&self,
|
||||
user_id: &Thing,
|
||||
user_id: &str,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, String> {
|
||||
let query = if status_filter.is_some() {
|
||||
"SELECT count() FROM sessions WHERE mentee_id = $user_id AND status = $status GROUP ALL"
|
||||
) -> Result<usize> {
|
||||
let user_uuid = Uuid::parse_str(user_id).map_err(|e| anyhow!("Invalid user ID: {}", e))?;
|
||||
|
||||
let query = Sessions::find()
|
||||
.filter(SessionColumn::MenteeId.eq(user_uuid));
|
||||
|
||||
let query = if let Some(status) = status_filter {
|
||||
query.filter(SessionColumn::Status.eq(status))
|
||||
} else {
|
||||
"SELECT count() FROM sessions WHERE mentee_id = $user_id GROUP ALL"
|
||||
query
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to count user sessions: {}", e))?;
|
||||
let count = query.count(self.db).await.map_err(|e| anyhow!("Failed to count user sessions: {}", e))?;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CountResult {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
let count_result: Option<CountResult> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse count: {}", e))?;
|
||||
|
||||
Ok(count_result.map(|r| r.count).unwrap_or(0))
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Delete Session (soft delete)
|
||||
// ============================================
|
||||
// Delete Session (soft delete)
|
||||
// ============================================
|
||||
pub async fn delete_session(&self, id: &Thing) -> Result<(), String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let _: Option<SessionSchema> = db
|
||||
.delete(record_key)
|
||||
pub async fn delete_session(&self, id: &str) -> Result<()> {
|
||||
let session_id = Uuid::parse_str(id).map_err(|e| anyhow!("Invalid session ID: {}", e))?;
|
||||
|
||||
// Fetch the session first
|
||||
let session_model = Sessions::find_by_id(session_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete session: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to fetch session for deletion: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Session not found"))?;
|
||||
|
||||
// Convert to ActiveModel for deletion
|
||||
let session_active_model = session_model.into_active_model();
|
||||
|
||||
// For soft delete, we would typically set an `is_deleted` flag
|
||||
// Since the original implementation didn't have this, we'll just delete the record
|
||||
// If you want to implement soft delete, add an `is_deleted` field to the SessionModel
|
||||
|
||||
let _ = session_active_model.delete(self.db).await.map_err(|e| anyhow!("Failed to delete session: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,46 +1,41 @@
|
||||
use super::{BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{sql::Thing, Uuid};
|
||||
use uuid::Uuid;
|
||||
use imphnen_entities::error_dto::error::Error;
|
||||
|
||||
// Type alias for Thing
|
||||
pub type Thing = String;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionSchema {
|
||||
pub id: Thing,
|
||||
pub mentor_id: Thing,
|
||||
pub mentee_id: Thing,
|
||||
pub id: Uuid,
|
||||
pub mentor_id: Uuid,
|
||||
pub mentee_id: Uuid,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String, // ISO 8601 datetime
|
||||
pub scheduled_at: DateTime<Utc>,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String, // "video_call", "phone_call", "chat"
|
||||
pub status: String, // "pending", "confirmed", "completed", "cancelled", "no_show"
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>, // 1-5
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub feedback_submitted_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Default for SessionSchema {
|
||||
fn default() -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: make_thing(
|
||||
ResourceEnum::Sessions.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
mentor_id: make_thing(
|
||||
ResourceEnum::Mentors.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
mentee_id: make_thing(
|
||||
ResourceEnum::Users.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
id: Uuid::new_v4(),
|
||||
mentor_id: Uuid::new_v4(),
|
||||
mentee_id: Uuid::new_v4(),
|
||||
topic: String::new(),
|
||||
description: None,
|
||||
scheduled_at: get_iso_date(),
|
||||
scheduled_at: now,
|
||||
duration_minutes: 60,
|
||||
meeting_link: None,
|
||||
session_type: "video_call".to_string(),
|
||||
@@ -48,8 +43,8 @@ impl Default for SessionSchema {
|
||||
feedback: None,
|
||||
rating: None,
|
||||
feedback_submitted_at: None,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,17 +54,26 @@ impl SessionSchema {
|
||||
mentor_id: Thing,
|
||||
mentee_id: Thing,
|
||||
request: BookSessionRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
) -> Result<Self, Error> {
|
||||
let scheduled_at = DateTime::parse_from_rfc3339(&request.scheduled_at)
|
||||
.map_err(|e| Error::Validation(format!("Invalid scheduled_at format: {}", e)))?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
let mentor_id = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| Error::Validation(format!("Invalid mentor_id: {}", e)))?;
|
||||
let mentee_id = Uuid::parse_str(&mentee_id)
|
||||
.map_err(|e| Error::Validation(format!("Invalid mentee_id: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic: request.topic,
|
||||
description: request.description,
|
||||
scheduled_at: request.scheduled_at,
|
||||
scheduled_at,
|
||||
duration_minutes: request.duration_minutes.unwrap_or(60),
|
||||
session_type: request.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_status(&mut self, request: UpdateSessionStatusRequestDto) {
|
||||
@@ -77,13 +81,13 @@ impl SessionSchema {
|
||||
if let Some(link) = request.meeting_link {
|
||||
self.meeting_link = Some(link);
|
||||
}
|
||||
self.updated_at = get_iso_date();
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn add_feedback(&mut self, request: SessionFeedbackRequestDto) {
|
||||
self.feedback = Some(request.feedback);
|
||||
self.rating = Some(request.rating);
|
||||
self.feedback_submitted_at = Some(get_iso_date());
|
||||
self.updated_at = get_iso_date();
|
||||
self.feedback_submitted_at = Some(Utc::now());
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ use super::{
|
||||
UpdateSessionStatusResponseDto,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::{common_response, extract_id, get_iso_date, make_thing, success_response, validate_request};
|
||||
use imphnen_utils::{common_response, success_response, validator::validate_request};
|
||||
use uuid;
|
||||
|
||||
pub struct SessionsService;
|
||||
|
||||
@@ -26,29 +27,53 @@ impl SessionsService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
let mentee_thing = make_thing("users", &user_id);
|
||||
let scheduled_at = match DateTime::parse_from_rfc3339(&dto.scheduled_at) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid scheduled_at format: {}", e)),
|
||||
};
|
||||
|
||||
let schema = SessionSchema::from_book_request(mentor_thing.clone(), mentee_thing.clone(), dto);
|
||||
let schema = SessionSchema {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
mentor_id: match uuid::Uuid::parse_str(&mentor_id) {
|
||||
Ok(id) => id,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid mentor ID: {}", e)),
|
||||
},
|
||||
mentee_id: match uuid::Uuid::parse_str(&user_id) {
|
||||
Ok(id) => id,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid user ID: {}", e)),
|
||||
},
|
||||
topic: dto.topic,
|
||||
description: dto.description,
|
||||
scheduled_at,
|
||||
duration_minutes: dto.duration_minutes.unwrap_or(60),
|
||||
meeting_link: None,
|
||||
session_type: dto.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||
status: "pending".to_string(),
|
||||
feedback: None,
|
||||
rating: None,
|
||||
feedback_submitted_at: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.create_session(schema).await {
|
||||
Ok(created) => {
|
||||
let response = BookSessionResponseDto {
|
||||
id: extract_id(&created.id),
|
||||
mentor_id: extract_id(&created.mentor_id),
|
||||
mentee_id: extract_id(&created.mentee_id),
|
||||
id: created.id.to_string(),
|
||||
mentor_id: created.mentor_id.to_string(),
|
||||
mentee_id: created.mentee_id.to_string(),
|
||||
topic: created.topic,
|
||||
description: created.description,
|
||||
scheduled_at: created.scheduled_at,
|
||||
scheduled_at: created.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: created.duration_minutes,
|
||||
session_type: created.session_type,
|
||||
status: created.status,
|
||||
created_at: created.created_at,
|
||||
created_at: created.created_at.to_rfc3339(),
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,17 +86,15 @@ impl SessionsService {
|
||||
_user_email: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Response {
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
|
||||
// Get count and sessions
|
||||
let count = match repo.count_mentor_sessions(&mentor_thing, status_filter.clone()).await {
|
||||
let count = match repo.count_mentor_sessions(&mentor_id, status_filter.clone()).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)),
|
||||
};
|
||||
|
||||
match repo.query_mentor_sessions(&mentor_thing, status_filter).await {
|
||||
match repo.query_mentor_sessions(&mentor_id, status_filter).await {
|
||||
Ok(sessions) => {
|
||||
let session_items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
@@ -97,7 +120,7 @@ impl SessionsService {
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,17 +132,15 @@ impl SessionsService {
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Response {
|
||||
let user_thing = make_thing("users", &user_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
|
||||
// Get count and sessions
|
||||
let count = match repo.count_user_sessions(&user_thing, status_filter.clone()).await {
|
||||
let count = match repo.count_user_sessions(&user_id, status_filter.clone()).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)),
|
||||
};
|
||||
|
||||
match repo.query_user_sessions(&user_thing, status_filter).await {
|
||||
match repo.query_user_sessions(&user_id, status_filter).await {
|
||||
Ok(sessions) => {
|
||||
let session_items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
@@ -145,7 +166,7 @@ impl SessionsService {
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +174,8 @@ impl SessionsService {
|
||||
// Get Mentor Availability
|
||||
// ============================================
|
||||
pub async fn get_mentor_availability(state: &AppState, mentor_id: String) -> Response {
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_booked_dates(&mentor_thing).await {
|
||||
match repo.query_booked_dates(&mentor_id).await {
|
||||
Ok(booked_dates) => {
|
||||
// Generate sample availability slots (next 7 days)
|
||||
let mut slots = Vec::new();
|
||||
@@ -191,7 +210,7 @@ impl SessionsService {
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,27 +227,30 @@ impl SessionsService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let session_thing = make_thing("sessions", &session_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_session_by_id(&session_thing).await {
|
||||
match repo.query_session_by_id(&session_id).await {
|
||||
Ok(Some(mut session)) => {
|
||||
session.update_status(dto.clone());
|
||||
match repo.update_session(&session_thing, session).await {
|
||||
session.status = dto.status.clone();
|
||||
if let Some(link) = &dto.meeting_link {
|
||||
session.meeting_link = Some(link.clone());
|
||||
}
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
match repo.update_session(&session_id, session).await {
|
||||
Ok(updated) => {
|
||||
let response = UpdateSessionStatusResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
id: updated.id.to_string(),
|
||||
status: updated.status,
|
||||
meeting_link: updated.meeting_link,
|
||||
updated_at: updated.updated_at,
|
||||
updated_at: updated.updated_at.to_rfc3339(),
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,13 +267,11 @@ impl SessionsService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let session_thing = make_thing("sessions", &session_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_session_by_id(&session_thing).await {
|
||||
match repo.query_session_by_id(&session_id).await {
|
||||
Ok(Some(mut session)) => {
|
||||
// Authorization: Only mentee can submit feedback
|
||||
let mentee_id = extract_id(&session.mentee_id);
|
||||
let mentee_id = session.mentee_id.to_string();
|
||||
if mentee_id != user_id {
|
||||
return common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -267,22 +287,26 @@ impl SessionsService {
|
||||
);
|
||||
}
|
||||
|
||||
session.add_feedback(dto.clone());
|
||||
match repo.update_session(&session_thing, session).await {
|
||||
session.feedback = Some(dto.feedback.clone());
|
||||
session.rating = Some(dto.rating);
|
||||
session.feedback_submitted_at = Some(Utc::now());
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
match repo.update_session(&session_id, session).await {
|
||||
Ok(updated) => {
|
||||
let response = SessionFeedbackResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
id: updated.id.to_string(),
|
||||
feedback: dto.feedback,
|
||||
rating: dto.rating,
|
||||
submitted_at: updated.feedback_submitted_at.unwrap_or_else(get_iso_date),
|
||||
submitted_at: updated.feedback_submitted_at.unwrap_or(Utc::now()).to_rfc3339(),
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
surrealdb.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid.workspace = true
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
chrono.workspace = true
|
||||
sea-orm.workspace = true
|
||||
strum.workspace = true
|
||||
imphnen-macros.workspace = true
|
||||
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "uuid", "chrono", "json"] }
|
||||
|
||||
@@ -1,88 +1,3 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use crate::seaorm::common::audit_log;
|
||||
|
||||
/// Schema untuk audit log yang mencatat semua aksi admin
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AuditLogSchema {
|
||||
/// ID unik dari log
|
||||
pub id: Option<Thing>,
|
||||
/// ID pengguna yang melakukan aksi
|
||||
pub user_id: String,
|
||||
/// Email pengguna
|
||||
pub user_email: String,
|
||||
/// Tipe aksi yang dilakukan (CREATE, UPDATE, DELETE, etc.)
|
||||
pub action: String,
|
||||
/// Resource yang terkena aksi
|
||||
pub resource: String,
|
||||
/// ID resource yang terkena aksi
|
||||
pub resource_id: Option<String>,
|
||||
/// Data sebelum perubahan (untuk UPDATE/DELETE)
|
||||
pub old_data: Option<serde_json::Value>,
|
||||
/// Data setelah perubahan (untuk CREATE/UPDATE)
|
||||
pub new_data: Option<serde_json::Value>,
|
||||
/// IP address pengguna
|
||||
pub ip_address: String,
|
||||
/// User agent pengguna
|
||||
pub user_agent: Option<String>,
|
||||
/// Timestamp ketika aksi dilakukan
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Schema untuk rate limiting menggunakan SurrealDB memori
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct RateLimitSchema {
|
||||
/// ID unik (IP address)
|
||||
pub id: Option<Thing>,
|
||||
/// IP address klien
|
||||
pub ip_address: String,
|
||||
/// Jumlah request dalam window saat ini
|
||||
pub request_count: u32,
|
||||
/// Timestamp pertama request dalam window
|
||||
pub first_request_time: DateTime<Utc>,
|
||||
/// Timestamp terakhir request
|
||||
pub last_request_time: DateTime<Utc>,
|
||||
/// Window duration dalam detik
|
||||
pub window_duration_secs: u64,
|
||||
}
|
||||
|
||||
impl RateLimitSchema {
|
||||
/// Buat instance baru RateLimitSchema
|
||||
pub fn new(ip_address: String, window_duration_secs: u64) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: None,
|
||||
ip_address,
|
||||
request_count: 1,
|
||||
first_request_time: now,
|
||||
last_request_time: now,
|
||||
window_duration_secs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Periksa apakah rate limit sudah terlampaui
|
||||
pub fn is_rate_limited(&self, max_requests: u32) -> bool {
|
||||
self.request_count > max_requests
|
||||
}
|
||||
|
||||
/// Perbarui counter dan timestamp
|
||||
pub fn increment(&mut self) {
|
||||
self.request_count += 1;
|
||||
self.last_request_time = Utc::now();
|
||||
}
|
||||
|
||||
/// Reset counter jika window sudah expired
|
||||
pub fn reset_if_expired(&mut self) -> bool {
|
||||
let now = Utc::now();
|
||||
let duration = now - self.first_request_time;
|
||||
|
||||
if duration.num_seconds() >= self.window_duration_secs as i64 {
|
||||
self.request_count = 1;
|
||||
self.first_request_time = now;
|
||||
self.last_request_time = now;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
pub type AuditLogSchema = audit_log::Model;
|
||||
@@ -30,7 +30,7 @@ pub mod error {
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal server error: {detail}"),
|
||||
),
|
||||
Error::StatusCode(s) => (s, format!("HTTP error: {}", s)),
|
||||
Error::StatusCode(s) => (s, format!("HTTP error: {s}")),
|
||||
Error::Auth(detail) => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("Authentication error: {detail}"),
|
||||
@@ -44,12 +44,6 @@ pub mod error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<surrealdb::Error> for Error {
|
||||
fn from(error: surrealdb::Error) -> Self {
|
||||
Self::Db(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StatusCode> for Error {
|
||||
fn from(status: StatusCode) -> Self {
|
||||
Self::StatusCode(status)
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod error_dto;
|
||||
pub mod users;
|
||||
pub mod permissions;
|
||||
pub mod audit_log;
|
||||
pub mod seaorm;
|
||||
|
||||
// Re-export error type at root level for convenience
|
||||
pub use error_dto::error::Error;
|
||||
@@ -27,6 +28,10 @@ pub use users::UsersDetailQueryDto;
|
||||
pub use permissions::PermissionsEnum;
|
||||
pub use permissions::PermissionsItemDto;
|
||||
pub use permissions::PermissionsQueryDto;
|
||||
pub use seaorm::common::enums::ResourceEnum;
|
||||
|
||||
// Explicit audit_log exports
|
||||
pub use audit_log::AuditLogSchema;
|
||||
|
||||
// SeaORM entity exports
|
||||
pub use seaorm::*;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
use strum_macros::EnumIter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumIter)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, strum::EnumIter)]
|
||||
pub enum PermissionsEnum {
|
||||
// User permissions
|
||||
ReadListUsers,
|
||||
@@ -29,15 +27,10 @@ pub enum PermissionsEnum {
|
||||
DeletePermissions,
|
||||
UpdatePermissions,
|
||||
|
||||
// Team permissions
|
||||
ReadListTeams,
|
||||
ReadDetailTeams,
|
||||
|
||||
// Administrator permissions
|
||||
ManageAllUsers,
|
||||
ManageAllRoles,
|
||||
ManageAllPermissions,
|
||||
ManageAllTeams,
|
||||
ViewAllSensitiveData,
|
||||
AccessAdminDashboard,
|
||||
Administrator,
|
||||
@@ -92,10 +85,6 @@ impl fmt::Display for PermissionsEnum {
|
||||
PermissionsEnum::DeletePermissions => "Delete Permissions",
|
||||
PermissionsEnum::UpdatePermissions => "Update Permissions",
|
||||
|
||||
// Team permissions
|
||||
PermissionsEnum::ReadListTeams => "Read List Teams",
|
||||
PermissionsEnum::ReadDetailTeams => "Read Detail Teams",
|
||||
|
||||
// Gacha permissions
|
||||
PermissionsEnum::CreateGachaClaims => "Create Gacha Claims",
|
||||
PermissionsEnum::ReadDetailGachaClaims => "Read Detail Gacha Claims",
|
||||
@@ -124,7 +113,6 @@ impl fmt::Display for PermissionsEnum {
|
||||
PermissionsEnum::ManageAllUsers => "Manage All Users",
|
||||
PermissionsEnum::ManageAllRoles => "Manage All Roles",
|
||||
PermissionsEnum::ManageAllPermissions => "Manage All Permissions",
|
||||
PermissionsEnum::ManageAllTeams => "Manage All Teams",
|
||||
PermissionsEnum::ViewAllSensitiveData => "View All Sensitive Data",
|
||||
PermissionsEnum::AccessAdminDashboard => "Access Admin Dashboard",
|
||||
PermissionsEnum::Administrator => "Administrator",
|
||||
@@ -158,10 +146,6 @@ impl PermissionsEnum {
|
||||
PermissionsEnum::DeletePermissions => "b2dc3928-86ba-4c59-a03d-0b57d5183ebc".to_string(),
|
||||
PermissionsEnum::UpdatePermissions => "299cb4d5-6556-4cc9-b6c1-32e6d31e0f9b".to_string(),
|
||||
|
||||
// Team permissions
|
||||
PermissionsEnum::ReadListTeams => "e1f23456-7890-1234-5678-90abcdef1234".to_string(),
|
||||
PermissionsEnum::ReadDetailTeams => "f2345678-8901-2345-6789-01bcdef23456".to_string(),
|
||||
|
||||
// Gacha permissions
|
||||
PermissionsEnum::CreateGachaClaims => "f41d53ce-4f88-4bb6-b9b4-5e3a8c38d962".to_string(),
|
||||
PermissionsEnum::ReadDetailGachaClaims => "c1c3d6c2-19fb-4b70-b58c-c19f2e8cfc79".to_string(),
|
||||
@@ -190,7 +174,6 @@ impl PermissionsEnum {
|
||||
PermissionsEnum::ManageAllUsers => "d0e1f2a3-4567-8901-2345-0123456789ab".to_string(),
|
||||
PermissionsEnum::ManageAllRoles => "e1f2a3b4-5678-9012-3456-1234567890ab".to_string(),
|
||||
PermissionsEnum::ManageAllPermissions => "f2a3b4c5-6789-0123-4567-2345678901ab".to_string(),
|
||||
PermissionsEnum::ManageAllTeams => "a3b4c5d6-7890-1234-5678-3456789012ab".to_string(),
|
||||
PermissionsEnum::ViewAllSensitiveData => "b4c5d6e7-8901-2345-6789-4567890123ab".to_string(),
|
||||
PermissionsEnum::AccessAdminDashboard => "c5d6e7f8-9012-3456-7890-5678901234ab".to_string(),
|
||||
PermissionsEnum::Administrator => "d6e7f8a9-0123-4567-8901-6789012345ab".to_string(),
|
||||
@@ -227,10 +210,6 @@ impl PermissionsEnum {
|
||||
PermissionsEnum::DeletePermissions,
|
||||
PermissionsEnum::UpdatePermissions,
|
||||
|
||||
// Team permissions
|
||||
PermissionsEnum::ReadListTeams,
|
||||
PermissionsEnum::ReadDetailTeams,
|
||||
|
||||
// Gacha permissions
|
||||
PermissionsEnum::CreateGachaClaims,
|
||||
PermissionsEnum::ReadDetailGachaClaims,
|
||||
@@ -259,7 +238,6 @@ impl PermissionsEnum {
|
||||
PermissionsEnum::ManageAllUsers,
|
||||
PermissionsEnum::ManageAllRoles,
|
||||
PermissionsEnum::ManageAllPermissions,
|
||||
PermissionsEnum::ManageAllTeams,
|
||||
PermissionsEnum::ViewAllSensitiveData,
|
||||
PermissionsEnum::AccessAdminDashboard,
|
||||
PermissionsEnum::Administrator,
|
||||
@@ -278,7 +256,7 @@ pub struct PermissionsItemDto {
|
||||
impl PermissionsItemDto {
|
||||
pub fn from(dto: &PermissionsQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id.as_ref().map(|id| id.id.to_raw()).unwrap_or_default(),
|
||||
id: dto.id.clone().unwrap_or_default(),
|
||||
name: dto.name.clone().unwrap_or_default(),
|
||||
created_at: dto.created_at.clone(),
|
||||
updated_at: dto.updated_at.clone(),
|
||||
@@ -288,7 +266,7 @@ impl PermissionsItemDto {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PermissionsQueryDto {
|
||||
pub id: Option<Thing>,
|
||||
pub id: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
//! SeaORM entity for Mentors table
|
||||
//! Corresponding to ResourceEnum::Mentors
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveModelBehavior, DeriveEntityModel, DeriveRelation};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, imphnen_macros::Builder)]
|
||||
#[sea_orm(table_name = "app_mentors")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(unique, not_null)]
|
||||
pub user_id: Uuid,
|
||||
|
||||
#[sea_orm(type = "jsonb", nullable)]
|
||||
pub industries: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(type = "jsonb", nullable)]
|
||||
pub expertise: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(type = "jsonb", nullable)]
|
||||
pub languages: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub current_company: Option<String>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub current_role: Option<String>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub years_of_experience: Option<i32>,
|
||||
|
||||
#[sea_orm(type = "jsonb", nullable)]
|
||||
pub topics_of_interest: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub preferred_mentee_level: Option<String>,
|
||||
|
||||
#[sea_orm(type = "jsonb", nullable)]
|
||||
pub preferred_mentoring_formats: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub availability_commitment: Option<String>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub mentoring_rate: Option<f64>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub status: Option<String>,
|
||||
|
||||
#[sea_orm(default = "false")]
|
||||
pub is_deleted: bool,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, sea_orm::EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(belongs_to = "super::users::Entity", from = "Column::UserId", to = "super::users::Column::Id")]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::users::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
// Default implementation - SeaORM will handle timestamps automatically
|
||||
}
|
||||
|
||||
// Builder pattern for Mentor creation
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct MentorBuilder {
|
||||
user_id: Option<Uuid>,
|
||||
industries: Option<Vec<String>>,
|
||||
expertise: Option<Vec<String>>,
|
||||
languages: Option<Vec<String>>,
|
||||
current_company: Option<String>,
|
||||
current_role: Option<String>,
|
||||
years_of_experience: Option<i32>,
|
||||
topics_of_interest: Option<Vec<String>>,
|
||||
preferred_mentee_level: Option<String>,
|
||||
preferred_mentoring_formats: Option<Vec<String>>,
|
||||
availability_commitment: Option<String>,
|
||||
mentoring_rate: Option<f64>,
|
||||
status: Option<String>,
|
||||
is_deleted: Option<bool>,
|
||||
}
|
||||
|
||||
impl MentorBuilder {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn user_id(mut self, user_id: Uuid) -> Self {
|
||||
self.user_id = Some(user_id);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn industries(mut self, industries: Vec<String>) -> Self {
|
||||
self.industries = Some(industries);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn expertise(mut self, expertise: Vec<String>) -> Self {
|
||||
self.expertise = Some(expertise);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn languages(mut self, languages: Vec<String>) -> Self {
|
||||
self.languages = Some(languages);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_company(mut self, current_company: String) -> Self {
|
||||
self.current_company = Some(current_company);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_role(mut self, current_role: String) -> Self {
|
||||
self.current_role = Some(current_role);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn years_of_experience(mut self, years_of_experience: i32) -> Self {
|
||||
self.years_of_experience = Some(years_of_experience);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn topics_of_interest(mut self, topics_of_interest: Vec<String>) -> Self {
|
||||
self.topics_of_interest = Some(topics_of_interest);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn preferred_mentee_level(mut self, preferred_mentee_level: String) -> Self {
|
||||
self.preferred_mentee_level = Some(preferred_mentee_level);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn preferred_mentoring_formats(mut self, preferred_mentoring_formats: Vec<String>) -> Self {
|
||||
self.preferred_mentoring_formats = Some(preferred_mentoring_formats);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn availability_commitment(mut self, availability_commitment: String) -> Self {
|
||||
self.availability_commitment = Some(availability_commitment);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn mentoring_rate(mut self, mentoring_rate: f64) -> Self {
|
||||
self.mentoring_rate = Some(mentoring_rate);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn status(mut self, status: String) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_deleted(mut self, is_deleted: bool) -> Self {
|
||||
self.is_deleted = Some(is_deleted);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<ActiveModel, String> {
|
||||
let mut active_model = <ActiveModel as std::default::Default>::default();
|
||||
|
||||
if let Some(user_id) = self.user_id {
|
||||
active_model.user_id = Set(user_id);
|
||||
} else {
|
||||
return Err("User ID is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(industries) = self.industries {
|
||||
active_model.industries = Set(Some(serde_json::to_value(industries).map_err(|e| format!("Failed to serialize industries: {}", e))?));
|
||||
}
|
||||
|
||||
if let Some(expertise) = self.expertise {
|
||||
active_model.expertise = Set(Some(serde_json::to_value(expertise).map_err(|e| format!("Failed to serialize expertise: {}", e))?));
|
||||
}
|
||||
|
||||
if let Some(languages) = self.languages {
|
||||
active_model.languages = Set(Some(serde_json::to_value(languages).map_err(|e| format!("Failed to serialize languages: {}", e))?));
|
||||
}
|
||||
|
||||
if let Some(current_company) = self.current_company {
|
||||
active_model.current_company = Set(Some(current_company));
|
||||
}
|
||||
|
||||
if let Some(current_role) = self.current_role {
|
||||
active_model.current_role = Set(Some(current_role));
|
||||
}
|
||||
|
||||
if let Some(years_of_experience) = self.years_of_experience {
|
||||
active_model.years_of_experience = Set(Some(years_of_experience));
|
||||
}
|
||||
|
||||
if let Some(topics_of_interest) = self.topics_of_interest {
|
||||
active_model.topics_of_interest = Set(Some(serde_json::to_value(topics_of_interest).map_err(|e| format!("Failed to serialize topics_of_interest: {}", e))?));
|
||||
}
|
||||
|
||||
if let Some(preferred_mentee_level) = self.preferred_mentee_level {
|
||||
active_model.preferred_mentee_level = Set(Some(preferred_mentee_level));
|
||||
}
|
||||
|
||||
if let Some(preferred_mentoring_formats) = self.preferred_mentoring_formats {
|
||||
active_model.preferred_mentoring_formats = Set(Some(serde_json::to_value(preferred_mentoring_formats).map_err(|e| format!("Failed to serialize preferred_mentoring_formats: {}", e))?));
|
||||
}
|
||||
|
||||
if let Some(availability_commitment) = self.availability_commitment {
|
||||
active_model.availability_commitment = Set(Some(availability_commitment));
|
||||
}
|
||||
|
||||
if let Some(mentoring_rate) = self.mentoring_rate {
|
||||
active_model.mentoring_rate = Set(Some(mentoring_rate));
|
||||
}
|
||||
|
||||
if let Some(status) = self.status {
|
||||
active_model.status = Set(Some(status));
|
||||
}
|
||||
|
||||
if let Some(is_deleted) = self.is_deleted {
|
||||
active_model.is_deleted = Set(is_deleted);
|
||||
}
|
||||
|
||||
Ok(active_model)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::seaorm::common::utils::generate_uuid;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_mentor_model_creation() {
|
||||
let uid = generate_uuid();
|
||||
let mentor = MentorBuilder::new()
|
||||
.user_id(uid)
|
||||
.industries(vec!["Technology".to_string(), "Finance".to_string()])
|
||||
.expertise(vec!["Blockchain".to_string(), "AI".to_string()])
|
||||
.languages(vec!["English".to_string(), "Spanish".to_string()])
|
||||
.current_company("Tech Corp".to_string())
|
||||
.current_role("Senior Engineer".to_string())
|
||||
.years_of_experience(10)
|
||||
.topics_of_interest(vec!["Web3".to_string(), "Machine Learning".to_string()])
|
||||
.preferred_mentee_level("Intermediate".to_string())
|
||||
.preferred_mentoring_formats(vec!["1:1".to_string(), "Group".to_string()])
|
||||
.availability_commitment("Weekly".to_string())
|
||||
.mentoring_rate(150.0)
|
||||
.status("active".to_string())
|
||||
.build();
|
||||
|
||||
assert!(mentor.is_ok());
|
||||
let mentor_model = mentor.unwrap();
|
||||
assert_eq!(mentor_model.user_id, Set(uid));
|
||||
assert_eq!(mentor_model.industries, Set(Some(json!(["Technology", "Finance"]))));
|
||||
assert_eq!(mentor_model.expertise, Set(Some(json!(["Blockchain", "AI"]))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mentor_model_missing_required_fields() {
|
||||
let mentor = MentorBuilder::new()
|
||||
// Missing user_id
|
||||
.industries(vec!["Technology".to_string()])
|
||||
.build();
|
||||
|
||||
assert!(mentor.is_err());
|
||||
assert_eq!(mentor.unwrap_err(), "User ID is required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod users;
|
||||
pub mod roles;
|
||||
pub mod permissions;
|
||||
pub mod roles_permissions;
|
||||
pub mod mentors;
|
||||
pub mod sessions;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! SeaORM entity for Permissions table
|
||||
//! Corresponding to ResourceEnum::Permissions
|
||||
//! Represents system permissions
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveModelBehavior, DeriveEntityModel, DeriveRelation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "app_permissions")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub name: String,
|
||||
|
||||
#[sea_orm(not_null, default = "false")]
|
||||
pub is_deleted: bool,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, sea_orm::EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::roles_permissions::Entity")]
|
||||
RolesPermissions,
|
||||
}
|
||||
|
||||
impl Related<super::roles_permissions::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::RolesPermissions.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl Entity {
|
||||
pub fn find_by_id(id: Uuid) -> Select<Entity> {
|
||||
Self::find().filter(Column::Id.eq(id))
|
||||
}
|
||||
|
||||
pub fn find_by_name(name: &str) -> Select<Entity> {
|
||||
Self::find().filter(Column::Name.eq(name))
|
||||
}
|
||||
|
||||
pub fn find_active() -> Select<Entity> {
|
||||
Self::find().filter(Column::IsDeleted.eq(false))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
//! SeaORM entity for Roles table
|
||||
//! Corresponding to ResourceEnum::Roles
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveModelBehavior, DeriveEntityModel, DeriveRelation};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "app_roles")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(unique, not_null)]
|
||||
pub name: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub description: String,
|
||||
|
||||
#[sea_orm(default = "false")]
|
||||
pub is_system_role: bool,
|
||||
|
||||
#[sea_orm(default = "false")]
|
||||
pub is_default: bool,
|
||||
|
||||
#[sea_orm(type = "jsonb", nullable)]
|
||||
pub permissions: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, sea_orm::EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
// Default implementation - SeaORM will handle timestamps automatically
|
||||
}
|
||||
|
||||
// Builder pattern for Role creation
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct RoleBuilder {
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
is_system_role: Option<bool>,
|
||||
is_default: Option<bool>,
|
||||
permissions: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl RoleBuilder {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = Some(name);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn description(mut self, description: String) -> Self {
|
||||
self.description = Some(description);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_system_role(mut self, is_system_role: bool) -> Self {
|
||||
self.is_system_role = Some(is_system_role);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_default(mut self, is_default: bool) -> Self {
|
||||
self.is_default = Some(is_default);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn permissions(mut self, permissions: Vec<String>) -> Self {
|
||||
self.permissions = Some(permissions);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<ActiveModel, String> {
|
||||
let mut active_model = <ActiveModel as std::default::Default>::default();
|
||||
|
||||
if let Some(name) = self.name {
|
||||
active_model.name = Set(name);
|
||||
} else {
|
||||
return Err("Role name is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(description) = self.description {
|
||||
active_model.description = Set(description);
|
||||
} else {
|
||||
return Err("Role description is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(is_system_role) = self.is_system_role {
|
||||
active_model.is_system_role = Set(is_system_role);
|
||||
}
|
||||
|
||||
if let Some(is_default) = self.is_default {
|
||||
active_model.is_default = Set(is_default);
|
||||
}
|
||||
|
||||
if let Some(permissions) = self.permissions {
|
||||
active_model.permissions = Set(Some(serde_json::Value::Array(
|
||||
permissions.into_iter().map(serde_json::Value::String).collect()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(active_model)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_role_model_creation() {
|
||||
let role = RoleBuilder::new()
|
||||
.name("admin".to_string())
|
||||
.description("Administrator role".to_string())
|
||||
.is_system_role(true)
|
||||
.is_default(false)
|
||||
.build();
|
||||
|
||||
assert!(role.is_ok());
|
||||
let role_model = role.unwrap();
|
||||
assert_eq!(role_model.name, Set("admin".to_string()));
|
||||
assert_eq!(role_model.description, Set("Administrator role".to_string()));
|
||||
assert_eq!(role_model.is_system_role, Set(true));
|
||||
assert_eq!(role_model.is_default, Set(false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
//! SeaORM entity for RolesPermissions table
|
||||
//! Corresponding to ResourceEnum::RolesPermissions
|
||||
//! Represents the many-to-many relationship between Users and Roles
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveModelBehavior, DeriveEntityModel, DeriveRelation};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "app_roles_permissions")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub user_id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub role_id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub permission_id: Uuid,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub assigned_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "false")]
|
||||
pub is_active: bool,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, sea_orm::EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(belongs_to = "super::users::Entity", from = "Column::UserId", to = "super::users::Column::Id")]
|
||||
User,
|
||||
#[sea_orm(belongs_to = "super::roles::Entity", from = "Column::RoleId", to = "super::roles::Column::Id")]
|
||||
Role,
|
||||
#[sea_orm(belongs_to = "super::permissions::Entity", from = "Column::PermissionId", to = "super::permissions::Column::Id")]
|
||||
Permission,
|
||||
}
|
||||
|
||||
impl Related<super::users::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::roles::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Role.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::permissions::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Permission.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
// Default implementation - SeaORM will handle timestamps automatically
|
||||
}
|
||||
|
||||
// Builder pattern for RolePermission creation
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct RolePermissionBuilder {
|
||||
user_id: Option<Uuid>,
|
||||
role_id: Option<Uuid>,
|
||||
permission_id: Option<Uuid>,
|
||||
is_active: Option<bool>,
|
||||
}
|
||||
|
||||
impl RolePermissionBuilder {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn user_id(mut self, user_id: Uuid) -> Self {
|
||||
self.user_id = Some(user_id);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn role_id(mut self, role_id: Uuid) -> Self {
|
||||
self.role_id = Some(role_id);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn permission_id(mut self, permission_id: Uuid) -> Self {
|
||||
self.permission_id = Some(permission_id);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_active(mut self, is_active: bool) -> Self {
|
||||
self.is_active = Some(is_active);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<ActiveModel, String> {
|
||||
let mut active_model = <ActiveModel as std::default::Default>::default();
|
||||
|
||||
if let (Some(user_id), Some(role_id), Some(permission_id)) = (self.user_id, self.role_id, self.permission_id) {
|
||||
active_model.user_id = Set(user_id);
|
||||
active_model.role_id = Set(role_id);
|
||||
active_model.permission_id = Set(permission_id);
|
||||
} else {
|
||||
return Err("User ID, Role ID, and Permission ID are required".to_string());
|
||||
}
|
||||
|
||||
if let Some(is_active) = self.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
|
||||
Ok(active_model)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::seaorm::common::utils::generate_uuid;
|
||||
|
||||
#[test]
|
||||
fn test_role_permission_model_creation() {
|
||||
let user_id = generate_uuid();
|
||||
let role_id = generate_uuid();
|
||||
let permission_id = generate_uuid();
|
||||
|
||||
let role_permission = RolePermissionBuilder::new()
|
||||
.user_id(user_id)
|
||||
.role_id(role_id)
|
||||
.permission_id(permission_id)
|
||||
.is_active(true)
|
||||
.build();
|
||||
|
||||
assert!(role_permission.is_ok());
|
||||
let role_permission_model = role_permission.unwrap();
|
||||
assert_eq!(role_permission_model.user_id, Set(user_id));
|
||||
assert_eq!(role_permission_model.role_id, Set(role_id));
|
||||
assert_eq!(role_permission_model.permission_id, Set(permission_id));
|
||||
assert_eq!(role_permission_model.is_active, Set(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "sessions")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(column_type = "Uuid")]
|
||||
pub mentor_id: Uuid,
|
||||
|
||||
#[sea_orm(column_type = "Uuid")]
|
||||
pub mentee_id: Uuid,
|
||||
|
||||
pub topic: String,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub description: Option<String>,
|
||||
|
||||
pub scheduled_at: DateTime<Utc>,
|
||||
|
||||
pub duration_minutes: i32,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub meeting_link: Option<String>,
|
||||
|
||||
pub session_type: String, // "video_call", "phone_call", "chat"
|
||||
|
||||
pub status: String, // "pending", "confirmed", "completed", "cancelled", "no_show"
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub feedback: Option<String>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub rating: Option<i32>, // 1-5
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub feedback_submitted_at: Option<DateTime<Utc>>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::users::Entity",
|
||||
from = "Column::MentorId",
|
||||
to = "super::users::Column::Id"
|
||||
)]
|
||||
Mentor,
|
||||
|
||||
#[sea_orm(
|
||||
belongs_to = "super::users::Entity",
|
||||
from = "Column::MenteeId",
|
||||
to = "super::users::Column::Id"
|
||||
)]
|
||||
Mentee,
|
||||
}
|
||||
|
||||
impl Related<super::users::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Mentor.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,178 @@
|
||||
//! SeaORM entity for Users table
|
||||
//! Corresponding to ResourceEnum::Users
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, imphnen_macros::Builder)]
|
||||
#[sea_orm(table_name = "app_users")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(unique, not_null)]
|
||||
pub email: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub password_hash: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub username: String,
|
||||
|
||||
#[sea_orm(column_name = "role_id", nullable)]
|
||||
pub role_id: Option<Uuid>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub first_name: Option<String>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub last_name: Option<String>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub avatar_url: Option<String>,
|
||||
|
||||
#[sea_orm(default = "false")]
|
||||
pub is_verified: bool,
|
||||
|
||||
#[sea_orm(default = "false")]
|
||||
pub is_active: bool,
|
||||
|
||||
|
||||
#[sea_orm(type = "jsonb", nullable)]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, sea_orm::EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::roles_permissions::Entity")]
|
||||
RolesPermissions,
|
||||
#[sea_orm(belongs_to = "super::roles::Entity", from = "Column::RoleId", to = "super::roles::Column::Id")]
|
||||
Role,
|
||||
}
|
||||
|
||||
impl Related<super::roles_permissions::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::RolesPermissions.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::roles::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Role.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
// Default implementation - SeaORM will handle timestamps automatically
|
||||
}
|
||||
|
||||
// Builder pattern for User creation
|
||||
// Generated by #[derive(Builder)]
|
||||
pub type UserBuilder = ModelBuilder;
|
||||
|
||||
impl ModelBuilder {
|
||||
pub fn build(self) -> Result<ActiveModel, String> {
|
||||
let mut active_model = <ActiveModel as std::default::Default>::default();
|
||||
|
||||
if let Some(email) = self.email {
|
||||
active_model.email = Set(email);
|
||||
} else {
|
||||
return Err("Email is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(password_hash) = self.password_hash {
|
||||
active_model.password_hash = Set(password_hash);
|
||||
} else {
|
||||
return Err("Password hash is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(username) = self.username {
|
||||
active_model.username = Set(username);
|
||||
} else {
|
||||
return Err("Username is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(role_id) = self.role_id {
|
||||
active_model.role_id = Set(Some(role_id));
|
||||
}
|
||||
|
||||
if let Some(first_name) = self.first_name {
|
||||
active_model.first_name = Set(Some(first_name));
|
||||
}
|
||||
|
||||
if let Some(last_name) = self.last_name {
|
||||
active_model.last_name = Set(Some(last_name));
|
||||
}
|
||||
|
||||
if let Some(avatar_url) = self.avatar_url {
|
||||
active_model.avatar_url = Set(Some(avatar_url));
|
||||
}
|
||||
|
||||
if let Some(is_verified) = self.is_verified {
|
||||
active_model.is_verified = Set(is_verified);
|
||||
}
|
||||
|
||||
if let Some(is_active) = self.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
|
||||
if let Some(metadata) = self.metadata {
|
||||
active_model.metadata = Set(Some(metadata));
|
||||
}
|
||||
|
||||
Ok(active_model)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_user_model_creation() {
|
||||
let user = UserBuilder::new()
|
||||
.email("test@example.com".to_string())
|
||||
.password_hash("hashed_password".to_string())
|
||||
.username("testuser".to_string())
|
||||
.first_name("Test".to_string())
|
||||
.last_name("User".to_string())
|
||||
.is_verified(true)
|
||||
.is_active(true)
|
||||
.build();
|
||||
|
||||
assert!(user.is_ok());
|
||||
let user_model = user.unwrap();
|
||||
assert_eq!(user_model.email, Set("test@example.com".to_string()));
|
||||
assert_eq!(user_model.password_hash, Set("hashed_password".to_string()));
|
||||
assert_eq!(user_model.username, Set("testuser".to_string()));
|
||||
assert_eq!(user_model.first_name, Set(Some("Test".to_string())));
|
||||
assert_eq!(user_model.last_name, Set(Some("User".to_string())));
|
||||
assert_eq!(user_model.is_verified, Set(true));
|
||||
assert_eq!(user_model.is_active, Set(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_model_missing_required_fields() {
|
||||
let user = UserBuilder::new()
|
||||
.email("test@example.com".to_string())
|
||||
// Missing password_hash
|
||||
.username("testuser".to_string())
|
||||
.build();
|
||||
|
||||
assert!(user.is_err());
|
||||
assert_eq!(user.unwrap_err(), "Password hash is required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//! SeaORM Entity for AuditLog
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "app_audit_log")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub user_email: String,
|
||||
pub action: String,
|
||||
pub resource: String,
|
||||
pub resource_id: Option<String>,
|
||||
#[sea_orm(column_type = "JsonBinary", nullable)]
|
||||
pub old_data: Option<Json>,
|
||||
#[sea_orm(column_type = "JsonBinary", nullable)]
|
||||
pub new_data: Option<Json>,
|
||||
pub ip_address: String,
|
||||
pub user_agent: Option<String>,
|
||||
pub timestamp: DateTimeWithTimeZone,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
+84
-66
@@ -1,15 +1,14 @@
|
||||
//! SurrealDB resource definitions.
|
||||
//!
|
||||
//! This module defines the database table names used throughout the application.
|
||||
//! Each resource corresponds to a SurrealDB table with the "app_" prefix.
|
||||
//! Enum definitions for SeaORM entities
|
||||
//! Provides resource type enumerations matching SurrealDB ResourceEnum
|
||||
|
||||
use std::fmt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Database resource enumeration.
|
||||
///
|
||||
/// Represents all database tables used in the application.
|
||||
/// Each variant corresponds to a SurrealDB table name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
use super::types::PgUuid;
|
||||
|
||||
/// Database resource enumeration for SeaORM
|
||||
/// Matches the SurrealDB ResourceEnum with PostgreSQL compatibility
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ResourceEnum {
|
||||
/// OTP cache table for temporary authentication codes
|
||||
OtpCache,
|
||||
@@ -37,22 +36,6 @@ pub enum ResourceEnum {
|
||||
Testimonials,
|
||||
/// Mentors table for mentor profiles
|
||||
Mentors,
|
||||
/// Teams table for user teams
|
||||
Teams,
|
||||
/// Team members table for team membership
|
||||
TeamMembers,
|
||||
/// Team invitations table for pending invitations
|
||||
TeamInvitations,
|
||||
/// Hackathons table for hackathon events
|
||||
Hackathons,
|
||||
/// Hackathon events table for hackathon-specific events
|
||||
HackathonEvents,
|
||||
/// Hackathon timeline table for schedule milestones
|
||||
HackathonTimeline,
|
||||
/// Hackathon submissions table for project submissions
|
||||
HackathonSubmissions,
|
||||
/// Hackathon registrations table for participant registrations
|
||||
HackathonRegistrations,
|
||||
/// Notifications table for user notifications
|
||||
Notifications,
|
||||
/// Rate limiting table for IP-based rate limiting
|
||||
@@ -61,6 +44,8 @@ pub enum ResourceEnum {
|
||||
AuditLog,
|
||||
/// Sessions table for mentoring sessions
|
||||
Sessions,
|
||||
/// Migration status tracking table
|
||||
MigrationStatus,
|
||||
}
|
||||
|
||||
impl fmt::Display for ResourceEnum {
|
||||
@@ -79,18 +64,11 @@ impl fmt::Display for ResourceEnum {
|
||||
ResourceEnum::Events => "app_events",
|
||||
ResourceEnum::Testimonials => "app_testimonials",
|
||||
ResourceEnum::Mentors => "app_mentors",
|
||||
ResourceEnum::Teams => "app_teams",
|
||||
ResourceEnum::TeamMembers => "app_team_members",
|
||||
ResourceEnum::TeamInvitations => "app_team_invitations",
|
||||
ResourceEnum::Hackathons => "app_hackathons",
|
||||
ResourceEnum::HackathonEvents => "app_hackathon_events",
|
||||
ResourceEnum::HackathonTimeline => "app_hackathon_timeline",
|
||||
ResourceEnum::HackathonSubmissions => "app_hackathon_submissions",
|
||||
ResourceEnum::HackathonRegistrations => "hackathon_registrations",
|
||||
ResourceEnum::Notifications => "notifications",
|
||||
ResourceEnum::Notifications => "app_notifications",
|
||||
ResourceEnum::RateLimit => "app_rate_limit",
|
||||
ResourceEnum::AuditLog => "app_audit_log",
|
||||
ResourceEnum::Sessions => "app_sessions",
|
||||
ResourceEnum::MigrationStatus => "app_migration_status",
|
||||
};
|
||||
write!(f, "{}", table_name)
|
||||
}
|
||||
@@ -100,15 +78,7 @@ impl ResourceEnum {
|
||||
/// Get the table name as a string slice.
|
||||
///
|
||||
/// # Returns
|
||||
/// The SurrealDB table name for this resource
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use imphnen_libs::ResourceEnum;
|
||||
///
|
||||
/// let users = ResourceEnum::Users;
|
||||
/// assert_eq!(users.as_str(), "app_users");
|
||||
/// ```
|
||||
/// The PostgreSQL table name for this resource
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ResourceEnum::Users => "app_users",
|
||||
@@ -124,21 +94,30 @@ impl ResourceEnum {
|
||||
ResourceEnum::Events => "app_events",
|
||||
ResourceEnum::Testimonials => "app_testimonials",
|
||||
ResourceEnum::Mentors => "app_mentors",
|
||||
ResourceEnum::Teams => "app_teams",
|
||||
ResourceEnum::TeamMembers => "app_team_members",
|
||||
ResourceEnum::TeamInvitations => "app_team_invitations",
|
||||
ResourceEnum::Hackathons => "app_hackathons",
|
||||
ResourceEnum::HackathonEvents => "app_hackathon_events",
|
||||
ResourceEnum::HackathonTimeline => "app_hackathon_timeline",
|
||||
ResourceEnum::HackathonSubmissions => "app_hackathon_submissions",
|
||||
ResourceEnum::HackathonRegistrations => "hackathon_registrations",
|
||||
ResourceEnum::Notifications => "notifications",
|
||||
ResourceEnum::Notifications => "app_notifications",
|
||||
ResourceEnum::RateLimit => "app_rate_limit",
|
||||
ResourceEnum::AuditLog => "app_audit_log",
|
||||
ResourceEnum::Sessions => "app_sessions",
|
||||
ResourceEnum::MigrationStatus => "app_migration_status",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the schema name for the resource
|
||||
///
|
||||
/// # Returns
|
||||
/// The database schema name (usually "public" for PostgreSQL)
|
||||
pub fn schema(&self) -> &'static str {
|
||||
"public"
|
||||
}
|
||||
|
||||
/// Create a SeaORM entity name from the resource enum
|
||||
///
|
||||
/// # Returns
|
||||
/// A string suitable for use as a SeaORM entity name
|
||||
pub fn to_entity_name(&self) -> String {
|
||||
self.as_str().replace("app_", "").to_pascal_case()
|
||||
}
|
||||
|
||||
/// Check if this resource is cache-related.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -161,20 +140,6 @@ impl ResourceEnum {
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if this resource is hackathon-related.
|
||||
///
|
||||
/// # Returns
|
||||
/// true if the resource is part of the hackathon system, false otherwise
|
||||
pub fn is_hackathon(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ResourceEnum::Hackathons
|
||||
| ResourceEnum::HackathonEvents
|
||||
| ResourceEnum::HackathonTimeline
|
||||
| ResourceEnum::HackathonSubmissions
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if this resource is user-related.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -185,4 +150,57 @@ impl ResourceEnum {
|
||||
ResourceEnum::Users | ResourceEnum::UsersCache | ResourceEnum::Mentors
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate a reference ID for the resource
|
||||
///
|
||||
/// # Returns
|
||||
/// A formatted string suitable for use as a reference ID
|
||||
pub fn generate_ref_id(&self, uuid: &PgUuid) -> String {
|
||||
format!("{}_{}", self.as_str().replace("app_", ""), uuid.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper trait for string case conversion
|
||||
trait ToPascalCase {
|
||||
fn to_pascal_case(&self) -> String;
|
||||
}
|
||||
|
||||
impl ToPascalCase for str {
|
||||
fn to_pascal_case(&self) -> String {
|
||||
self.split('_')
|
||||
.map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..])
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resource_enum_table_names() {
|
||||
assert_eq!(ResourceEnum::Users.as_str(), "app_users");
|
||||
assert_eq!(ResourceEnum::Roles.as_str(), "app_roles");
|
||||
assert_eq!(ResourceEnum::GachaItems.as_str(), "app_gacha_items");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_enum_display() {
|
||||
assert_eq!(format!("{}", ResourceEnum::Users), "app_users");
|
||||
assert_eq!(format!("{}", ResourceEnum::RolesPermissions), "app_roles_permissions");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_enum_categories() {
|
||||
assert!(ResourceEnum::Users.is_user_related());
|
||||
assert!(ResourceEnum::GachaItems.is_gacha());
|
||||
assert!(ResourceEnum::OtpCache.is_cache());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_enum_to_entity_name() {
|
||||
assert_eq!(ResourceEnum::Users.to_entity_name(), "Users");
|
||||
assert_eq!(ResourceEnum::RolesPermissions.to_entity_name(), "RolesPermissions");
|
||||
assert_eq!(ResourceEnum::GachaItems.to_entity_name(), "GachaItems");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! SeaORM entity for Events table
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "events")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub name: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub description: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub detail_link: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub price: f64,
|
||||
|
||||
#[sea_orm(default = "false")]
|
||||
pub is_online: bool,
|
||||
|
||||
#[sea_orm(default = "false")]
|
||||
pub is_deleted: bool,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub location: Option<String>,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub start_date: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub end_date: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod enums;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
pub mod audit_log;
|
||||
pub mod rate_limit;
|
||||
pub mod events;
|
||||
pub mod testimonials;
|
||||
|
||||
pub use enums::ResourceEnum;
|
||||
pub use types::PgUuid;
|
||||
pub use utils::{generate_uuid, current_timestamp};
|
||||
@@ -0,0 +1,21 @@
|
||||
//! SeaORM Entity for RateLimit
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "app_rate_limit")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: String,
|
||||
pub ip_address: String,
|
||||
pub request_count: u32,
|
||||
pub first_request_time: DateTimeWithTimeZone,
|
||||
pub last_request_time: DateTimeWithTimeZone,
|
||||
pub window_duration_secs: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! SeaORM entity for Testimonials table
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid; // Added Uuid import
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "testimonials")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(not_null, column_type = "Uuid")]
|
||||
pub user_id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub role: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub content: String,
|
||||
|
||||
#[sea_orm(default = "false")]
|
||||
pub is_deleted: bool,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "crate::seaorm::auth::users::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "crate::seaorm::auth::users::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
Users,
|
||||
}
|
||||
|
||||
impl Related<crate::seaorm::auth::users::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Users.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Shared type definitions for SeaORM entities
|
||||
//! Provides PostgreSQL-compatible type aliases and custom types
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
/// UUID type alias for PostgreSQL UUID compatibility
|
||||
/// Uses `Uuid` from the `uuid` crate with SeaORM conversion traits
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct PgUuid(pub Uuid);
|
||||
|
||||
impl From<Uuid> for PgUuid {
|
||||
fn from(uuid: Uuid) -> Self {
|
||||
Self(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PgUuid> for Uuid {
|
||||
fn from(pg_uuid: PgUuid) -> Self {
|
||||
pg_uuid.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PgUuid> for String {
|
||||
fn from(pg_uuid: PgUuid) -> Self {
|
||||
pg_uuid.0.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Timestamp type alias for PostgreSQL TIMESTAMP with time zone
|
||||
/// Uses `DateTime<Utc>` from the `chrono` crate
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct PgTimestamp(pub DateTime<Utc>);
|
||||
|
||||
impl From<DateTime<Utc>> for PgTimestamp {
|
||||
fn from(timestamp: DateTime<Utc>) -> Self {
|
||||
Self(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PgTimestamp> for DateTime<Utc> {
|
||||
fn from(pg_timestamp: PgTimestamp) -> Self {
|
||||
pg_timestamp.0
|
||||
}
|
||||
}
|
||||
|
||||
/// JSONB type alias for PostgreSQL JSONB compatibility
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PgJsonB<T>(pub T);
|
||||
|
||||
impl<T> From<T> for PgJsonB<T>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Common fields that should be included in all entities
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CommonFields {
|
||||
pub id: PgUuid,
|
||||
pub created_at: PgTimestamp,
|
||||
pub updated_at: PgTimestamp,
|
||||
pub deleted_at: Option<PgTimestamp>,
|
||||
}
|
||||
|
||||
// Helper macros for common field definitions
|
||||
#[macro_export]
|
||||
macro_rules! common_fields {
|
||||
() => {
|
||||
pub id: ColumnDef<Uuid> = ColumnDef::new(sea_orm::sea_query::Column::new("id"))
|
||||
.primary_key()
|
||||
.not_null()
|
||||
.default(sea_orm::sea_query::Expr::cust("gen_random_uuid()")),
|
||||
pub created_at: ColumnDef<DateTime<Utc>> = ColumnDef::new(sea_orm::sea_query::Column::new("created_at"))
|
||||
.not_null()
|
||||
.default(sea_orm::sea_query::Expr::cust("now()")),
|
||||
pub updated_at: ColumnDef<DateTime<Utc>> = ColumnDef::new(sea_orm::sea_query::Column::new("updated_at"))
|
||||
.not_null()
|
||||
.default(sea_orm::sea_query::Expr::cust("now()"))
|
||||
.extra(sea_orm::sea_query::PostgresExtension::new("GENERATED ALWAYS AS (now()) STORED")),
|
||||
pub deleted_at: ColumnDef<Option<DateTime<Utc>>> = ColumnDef::new(sea_orm::sea_query::Column::new("deleted_at"))
|
||||
.default(None),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Utility functions for SeaORM entities
|
||||
//! Provides helper functions for UUID generation, timestamp handling, and resource management
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::types::{PgTimestamp, PgUuid};
|
||||
|
||||
/// Generate a new UUID for entity IDs
|
||||
/// Uses cryptographically secure random UUID version 4
|
||||
pub fn generate_uuid() -> Uuid {
|
||||
Uuid::new_v4()
|
||||
}
|
||||
|
||||
/// Generate a new timestamp for entity timestamps
|
||||
/// Uses UTC timezone with millisecond precision
|
||||
pub fn generate_timestamp() -> PgTimestamp {
|
||||
PgTimestamp(DateTime::from_timestamp_millis(Utc::now().timestamp_millis()).unwrap())
|
||||
}
|
||||
|
||||
/// Convert a string to PgUuid
|
||||
/// Returns Result<PgUuid, String> with error message on failure
|
||||
pub fn string_to_uuid(uuid_str: &str) -> Result<PgUuid, String> {
|
||||
Uuid::parse_str(uuid_str)
|
||||
.map(PgUuid)
|
||||
.map_err(|e| format!("Invalid UUID format: {e}"))
|
||||
}
|
||||
|
||||
/// Convert PgUuid to string representation
|
||||
pub fn uuid_to_string(uuid: &uuid::Uuid) -> String {
|
||||
uuid.to_string()
|
||||
}
|
||||
|
||||
/// Get current timestamp as DateTime<Utc>
|
||||
pub fn current_timestamp() -> DateTime<Utc> {
|
||||
Utc::now()
|
||||
}
|
||||
|
||||
/// Format timestamp for display
|
||||
pub fn format_timestamp(timestamp: &PgTimestamp) -> String {
|
||||
timestamp.0.format("%Y-%m-%d %H:%M:%S UTC").to_string()
|
||||
}
|
||||
|
||||
/// Create a soft delete timestamp
|
||||
pub fn create_deleted_at() -> Option<DateTime<Utc>> {
|
||||
Some(current_timestamp())
|
||||
}
|
||||
|
||||
/// Remove soft delete timestamp
|
||||
pub fn remove_deleted_at() -> Option<PgTimestamp> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_generate_uuid() {
|
||||
let uuid1 = generate_uuid();
|
||||
let uuid2 = generate_uuid();
|
||||
assert_ne!(uuid1, uuid2);
|
||||
assert!(Uuid::parse_str(&uuid_to_string(&uuid1)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_timestamp() {
|
||||
let ts1 = generate_timestamp();
|
||||
let ts2 = generate_timestamp();
|
||||
// Timestamps should be close to each other
|
||||
let diff = ts2.0.signed_duration_since(ts1.0).num_milliseconds();
|
||||
assert!(diff >= 0);
|
||||
assert!(diff < 1000); // Should be within 1 second
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_to_uuid() {
|
||||
let uuid_str = "123e4567-e89b-12d3-a456-426614174000";
|
||||
let result = string_to_uuid(uuid_str);
|
||||
assert!(result.is_ok());
|
||||
let uuid = result.unwrap();
|
||||
// `uuid` is a `PgUuid`; convert to `Uuid` before comparing string representation
|
||||
let uuid_plain: uuid::Uuid = uuid.into();
|
||||
assert_eq!(uuid_to_string(&uuid_plain), uuid_str);
|
||||
|
||||
let invalid_uuid = "invalid-uuid";
|
||||
let result = string_to_uuid(invalid_uuid);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
//! SeaORM entity for GachaClaims table
|
||||
//! Corresponding to ResourceEnum::GachaClaims
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveModelBehavior, DeriveEntityModel, DeriveRelation};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "app_gacha_claims")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub user_id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub gacha_item_id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub claim_id: Uuid,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub claim_type: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub status: String,
|
||||
|
||||
#[sea_orm(default = "0")]
|
||||
pub quantity: i32,
|
||||
|
||||
#[sea_orm(type = "jsonb", nullable)]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub claimed_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, sea_orm::EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
// Default implementation - SeaORM will handle timestamps automatically
|
||||
}
|
||||
|
||||
// Builder pattern for GachaClaim creation
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct GachaClaimBuilder {
|
||||
user_id: Option<Uuid>,
|
||||
gacha_item_id: Option<Uuid>,
|
||||
claim_type: Option<String>,
|
||||
status: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl GachaClaimBuilder {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn user_id(mut self, user_id: Uuid) -> Self {
|
||||
self.user_id = Some(user_id);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn gacha_item_id(mut self, gacha_item_id: Uuid) -> Self {
|
||||
self.gacha_item_id = Some(gacha_item_id);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn claim_type(mut self, claim_type: String) -> Self {
|
||||
self.claim_type = Some(claim_type);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn status(mut self, status: String) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
self.quantity = Some(quantity);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
|
||||
self.metadata = Some(metadata);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<ActiveModel, String> {
|
||||
let mut active_model = <ActiveModel as std::default::Default>::default();
|
||||
|
||||
if let Some(user_id) = self.user_id {
|
||||
active_model.user_id = Set(user_id);
|
||||
} else {
|
||||
return Err("User ID is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(gacha_item_id) = self.gacha_item_id {
|
||||
active_model.gacha_item_id = Set(gacha_item_id);
|
||||
} else {
|
||||
return Err("Gacha Item ID is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(claim_type) = self.claim_type {
|
||||
active_model.claim_type = Set(claim_type);
|
||||
} else {
|
||||
return Err("Claim type is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(status) = self.status {
|
||||
active_model.status = Set(status);
|
||||
} else {
|
||||
return Err("Status is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(quantity) = self.quantity {
|
||||
active_model.quantity = Set(quantity);
|
||||
}
|
||||
|
||||
if let Some(metadata) = self.metadata {
|
||||
active_model.metadata = Set(Some(metadata));
|
||||
}
|
||||
|
||||
Ok(active_model)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::seaorm::common::utils::generate_uuid;
|
||||
|
||||
#[test]
|
||||
fn test_gacha_claim_model_creation() {
|
||||
let user_id = generate_uuid();
|
||||
let gacha_item_id = generate_uuid();
|
||||
|
||||
let claim = GachaClaimBuilder::new()
|
||||
.user_id(user_id)
|
||||
.gacha_item_id(gacha_item_id)
|
||||
.claim_type("direct".to_string())
|
||||
.status("claimed".to_string())
|
||||
.quantity(1)
|
||||
.build();
|
||||
|
||||
assert!(claim.is_ok());
|
||||
let claim_model = claim.unwrap();
|
||||
assert_eq!(claim_model.user_id, Set(user_id));
|
||||
assert_eq!(claim_model.gacha_item_id, Set(gacha_item_id));
|
||||
assert_eq!(claim_model.claim_type, Set("direct".to_string()));
|
||||
assert_eq!(claim_model.status, Set("claimed".to_string()));
|
||||
assert_eq!(claim_model.quantity, Set(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid; // Added Uuid import
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "gacha_credits")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
#[sea_orm(column_type = "Uuid")]
|
||||
pub user_id: Uuid,
|
||||
pub available_rolls: i32,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<DateTime>,
|
||||
pub updated_at: Option<DateTime>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::super::auth::users::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::super::auth::users::Column::Id"
|
||||
)]
|
||||
Users,
|
||||
}
|
||||
|
||||
impl Related<super::super::auth::users::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Users.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,256 @@
|
||||
//! SeaORM entity for GachaItems table
|
||||
//! Corresponding to ResourceEnum::GachaItems
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveModelBehavior, DeriveEntityModel, DeriveRelation};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "app_gacha_items")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(unique, not_null)]
|
||||
pub item_code: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub name: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub description: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub rarity: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub type_: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub category: String,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub value: i32,
|
||||
|
||||
#[sea_orm(not_null)]
|
||||
pub weight: f64,
|
||||
|
||||
#[sea_orm(default = "0")]
|
||||
pub stock: i32,
|
||||
|
||||
#[sea_orm(default = "false")]
|
||||
pub is_limited: bool,
|
||||
|
||||
#[sea_orm(type = "jsonb", nullable)]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(not_null, default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(nullable)]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, sea_orm::EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
// Default implementation - SeaORM will handle timestamps automatically
|
||||
}
|
||||
|
||||
// Builder pattern for GachaItem creation
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct GachaItemBuilder {
|
||||
item_code: Option<String>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
rarity: Option<String>,
|
||||
type_: Option<String>,
|
||||
category: Option<String>,
|
||||
value: Option<i32>,
|
||||
weight: Option<f64>,
|
||||
stock: Option<i32>,
|
||||
is_limited: Option<bool>,
|
||||
metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl GachaItemBuilder {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn item_code(mut self, item_code: String) -> Self {
|
||||
self.item_code = Some(item_code);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = Some(name);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn description(mut self, description: String) -> Self {
|
||||
self.description = Some(description);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn rarity(mut self, rarity: String) -> Self {
|
||||
self.rarity = Some(rarity);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn type_(mut self, type_: String) -> Self {
|
||||
self.type_ = Some(type_);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn category(mut self, category: String) -> Self {
|
||||
self.category = Some(category);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn value(mut self, value: i32) -> Self {
|
||||
self.value = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn weight(mut self, weight: f64) -> Self {
|
||||
self.weight = Some(weight);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn stock(mut self, stock: i32) -> Self {
|
||||
self.stock = Some(stock);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_limited(mut self, is_limited: bool) -> Self {
|
||||
self.is_limited = Some(is_limited);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
|
||||
self.metadata = Some(metadata);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<ActiveModel, String> {
|
||||
let mut active_model = <ActiveModel as std::default::Default>::default();
|
||||
|
||||
if let Some(item_code) = self.item_code {
|
||||
active_model.item_code = Set(item_code);
|
||||
} else {
|
||||
return Err("Item code is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(name) = self.name {
|
||||
active_model.name = Set(name);
|
||||
} else {
|
||||
return Err("Name is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(description) = self.description {
|
||||
active_model.description = Set(description);
|
||||
} else {
|
||||
return Err("Description is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(rarity) = self.rarity {
|
||||
active_model.rarity = Set(rarity);
|
||||
} else {
|
||||
return Err("Rarity is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(type_) = self.type_ {
|
||||
active_model.type_ = Set(type_);
|
||||
} else {
|
||||
return Err("Type is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(category) = self.category {
|
||||
active_model.category = Set(category);
|
||||
} else {
|
||||
return Err("Category is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(value) = self.value {
|
||||
active_model.value = Set(value);
|
||||
} else {
|
||||
return Err("Value is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(weight) = self.weight {
|
||||
active_model.weight = Set(weight);
|
||||
} else {
|
||||
return Err("Weight is required".to_string());
|
||||
}
|
||||
|
||||
if let Some(stock) = self.stock {
|
||||
active_model.stock = Set(stock);
|
||||
}
|
||||
|
||||
if let Some(is_limited) = self.is_limited {
|
||||
active_model.is_limited = Set(is_limited);
|
||||
}
|
||||
|
||||
if let Some(metadata) = self.metadata {
|
||||
active_model.metadata = Set(Some(metadata));
|
||||
}
|
||||
|
||||
Ok(active_model)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_gacha_item_model_creation() {
|
||||
let item = GachaItemBuilder::new()
|
||||
.item_code("SWORD_001".to_string())
|
||||
.name("Legendary Sword".to_string())
|
||||
.description("A powerful legendary sword".to_string())
|
||||
.rarity("legendary".to_string())
|
||||
.type_("weapon".to_string())
|
||||
.category("sword".to_string())
|
||||
.value(100)
|
||||
.weight(0.01)
|
||||
.stock(10)
|
||||
.is_limited(true)
|
||||
.build();
|
||||
|
||||
assert!(item.is_ok());
|
||||
let item_model = item.unwrap();
|
||||
assert_eq!(item_model.item_code, Set("SWORD_001".to_string()));
|
||||
assert_eq!(item_model.name, Set("Legendary Sword".to_string()));
|
||||
assert_eq!(item_model.description, Set("A powerful legendary sword".to_string()));
|
||||
assert_eq!(item_model.rarity, Set("legendary".to_string()));
|
||||
assert_eq!(item_model.type_, Set("weapon".to_string()));
|
||||
assert_eq!(item_model.category, Set("sword".to_string()));
|
||||
assert_eq!(item_model.value, Set(100));
|
||||
assert_eq!(item_model.weight, Set(0.01));
|
||||
assert_eq!(item_model.stock, Set(10));
|
||||
assert_eq!(item_model.is_limited, Set(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid; // Added Uuid import
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "gacha_rolls")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
#[sea_orm(column_type = "Uuid")]
|
||||
pub user_id: Uuid,
|
||||
pub gacha_id: String,
|
||||
#[sea_orm(column_type = "Uuid")]
|
||||
pub item_id: Uuid,
|
||||
pub weight: f32,
|
||||
pub quantity: i32,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<DateTime>,
|
||||
pub updated_at: Option<DateTime>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::gacha_items::Entity",
|
||||
from = "Column::ItemId",
|
||||
to = "super::gacha_items::Column::Id"
|
||||
)]
|
||||
GachaItems,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::super::auth::users::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::super::auth::users::Column::Id"
|
||||
)]
|
||||
Users,
|
||||
}
|
||||
|
||||
impl Related<super::gacha_items::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::GachaItems.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::super::auth::users::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Users.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod gacha_credits;
|
||||
pub mod gacha_rolls;
|
||||
pub mod gacha_items;
|
||||
pub mod gacha_claims;
|
||||
@@ -0,0 +1,72 @@
|
||||
//! SeaORM entity definitions for Imphenia backend
|
||||
//! Provides PostgreSQL-compatible entity definitions corresponding to SurrealDB ResourceEnum
|
||||
|
||||
pub mod auth;
|
||||
pub mod gacha;
|
||||
pub mod common;
|
||||
pub mod relationships;
|
||||
pub mod schema_validation;
|
||||
pub mod examples;
|
||||
|
||||
// Re-export specific items from modules for better API clarity
|
||||
pub use auth::{
|
||||
users, mentors, roles, permissions, roles_permissions, sessions
|
||||
};
|
||||
pub use gacha::{
|
||||
gacha_items, gacha_claims, gacha_credits, gacha_rolls
|
||||
};
|
||||
pub use common::{
|
||||
ResourceEnum, PgUuid, generate_uuid, current_timestamp,
|
||||
audit_log, rate_limit, events, testimonials
|
||||
};
|
||||
pub use relationships;
|
||||
pub use schema_validation;
|
||||
pub use examples;
|
||||
|
||||
/// Initialize the SeaORM entity system
|
||||
/// Should be called once at application startup
|
||||
pub fn initialize() -> Result<(), String> {
|
||||
// Perform schema validation on initialization
|
||||
validate_schema_equivalence()?;
|
||||
|
||||
// Initialize any global utilities or configurations
|
||||
common::utils::initialize_utils();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the table name for a given ResourceEnum
|
||||
/// Provides a consistent way to access table names across the application
|
||||
pub fn get_table_name(resource: &common::enums::ResourceEnum) -> &str {
|
||||
resource.as_str()
|
||||
}
|
||||
|
||||
/// Get the schema name for all entities (default: "public")
|
||||
pub fn get_schema_name() -> &str {
|
||||
"public"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use common::enums::ResourceEnum;
|
||||
|
||||
#[test]
|
||||
fn test_table_name_resolution() {
|
||||
assert_eq!(get_table_name(&ResourceEnum::Users), "app_users");
|
||||
assert_eq!(get_table_name(&ResourceEnum::Roles), "app_roles");
|
||||
assert_eq!(get_table_name(&ResourceEnum::GachaItems), "app_gacha_items");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_name() {
|
||||
assert_eq!(get_schema_name(), "public");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initialize() {
|
||||
// This should not panic and should return Ok(())
|
||||
let result = initialize();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Migration status tracking entity for database migration validation
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{Utc, DateTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
// PgUuid and PgTimestamp are not used in this file, but kept for potential future use
|
||||
// use crate::seaorm::common::types::{PgUuid, PgTimestamp};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)]
|
||||
#[sea_orm(table_name = "app_migration_status")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub resource_type: String,
|
||||
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub status: String,
|
||||
|
||||
#[sea_orm(column_type = "Json", default = "null")]
|
||||
pub validation_results: Option<serde_json::Value>,
|
||||
|
||||
#[sea_orm(column_type = "Text", default = "null")]
|
||||
pub last_error: Option<String>,
|
||||
|
||||
#[sea_orm(column_type = "Integer", default = 0)]
|
||||
pub total_records: i32,
|
||||
|
||||
#[sea_orm(column_type = "Integer", default = 0)]
|
||||
pub validated_records: i32,
|
||||
|
||||
#[sea_orm(column_type = "Integer", default = 0)]
|
||||
pub failed_records: i32,
|
||||
|
||||
#[sea_orm(column_type = "Integer", default = 0)]
|
||||
pub skipped_records: i32,
|
||||
|
||||
#[sea_orm(column_type = "Text", default = "null")]
|
||||
pub validation_mode: Option<String>,
|
||||
|
||||
#[sea_orm(column_type = "Timestamp", default = "now()")]
|
||||
pub last_validated_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(column_type = "Timestamp", default = "now()")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(column_type = "Timestamp", default = "now()")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
#[sea_orm(column_type = "Timestamp", default = "null")]
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, sea_orm::EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
// Default implementation - SeaORM will handle timestamps automatically
|
||||
}
|
||||
|
||||
/// Migration status constants
|
||||
pub mod status {
|
||||
pub const PENDING: &str = "pending";
|
||||
pub const IN_PROGRESS: &str = "in_progress";
|
||||
pub const COMPLETED: &str = "completed";
|
||||
pub const FAILED: &str = "failed";
|
||||
pub const PARTIAL: &str = "partial";
|
||||
pub const SKIPPED: &str = "skipped";
|
||||
}
|
||||
|
||||
/// Validation mode constants
|
||||
pub mod validation_mode {
|
||||
pub const FULL: &str = "full";
|
||||
pub const INCREMENTAL: &str = "incremental";
|
||||
pub const QUICK_CHECK: &str = "quick_check";
|
||||
}
|
||||
|
||||
/// Resource type constants matching ResourceEnum
|
||||
pub mod resource_type {
|
||||
pub const USERS: &str = "users";
|
||||
pub const ROLES: &str = "roles";
|
||||
pub const PERMISSIONS: &str = "permissions";
|
||||
pub const ROLES_PERMISSIONS: &str = "roles_permissions";
|
||||
pub const GACHA_ITEMS: &str = "gacha_items";
|
||||
pub const GACHA_CLAIMS: &str = "gacha_claims";
|
||||
pub const GACHA_ROLLS: &str = "gacha_rolls";
|
||||
pub const GACHA_CREDITS: &str = "gacha_credits";
|
||||
pub const NOTIFICATIONS: &str = "notifications";
|
||||
pub const AUDIT_LOG: &str = "audit_log";
|
||||
pub const SESSIONS: &str = "sessions";
|
||||
pub const OTP_CACHE: &str = "otp_cache";
|
||||
pub const USERS_CACHE: &str = "users_cache";
|
||||
pub const RATE_LIMIT: &str = "rate_limit";
|
||||
pub const TESTIMONIALS: &str = "testimonials";
|
||||
pub const MENTORS: &str = "mentors";
|
||||
pub const EVENTS: &str = "events";
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// SeaORM entity definitions for Imphenia backend
|
||||
// This module provides PostgreSQL-compatible entity definitions
|
||||
// corresponding to the SurrealDB ResourceEnum
|
||||
|
||||
pub mod auth;
|
||||
pub mod gacha;
|
||||
pub mod common;
|
||||
pub mod migration_status;
|
||||
+118
-65
@@ -1,5 +1,4 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use crate::permissions::{PermissionsQueryDto, PermissionsItemDto};
|
||||
|
||||
@@ -21,71 +20,12 @@ pub struct EducationDto {
|
||||
pub period: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RolesDetailQueryDto {
|
||||
pub id: Thing,
|
||||
pub name: String,
|
||||
pub permissions: Option<Vec<Option<PermissionsQueryDto>>>,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for RolesDetailQueryDto {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Thing::from(("".to_string(), surrealdb::sql::Id::Number(0))),
|
||||
name: String::new(),
|
||||
permissions: None,
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Default)]
|
||||
pub struct RolesDetailItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub is_deleted: bool,
|
||||
pub permissions: Vec<PermissionsItemDto>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl RolesDetailItemDto {
|
||||
pub fn from(dto: &RolesDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id.id.to_raw(),
|
||||
name: dto.name.clone(),
|
||||
is_deleted: dto.is_deleted,
|
||||
permissions: dto
|
||||
.permissions
|
||||
.as_ref()
|
||||
.unwrap_or(&vec![])
|
||||
.iter()
|
||||
.filter_map(|p| p.as_ref())
|
||||
.map(PermissionsItemDto::from)
|
||||
.collect(),
|
||||
created_at: dto.created_at.clone(),
|
||||
updated_at: dto.updated_at.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UsersDetailQueryDto {
|
||||
pub id: Thing,
|
||||
pub fullname: String,
|
||||
pub legal_name: Option<String>,
|
||||
pub email: String,
|
||||
pub avatar: Option<String>,
|
||||
pub phone_number: String,
|
||||
pub struct UserProfileExtensionDto {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_number: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub is_deleted: bool,
|
||||
pub gender: Option<String>,
|
||||
pub birthdate: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
@@ -102,11 +42,100 @@ pub struct UsersDetailQueryDto {
|
||||
pub experience: Option<Vec<ExperienceDto>>,
|
||||
pub education: Option<Vec<EducationDto>>,
|
||||
pub career_status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Default)]
|
||||
pub struct RolesDetailQueryDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub permissions: Option<Vec<Option<PermissionsQueryDto>>>,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Default)]
|
||||
pub struct RolesDetailItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub is_deleted: bool,
|
||||
pub permissions: Vec<PermissionsItemDto>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl RolesDetailItemDto {
|
||||
pub fn from(dto: &RolesDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id.clone(),
|
||||
name: dto.name.clone(),
|
||||
is_deleted: dto.is_deleted,
|
||||
permissions: dto
|
||||
.permissions
|
||||
.as_ref()
|
||||
.unwrap_or(&vec![])
|
||||
.iter()
|
||||
.filter_map(|p| p.as_ref())
|
||||
.map(PermissionsItemDto::from)
|
||||
.collect(),
|
||||
created_at: dto.created_at.clone(),
|
||||
updated_at: dto.updated_at.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct UsersDetailQueryDto {
|
||||
pub id: String,
|
||||
pub fullname: String,
|
||||
pub legal_name: Option<String>,
|
||||
pub email: String,
|
||||
pub avatar: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub is_deleted: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile_extension: Option<UserProfileExtensionDto>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_number: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bio: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub website_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub twitter_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub location: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub skills: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experience: Option<Vec<ExperienceDto>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub education: Option<Vec<EducationDto>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub career_status: Option<String>,
|
||||
pub password: String,
|
||||
pub role: RolesDetailQueryDto,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub mentor_id: Option<Thing>,
|
||||
pub mentor_id: Option<String>,
|
||||
}
|
||||
|
||||
impl UsersDetailQueryDto {
|
||||
@@ -114,3 +143,27 @@ impl UsersDetailQueryDto {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl UsersDetailQueryDto {
|
||||
pub fn from_profile_extension(mut self) -> Self {
|
||||
if let Some(ext) = &self.profile_extension {
|
||||
self.phone_number = ext.phone_number.clone();
|
||||
self.phone_for_verification = ext.phone_for_verification.clone();
|
||||
self.gender = ext.gender.clone();
|
||||
self.domicile = ext.domicile.clone();
|
||||
self.bio = ext.bio.clone();
|
||||
self.last_education = ext.last_education.clone();
|
||||
self.linkedin_url = ext.linkedin_url.clone();
|
||||
self.github_url = ext.github_url.clone();
|
||||
self.cv_url = ext.cv_url.clone();
|
||||
self.portfolio_url = ext.portfolio_url.clone();
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UsersDetailQueryDto {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.id)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ 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
|
||||
@@ -26,3 +25,5 @@ utoipa-swagger-ui.workspace = true
|
||||
rand_distr.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
sea-orm.workspace = true
|
||||
uuid = "1.18"
|
||||
|
||||
@@ -24,18 +24,13 @@ pub use imphnen_libs::{
|
||||
};
|
||||
|
||||
pub use imphnen_utils::{
|
||||
bind_filter,
|
||||
csrf_token,
|
||||
extract_email,
|
||||
generate_date,
|
||||
generate_otp,
|
||||
get_id,
|
||||
logger,
|
||||
make_thing,
|
||||
query_builder,
|
||||
query_list,
|
||||
response_format,
|
||||
serde_helpers,
|
||||
validator,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use imphnen_iam::{UsersDetailItemDto, UsersDetailQueryDto};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
@@ -45,7 +44,7 @@ pub struct GachaClaimItemDto {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaClaimQueryDto {
|
||||
pub id: Thing,
|
||||
pub id: String,
|
||||
pub user: UsersDetailQueryDto,
|
||||
pub item: GachaItemSchema,
|
||||
pub is_deleted: bool,
|
||||
@@ -56,7 +55,7 @@ pub struct GachaClaimQueryDto {
|
||||
impl GachaClaimItemDto {
|
||||
pub fn from(dto: &GachaClaimQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id.id.to_raw(),
|
||||
id: dto.id.clone(),
|
||||
user: UsersDetailItemDto::from(&dto.user),
|
||||
item: GachaItemDto::from(dto.item.clone()),
|
||||
is_deleted: dto.is_deleted,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::v1::gacha_claims::gacha_claims_dto::GachaClaimQueryDto;
|
||||
use crate::v1::gacha_claims::gacha_claims_schema::GachaClaimSchema;
|
||||
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||
use crate::AppState;
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::DetailQueryBuilder;
|
||||
use std::time::Instant;
|
||||
use tracing::{instrument, info};
|
||||
use anyhow::{Result, anyhow};
|
||||
use imphnen_entities::seaorm::gacha::gacha_claims::{Entity as GachaClaimsEntity, ActiveModel as GachaClaimsActiveModel};
|
||||
use imphnen_entities::seaorm::gacha::gacha_items::Entity as GachaItemsEntity;
|
||||
use imphnen_iam::{UsersRepository, UsersDetailQueryDto};
|
||||
use sea_orm::{EntityTrait, ActiveModelTrait, ActiveValue};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct GachaClaimRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -21,27 +24,55 @@ impl<'a> GachaClaimRepository<'a> {
|
||||
&self,
|
||||
id: String,
|
||||
) -> Result<GachaClaimQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::GachaClaims.to_string())
|
||||
.with_id(id.clone())
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("item")
|
||||
.with_fetch("user");
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query");
|
||||
let result: Option<GachaClaimQueryDto> =
|
||||
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_gacha_claim_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
match result {
|
||||
Some(claim) if !claim.is_deleted => Ok(claim),
|
||||
_ => bail!("Gacha Claim not found"),
|
||||
}
|
||||
let conn = &self.state.postgres_connection.conn;
|
||||
let claim_uuid = Uuid::parse_str(&id).map_err(|e| anyhow!("Invalid ID format: {}", e))?;
|
||||
|
||||
let claim_model = GachaClaimsEntity::find_by_id(claim_uuid)
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Gacha claim not found"))?;
|
||||
|
||||
// Fetch User
|
||||
let user_repo = UsersRepository::new(self.state);
|
||||
let user_dto: UsersDetailQueryDto = user_repo
|
||||
.query_user_by_id(&claim_model.user_id.to_string())
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch user: {}", e))?;
|
||||
|
||||
// Fetch Item
|
||||
let item_model = GachaItemsEntity::find_by_id(claim_model.gacha_item_id)
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Gacha item not found"))?;
|
||||
|
||||
// Convert Item Model to Schema
|
||||
let item_schema = GachaItemSchema {
|
||||
id: item_model.id.to_string(),
|
||||
item_code: item_model.item_code,
|
||||
name: item_model.name,
|
||||
description: item_model.description,
|
||||
rarity: item_model.rarity,
|
||||
type_: item_model.type_,
|
||||
category: item_model.category,
|
||||
value: item_model.value,
|
||||
weight: item_model.weight,
|
||||
stock: item_model.stock,
|
||||
is_limited: item_model.is_limited,
|
||||
metadata: item_model.metadata,
|
||||
image_url: "".to_string(), // Field not present in DB model
|
||||
is_deleted: item_model.deleted_at.is_some(),
|
||||
created_at: Some(item_model.created_at.to_rfc3339()),
|
||||
updated_at: Some(item_model.updated_at.to_rfc3339()),
|
||||
};
|
||||
|
||||
Ok(GachaClaimQueryDto {
|
||||
id: claim_model.id.to_string(),
|
||||
user: user_dto,
|
||||
item: item_schema,
|
||||
is_deleted: claim_model.deleted_at.is_some(),
|
||||
created_at: Some(claim_model.created_at.to_rfc3339()),
|
||||
updated_at: Some(claim_model.updated_at.to_rfc3339()),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
@@ -49,26 +80,43 @@ impl<'a> GachaClaimRepository<'a> {
|
||||
&self,
|
||||
data: GachaClaimSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
info!(
|
||||
resource = %ResourceEnum::GachaClaims.to_string(),
|
||||
content = ?data,
|
||||
"Executing SurrealDB create query"
|
||||
);
|
||||
let record: Option<GachaClaimSchema> = db
|
||||
.create(ResourceEnum::GachaClaims.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_gacha_claim' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Claim".into()),
|
||||
None => bail!("Failed to create Gacha Claim"),
|
||||
}
|
||||
let conn = &self.state.postgres_connection.conn;
|
||||
|
||||
// Extract UUIDs from data (which uses Strings)
|
||||
// GachaClaimSchema uses "thing" format (e.g., "Users:uuid"), we might need to strip prefix if present,
|
||||
// but looking at schema implementation it seems it might store just UUID string or thing string.
|
||||
// Let's assume it's a UUID string or clean it.
|
||||
|
||||
let user_id_str = data.user.split(':').next_back().unwrap_or(&data.user);
|
||||
let item_id_str = data.item.split(':').next_back().unwrap_or(&data.item);
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id_str).map_err(|e| anyhow!("Invalid User UUID: {}", e))?;
|
||||
let item_uuid = Uuid::parse_str(item_id_str).map_err(|e| anyhow!("Invalid Item UUID: {}", e))?;
|
||||
let claim_id = Uuid::new_v4(); // Generate a new ID for the claim record itself
|
||||
let id_uuid = if data.id.is_empty() {
|
||||
Uuid::new_v4()
|
||||
} else {
|
||||
let clean_id = data.id.split(':').next_back().unwrap_or(&data.id);
|
||||
Uuid::parse_str(clean_id).unwrap_or_else(|_| Uuid::new_v4())
|
||||
};
|
||||
|
||||
let active_model = GachaClaimsActiveModel {
|
||||
id: ActiveValue::Set(id_uuid),
|
||||
user_id: ActiveValue::Set(user_uuid),
|
||||
gacha_item_id: ActiveValue::Set(item_uuid),
|
||||
claim_id: ActiveValue::Set(claim_id), // Using random UUID for claim_id as it's required but not in Schema
|
||||
claim_type: ActiveValue::Set("standard".to_string()), // Default value
|
||||
status: ActiveValue::Set("claimed".to_string()), // Default value
|
||||
quantity: ActiveValue::Set(1),
|
||||
metadata: ActiveValue::Set(None),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
deleted_at: ActiveValue::NotSet,
|
||||
claimed_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
};
|
||||
|
||||
let result = active_model.insert(conn).await?;
|
||||
|
||||
Ok(result.id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use crate::v1::gacha_rolls::gacha_rolls_dto::GachaRollQueryDto;
|
||||
use crate::{make_thing};
|
||||
use imphnen_iam::get_iso_date;
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_entities::ResourceEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::v1::gacha_claims::gacha_claims_dto::GachaClaimRequestDto;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaClaimSchema {
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub item: Thing,
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
pub item: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
@@ -52,7 +52,7 @@ impl GachaClaimSchema {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn roll(roll: GachaRollQueryDto, user_id: Thing) -> Self {
|
||||
pub fn roll(roll: GachaRollQueryDto, user_id: String) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaClaims.to_string(),
|
||||
|
||||
@@ -27,8 +27,8 @@ pub struct GachaCreditResponseDto {
|
||||
impl From<&crate::v1::gacha_credits::gacha_credits_schema::GachaCreditSchema> for GachaCreditResponseDto {
|
||||
fn from(credit: &crate::v1::gacha_credits::gacha_credits_schema::GachaCreditSchema) -> Self {
|
||||
Self {
|
||||
id: credit.id.id.to_raw(),
|
||||
user_id: credit.user.id.to_raw(),
|
||||
id: credit.id.clone(),
|
||||
user_id: credit.user.clone(),
|
||||
available_rolls: credit.available_rolls,
|
||||
is_deleted: credit.is_deleted,
|
||||
created_at: credit.created_at.clone(),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::v1::gacha_credits::gacha_credits_dto::GachaCreditRequestDto;
|
||||
use crate::v1::gacha_credits::gacha_credits_schema::GachaCreditSchema;
|
||||
use crate::AppState;
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_libs::AppStatePostgresExt;
|
||||
use imphnen_entities::seaorm::gacha::gacha_credits::{self, Entity as GachaCreditsEntity, Column as GachaCreditsColumn};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::make_thing;
|
||||
use sea_orm::{QueryFilter, ActiveValue, EntityTrait, ColumnTrait};
|
||||
use std::time::Instant;
|
||||
use surrealdb::Uuid;
|
||||
use tracing::{instrument, info};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct GachaCreditRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -20,33 +20,32 @@ impl<'a> GachaCreditRepository<'a> {
|
||||
#[instrument(skip(self, user_id), err)]
|
||||
pub async fn query_by_user_id(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<Option<GachaCreditSchema>> {
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<gacha_credits::Model>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let sql = format!(
|
||||
"SELECT * FROM {} WHERE user = type::thing('{}', $user_id) AND is_deleted = false LIMIT 1",
|
||||
ResourceEnum::GachaCredits,
|
||||
ResourceEnum::Users.as_str()
|
||||
);
|
||||
info!(query = %sql, "Executing SurrealDB query");
|
||||
let result: Vec<GachaCreditSchema> =
|
||||
db.query(sql).bind(("user_id", user_id)).await?.take(0)?;
|
||||
let db = self.state.postgres_db();
|
||||
|
||||
let result = GachaCreditsEntity::find()
|
||||
.filter(GachaCreditsColumn::UserId.eq(user_id))
|
||||
.filter(GachaCreditsColumn::IsDeleted.eq(false))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_by_user_id' took: {elapsed:.2?}");
|
||||
}
|
||||
Ok(result.into_iter().next())
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, user_id), err)]
|
||||
pub async fn query_consume_credit(&self, user_id: String) -> Result<()> {
|
||||
pub async fn query_consume_credit(&self, user_id: Uuid) -> Result<()> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let db = self.state.postgres_db();
|
||||
let credit_opt = self.query_by_user_id(user_id).await?;
|
||||
let Some(mut credit) = credit_opt else {
|
||||
let Some(credit) = credit_opt else {
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
@@ -68,15 +67,12 @@ impl<'a> GachaCreditRepository<'a> {
|
||||
}
|
||||
bail!("No extra roll credits remaining");
|
||||
}
|
||||
credit.available_rolls -= 1;
|
||||
info!(operation = "update", table = %ResourceEnum::GachaCredits.to_string(), id = %credit.id.id.to_raw(), "Executing SurrealDB update for consume_credit");
|
||||
let _: Option<GachaCreditSchema> = db
|
||||
.update((
|
||||
&ResourceEnum::GachaCredits.to_string(),
|
||||
credit.id.id.to_raw(),
|
||||
))
|
||||
.merge(credit)
|
||||
.await?;
|
||||
|
||||
let mut active_model: gacha_credits::ActiveModel = credit.clone().into();
|
||||
active_model.available_rolls = ActiveValue::Set(credit.available_rolls - 1);
|
||||
|
||||
GachaCreditsEntity::update(active_model).exec(db).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
@@ -92,32 +88,23 @@ impl<'a> GachaCreditRepository<'a> {
|
||||
payload: GachaCreditRequestDto,
|
||||
) -> Result<()> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
if let Some(mut credit) = self.query_by_user_id(payload.user_id.clone()).await? {
|
||||
credit.available_rolls += payload.amount;
|
||||
info!(operation = "update", table = %ResourceEnum::GachaCredits.to_string(), id = %credit.id.id.to_raw(), "Executing SurrealDB update for add_credit");
|
||||
let _: Option<GachaCreditSchema> = db
|
||||
.update((
|
||||
&ResourceEnum::GachaCredits.to_string(),
|
||||
credit.id.id.to_raw(),
|
||||
))
|
||||
.merge(credit)
|
||||
.await?;
|
||||
let db = self.state.postgres_db();
|
||||
if let Some(credit) = self.query_by_user_id(Uuid::parse_str(&payload.user_id)?).await? {
|
||||
let mut active_model: gacha_credits::ActiveModel = credit.clone().into();
|
||||
active_model.available_rolls = ActiveValue::Set(credit.available_rolls + payload.amount);
|
||||
|
||||
GachaCreditsEntity::update(active_model).exec(db).await?;
|
||||
} else {
|
||||
let data = GachaCreditSchema {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaCredits.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
user: make_thing(&ResourceEnum::Users.to_string(), &payload.user_id),
|
||||
available_rolls: payload.amount,
|
||||
..Default::default()
|
||||
let active_model = gacha_credits::ActiveModel {
|
||||
id: ActiveValue::Set(uuid::Uuid::new_v4()),
|
||||
user_id: ActiveValue::Set(Uuid::parse_str(&payload.user_id)?),
|
||||
available_rolls: ActiveValue::Set(payload.amount),
|
||||
is_deleted: ActiveValue::Set(false),
|
||||
created_at: ActiveValue::Set(Some(chrono::Utc::now().naive_utc())),
|
||||
updated_at: ActiveValue::Set(Some(chrono::Utc::now().naive_utc())),
|
||||
};
|
||||
info!(operation = "create", table = %ResourceEnum::GachaCredits.to_string(), "Executing SurrealDB create for add_credit");
|
||||
let _: Option<GachaCreditSchema> = db
|
||||
.create(ResourceEnum::GachaCredits.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
|
||||
GachaCreditsEntity::insert(active_model).exec(db).await?;
|
||||
}
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use imphnen_iam::get_iso_date;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::{get_iso_date};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GachaCreditSchema {
|
||||
pub id: Thing,
|
||||
pub user: Thing,
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
pub available_rolls: i32,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
@@ -15,8 +15,8 @@ pub struct GachaCreditSchema {
|
||||
impl Default for GachaCreditSchema {
|
||||
fn default() -> Self {
|
||||
GachaCreditSchema {
|
||||
id: Thing::from(("app_gacha_credits", "uuid")),
|
||||
user: Thing::from(("app_users", "uuid")),
|
||||
id: Uuid::new_v4().to_string(),
|
||||
user: Uuid::new_v4().to_string(),
|
||||
available_rolls: 0,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::AppState;
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_utils::{errors::AppError, error_response};
|
||||
use imphnen_utils::{common_response, success_response, validate_request};
|
||||
use uuid::Uuid;
|
||||
use crate::v1::gacha_credits::gacha_credits_dto::{GachaCreditRequestDto, GachaCreditResponseDto};
|
||||
use crate::v1::gacha_credits::gacha_credits_repository::GachaCreditRepository;
|
||||
use axum::http::StatusCode;
|
||||
@@ -23,16 +24,28 @@ impl GachaCreditService {
|
||||
return error_response(AppError::NotFoundError("User not found".into()));
|
||||
};
|
||||
|
||||
match repo.query_by_user_id(user.id.id.to_raw()).await {
|
||||
let parsed_user_id = match Uuid::parse_str(&user.id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return error_response(AppError::BadRequestError(format!("Invalid User ID format: {}", e))),
|
||||
};
|
||||
|
||||
match repo.query_by_user_id(parsed_user_id).await {
|
||||
Ok(Some(credit)) => {
|
||||
let response_dto = GachaCreditResponseDto::from(&credit);
|
||||
let response_dto = GachaCreditResponseDto {
|
||||
id: credit.id.to_string(),
|
||||
user_id: credit.user_id.to_string(),
|
||||
available_rolls: credit.available_rolls,
|
||||
is_deleted: credit.is_deleted,
|
||||
created_at: credit.created_at.map(|d| d.to_string()),
|
||||
updated_at: credit.updated_at.map(|d| d.to_string()),
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Ok(None) => {
|
||||
// Return empty credits if no record exists
|
||||
let response_dto = GachaCreditResponseDto {
|
||||
id: "".to_string(),
|
||||
user_id: user.id.id.to_raw(),
|
||||
user_id: user.id,
|
||||
available_rolls: 0,
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
@@ -64,7 +77,7 @@ impl GachaCreditService {
|
||||
};
|
||||
|
||||
// Ensure the user can only modify their own credits
|
||||
if payload.user_id != user.id.id.to_raw() {
|
||||
if payload.user_id != user.id {
|
||||
return error_response(AppError::AuthorizationError("You can only modify your own credits".into()));
|
||||
}
|
||||
|
||||
@@ -89,7 +102,12 @@ impl GachaCreditService {
|
||||
return error_response(AppError::NotFoundError("User not found".into()));
|
||||
};
|
||||
|
||||
match repo.query_consume_credit(user.id.id.to_raw()).await {
|
||||
let parsed_user_id = match Uuid::parse_str(&user.id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return error_response(AppError::BadRequestError(format!("Invalid User ID format: {}", e))),
|
||||
};
|
||||
|
||||
match repo.query_consume_credit(parsed_user_id).await {
|
||||
Ok(_) => common_response(StatusCode::OK, "Consumed 1 credit successfully"),
|
||||
Err(e) => error_response(AppError::BadRequestError(e.to_string())),
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value; // Add this import
|
||||
use std::sync::LazyLock;
|
||||
use utoipa::ToSchema;
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
// Custom validator for image URLs
|
||||
static IMAGE_URL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^https?://[^\s]+\.(jpg|jpeg|png|gif|webp)$").unwrap());
|
||||
|
||||
pub fn validate_image_url(url: &str) -> Result<(), ValidationError> {
|
||||
lazy_static! {
|
||||
static ref IMAGE_URL_REGEX: Regex = Regex::new(r"^https?://[^\s]+\.(jpg|jpeg|png|gif|webp)$").unwrap();
|
||||
}
|
||||
if IMAGE_URL_REGEX.is_match(url) {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -19,9 +19,37 @@ pub fn validate_image_url(url: &str) -> Result<(), ValidationError> {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct GachaItemRequestDto {
|
||||
#[validate(length(min = 1, max = 100, message = "Item code must be between 1 and 100 characters"))]
|
||||
pub item_code: String,
|
||||
|
||||
#[validate(length(min = 1, max = 100, message = "Item name must be between 1 and 100 characters"))]
|
||||
pub name: String,
|
||||
|
||||
#[validate(length(min = 1, max = 500, message = "Description must be between 1 and 500 characters"))]
|
||||
pub description: String,
|
||||
|
||||
#[validate(length(min = 1, max = 50, message = "Rarity must be between 1 and 50 characters"))]
|
||||
pub rarity: String,
|
||||
|
||||
#[validate(length(min = 1, max = 50, message = "Type must be between 1 and 50 characters"))]
|
||||
pub type_: String,
|
||||
|
||||
#[validate(length(min = 1, max = 50, message = "Category must be between 1 and 50 characters"))]
|
||||
pub category: String,
|
||||
|
||||
#[validate(range(min = 0, message = "Value must be non-negative"))]
|
||||
pub value: i32,
|
||||
|
||||
#[validate(range(min = 0.0, message = "Weight must be non-negative"))]
|
||||
pub weight: f64,
|
||||
|
||||
#[validate(range(min = 0, message = "Stock must be non-negative"))]
|
||||
pub stock: i32,
|
||||
|
||||
pub is_limited: bool,
|
||||
|
||||
pub metadata: Option<Value>,
|
||||
|
||||
#[validate(length(min = 1, message = "Image URL must not be empty"))]
|
||||
#[validate(custom(
|
||||
function = "validate_image_url",
|
||||
@@ -57,7 +85,7 @@ pub struct GachaItemDto {
|
||||
impl GachaItemDto {
|
||||
pub fn from(dto: GachaItemSchema) -> Self {
|
||||
Self {
|
||||
id: dto.id.id.to_raw(),
|
||||
id: dto.id.to_string(),
|
||||
name: dto.name,
|
||||
is_deleted: dto.is_deleted,
|
||||
created_at: dto.created_at,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||
use crate::{AppState, MetaRequestDto, ResponseListSuccessDto, get_id, make_thing};
|
||||
use crate::v1::gacha_items::GachaItemDto;
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use crate::{AppState, MetaRequestDto, ResponseListSuccessDto};
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::QueryListBuilder;
|
||||
use imphnen_utils::get_iso_date;
|
||||
use serde_json::{Map, Value};
|
||||
// QueryListBuilder is not available in imphnen-iam, need to implement locally or use alternative
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
use sea_orm::{EntityTrait, ColumnTrait, QueryFilter, QueryOrder, PaginatorTrait, ActiveModelTrait, ActiveValue, QuerySelect};
|
||||
use imphnen_entities::seaorm::gacha::gacha_items::{Entity as GachaItemEntity, Column as GachaItemColumn, ActiveModel as GachaItemActiveModel};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct GachaItemRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -25,57 +24,124 @@ impl<'a> GachaItemRepository<'a> {
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> {
|
||||
let now = Instant::now();
|
||||
let surreal_query = format!(
|
||||
"SELECT * FROM {} WHERE is_deleted = false AND name LIKE ?",
|
||||
ResourceEnum::GachaItems
|
||||
);
|
||||
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
|
||||
QueryListBuilder::new(
|
||||
&self.state.surrealdb_ws,
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&meta,
|
||||
)
|
||||
.with_condition("is_deleted = false")
|
||||
.search_field("name")
|
||||
.select_fields(vec!["*"])
|
||||
.build()
|
||||
let db = &self.state.postgres_connection.conn;
|
||||
|
||||
let query = GachaItemEntity::find()
|
||||
.filter(GachaItemColumn::DeletedAt.is_null());
|
||||
|
||||
// Apply search if provided
|
||||
let query = if let Some(search) = &meta.search {
|
||||
query.filter(GachaItemColumn::Name.contains(search))
|
||||
} else {
|
||||
query
|
||||
};
|
||||
|
||||
// Apply sorting
|
||||
let query = if let Some(sort_by) = &meta.sort_by {
|
||||
match sort_by.as_str() {
|
||||
"name" => {
|
||||
if meta.order.as_deref() == Some("desc") {
|
||||
query.order_by_desc(GachaItemColumn::Name)
|
||||
} else {
|
||||
query.order_by_asc(GachaItemColumn::Name)
|
||||
}
|
||||
}
|
||||
"created_at" => {
|
||||
if meta.order.as_deref() == Some("desc") {
|
||||
query.order_by_desc(GachaItemColumn::CreatedAt)
|
||||
} else {
|
||||
query.order_by_asc(GachaItemColumn::CreatedAt)
|
||||
}
|
||||
}
|
||||
_ => query.order_by_desc(GachaItemColumn::CreatedAt),
|
||||
}
|
||||
} else {
|
||||
query.order_by_desc(GachaItemColumn::CreatedAt)
|
||||
};
|
||||
|
||||
// Get total count
|
||||
let total_count = query.clone().count(db).await?;
|
||||
|
||||
// Apply pagination
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let limit = meta.per_page.unwrap_or(10);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let items = query
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let data: Vec<GachaItemDto> = items
|
||||
.into_iter()
|
||||
.map(|item| GachaItemDto {
|
||||
id: item.id.to_string(),
|
||||
name: item.name,
|
||||
is_deleted: item.deleted_at.is_some(),
|
||||
created_at: Some(item.created_at.to_string()),
|
||||
updated_at: Some(item.updated_at.to_string()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let _total_pages = (total_count as f64 / limit as f64).ceil() as u32;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_gacha_item_list' took: {elapsed:.2?}");
|
||||
}
|
||||
let data = raw_result
|
||||
.data
|
||||
.into_iter()
|
||||
.map(GachaItemDto::from)
|
||||
.collect();
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data,
|
||||
meta: raw_result.meta,
|
||||
meta: Some(imphnen_libs::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(limit),
|
||||
total: Some(total_count),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_gacha_item_by_id(&self, id: String) -> Result<GachaItemSchema> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let surreal_query = format!("SELECT * FROM {} WHERE id = '{}'", ResourceEnum::GachaItems, id);
|
||||
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||
let result: Option<GachaItemSchema> = db
|
||||
.select((ResourceEnum::GachaItems.to_string(), id.clone()))
|
||||
let db = &self.state.postgres_connection.conn;
|
||||
|
||||
let uuid_id = Uuid::parse_str(&id)?;
|
||||
|
||||
let item = GachaItemEntity::find_by_id(uuid_id)
|
||||
.filter(GachaItemColumn::DeletedAt.is_null())
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_gacha_item_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
match result {
|
||||
Some(item) if !item.is_deleted => Ok(item),
|
||||
_ => bail!("Gacha Item not found"),
|
||||
|
||||
match item {
|
||||
Some(item) => Ok(GachaItemSchema {
|
||||
id: item.id.to_string(),
|
||||
item_code: item.item_code,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
rarity: item.rarity,
|
||||
type_: item.type_,
|
||||
category: item.category,
|
||||
value: item.value,
|
||||
weight: item.weight,
|
||||
stock: item.stock,
|
||||
is_limited: item.is_limited,
|
||||
metadata: item.metadata,
|
||||
image_url: "".to_string(), // Not present in DB model
|
||||
is_deleted: item.deleted_at.is_some(),
|
||||
created_at: Some(item.created_at.to_string()),
|
||||
updated_at: Some(item.updated_at.to_string()),
|
||||
}),
|
||||
None => bail!("Gacha Item not found"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,23 +151,36 @@ impl<'a> GachaItemRepository<'a> {
|
||||
data: GachaItemSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let surreal_query = format!("CREATE {} CONTENT ...", ResourceEnum::GachaItems);
|
||||
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||
let record: Option<GachaItemSchema> = db
|
||||
.create(ResourceEnum::GachaItems.to_string())
|
||||
.content(data)
|
||||
.await?;
|
||||
let db = &self.state.postgres_connection.conn;
|
||||
|
||||
let active_model = GachaItemActiveModel {
|
||||
id: ActiveValue::Set(Uuid::new_v4()),
|
||||
item_code: ActiveValue::Set(data.item_code),
|
||||
name: ActiveValue::Set(data.name),
|
||||
description: ActiveValue::Set(data.description),
|
||||
rarity: ActiveValue::Set(data.rarity),
|
||||
type_: ActiveValue::Set(data.type_),
|
||||
category: ActiveValue::Set(data.category),
|
||||
value: ActiveValue::Set(data.value),
|
||||
weight: ActiveValue::Set(data.weight),
|
||||
stock: ActiveValue::Set(data.stock),
|
||||
is_limited: ActiveValue::Set(data.is_limited),
|
||||
metadata: ActiveValue::Set(data.metadata),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
deleted_at: ActiveValue::NotSet,
|
||||
};
|
||||
|
||||
let result = active_model.insert(db).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_gacha_item' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Item".into()),
|
||||
None => bail!("Failed to create Gacha Item"),
|
||||
}
|
||||
|
||||
Ok(result.id.to_string())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
@@ -110,58 +189,70 @@ impl<'a> GachaItemRepository<'a> {
|
||||
data: GachaItemSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(&data.id)?;
|
||||
let existing = self.query_gacha_item_by_id(data.id.id.to_raw()).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Gacha Item already deleted");
|
||||
}
|
||||
let merged = GachaItemSchema {
|
||||
created_at: existing.created_at,
|
||||
..data.clone()
|
||||
};
|
||||
let surreal_query = format!("UPDATE {:?} MERGE ...", record_key);
|
||||
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||
let record: Option<GachaItemSchema> =
|
||||
db.update(record_key).merge(merged).await?;
|
||||
let db = &self.state.postgres_connection.conn;
|
||||
|
||||
let uuid_id = Uuid::parse_str(&data.id)?;
|
||||
|
||||
let mut active_model: GachaItemActiveModel = GachaItemEntity::find_by_id(uuid_id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Gacha Item not found"))?
|
||||
.into();
|
||||
|
||||
active_model.item_code = ActiveValue::Set(data.item_code);
|
||||
active_model.name = ActiveValue::Set(data.name);
|
||||
active_model.description = ActiveValue::Set(data.description);
|
||||
active_model.rarity = ActiveValue::Set(data.rarity);
|
||||
active_model.type_ = ActiveValue::Set(data.type_);
|
||||
active_model.category = ActiveValue::Set(data.category);
|
||||
active_model.value = ActiveValue::Set(data.value);
|
||||
active_model.weight = ActiveValue::Set(data.weight);
|
||||
active_model.stock = ActiveValue::Set(data.stock);
|
||||
active_model.is_limited = ActiveValue::Set(data.is_limited);
|
||||
active_model.metadata = ActiveValue::Set(data.metadata);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
let _result = 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_gacha_item' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success update Gacha Item".into()),
|
||||
None => bail!("Failed to update Gacha Item"),
|
||||
}
|
||||
|
||||
Ok("Success update Gacha Item".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_gacha_item(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let item_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id);
|
||||
let item = self.query_gacha_item_by_id(item_id.id.to_raw()).await?;
|
||||
if item.is_deleted {
|
||||
let db = &self.state.postgres_connection.conn;
|
||||
|
||||
let uuid_id = Uuid::parse_str(&id)?;
|
||||
|
||||
let mut active_model: GachaItemActiveModel = GachaItemEntity::find_by_id(uuid_id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Gacha Item not found"))?
|
||||
.into();
|
||||
|
||||
if active_model.deleted_at.is_set() {
|
||||
bail!("Gacha Item already deleted");
|
||||
}
|
||||
let record_key = get_id(&item.id)?;
|
||||
let mut patch = Map::new();
|
||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
|
||||
let surreal_query = format!("UPDATE {:?} MERGE ...", record_key);
|
||||
info!(query = %surreal_query, "Executing SurrealDB query");
|
||||
let record: Option<GachaItemSchema> = db.update(record_key).merge(patch).await?;
|
||||
active_model.deleted_at = ActiveValue::Set(Some(chrono::Utc::now()));
|
||||
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_gacha_item' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success soft delete Gacha Item".into()),
|
||||
None => bail!("Failed to soft delete Gacha Item"),
|
||||
}
|
||||
|
||||
Ok("Success soft delete Gacha Item".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
use crate::make_thing;
|
||||
use imphnen_iam::get_iso_date;
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
use uuid::Uuid;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::v1::gacha_items::gacha_items_dto::GachaItemRequestDto;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaItemSchema {
|
||||
pub id: Thing,
|
||||
pub id: String,
|
||||
pub item_code: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub rarity: String,
|
||||
pub type_: String,
|
||||
pub category: String,
|
||||
pub value: i32,
|
||||
pub weight: f64,
|
||||
pub stock: i32,
|
||||
pub is_limited: bool,
|
||||
pub metadata: Option<Value>,
|
||||
pub image_url: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
@@ -19,15 +27,22 @@ pub struct GachaItemSchema {
|
||||
impl Default for GachaItemSchema {
|
||||
fn default() -> Self {
|
||||
GachaItemSchema {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
id: Uuid::new_v4().to_string(),
|
||||
item_code: String::new(),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
rarity: String::new(),
|
||||
type_: String::new(),
|
||||
category: String::new(),
|
||||
value: 0,
|
||||
weight: 0.0,
|
||||
stock: 0,
|
||||
is_limited: false,
|
||||
metadata: None,
|
||||
image_url: String::new(),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,13 +50,22 @@ impl Default for GachaItemSchema {
|
||||
impl GachaItemSchema {
|
||||
pub fn from(dto: GachaItemRequestDto) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
id: Uuid::new_v4().to_string(),
|
||||
item_code: dto.item_code,
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
rarity: dto.rarity,
|
||||
type_: dto.type_,
|
||||
category: dto.category,
|
||||
value: dto.value,
|
||||
weight: dto.weight,
|
||||
stock: dto.stock,
|
||||
is_limited: dto.is_limited,
|
||||
metadata: dto.metadata,
|
||||
image_url: dto.image_url,
|
||||
..Default::default()
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::v1::gacha_items::GachaItemDto;
|
||||
use crate::v1::gacha_items::gacha_items_dto::{GachaItemRequestDto, GachaItemUpdateRequestDto};
|
||||
use crate::v1::gacha_items::gacha_items_repository::GachaItemRepository;
|
||||
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_entities::ResourceEnum;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Response;
|
||||
use imphnen_utils::get_iso_date;
|
||||
|
||||
@@ -3,9 +3,11 @@ use imphnen_entities::{MessageResponseDto, ResponseSuccessDto};
|
||||
use crate::v1::gacha_rolls::gacha_rolls_dto::{GachaRollItemDto, GachaRollRequestDto};
|
||||
use crate::v1::gacha_rolls::gacha_rolls_service::GachaRollService;
|
||||
use axum::{
|
||||
Extension, Json, extract::Path, http::HeaderMap, response::IntoResponse,
|
||||
Extension, Json, extract::Path, http::{HeaderMap, StatusCode}, response::IntoResponse,
|
||||
};
|
||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||
use imphnen_utils::common_response;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -31,7 +33,13 @@ pub async fn get_detail_gacha_roll(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_user, state)) => GachaRollService::get_gacha_roll_by_id(&state, id).await,
|
||||
Ok((_user, state)) => {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
GachaRollService::get_gacha_roll_by_id(&state, parsed_id).await
|
||||
},
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
@@ -54,13 +62,13 @@ pub async fn post_create_gacha_roll(
|
||||
Json(payload): Json<GachaRollRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
headers,
|
||||
Extension(state),
|
||||
headers.clone(),
|
||||
Extension(state.clone()),
|
||||
vec![PermissionsEnum::CreateGachaRolls],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_user, state)) => GachaRollService::create_gacha_roll(&state, payload).await,
|
||||
Ok((_user, _)) => GachaRollService::create_gacha_roll(headers, &state, payload, "default".to_string()).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
@@ -116,7 +124,13 @@ pub async fn delete_gacha_roll(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_user, state)) => GachaRollService::soft_delete_gacha_roll(&state, id).await,
|
||||
Ok((_user, state)) => {
|
||||
let parsed_id = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid UUID format: {}", e)),
|
||||
};
|
||||
GachaRollService::soft_delete_gacha_roll(&state, parsed_id).await
|
||||
},
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::v1::gacha_items::GachaItemDto;
|
||||
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
use crate::v1::gacha_items::GachaItemDto;
|
||||
use crate::v1::gacha_items::gacha_items_schema::GachaItemSchema;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct GachaRollRequestDto {
|
||||
@@ -31,7 +30,7 @@ pub struct GachaRollItemDto {
|
||||
impl GachaRollItemDto {
|
||||
pub fn from(dto: &GachaRollQueryDto) -> Self {
|
||||
Self {
|
||||
id: dto.id.id.to_raw(),
|
||||
id: dto.id.clone(),
|
||||
// Handle case where item might be missing
|
||||
item: match &dto.item {
|
||||
Some(item) => GachaItemDto::from(item.clone()),
|
||||
@@ -54,7 +53,7 @@ impl GachaRollItemDto {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaRollQueryDto {
|
||||
pub id: Thing,
|
||||
pub id: String,
|
||||
// item can be missing in the DB (during partial queries); make optional to allow graceful handling
|
||||
pub item: Option<GachaItemSchema>,
|
||||
pub weight: f32,
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
use crate::v1::gacha_rolls::gacha_rolls_dto::GachaRollQueryDto;
|
||||
use crate::v1::gacha_rolls::gacha_rolls_schema::GachaRollSchema;
|
||||
use crate::AppState;
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::DetailQueryBuilder;
|
||||
use crate::{get_id, make_thing};
|
||||
use imphnen_entities::seaorm::gacha::gacha_rolls::{Entity as GachaRollsEntity, ActiveModel as GachaRollActiveModel, Column as GachaRollColumn};
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
use chrono::Utc;
|
||||
use rand::prelude::*;
|
||||
|
||||
use imphnen_utils::get_iso_date;
|
||||
use serde_json::{Map, Value};
|
||||
use sea_orm::{EntityTrait, QueryFilter, Set, ColumnTrait, ActiveModelTrait};
|
||||
use imphnen_libs::postgres::AppStatePostgresExt;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct GachaRollRepository<'a> {
|
||||
state: &'a AppState,
|
||||
@@ -26,28 +23,35 @@ impl<'a> GachaRollRepository<'a> {
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_gacha_roll_by_id(
|
||||
&self,
|
||||
id: String,
|
||||
id: Uuid,
|
||||
) -> Result<GachaRollQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let builder = DetailQueryBuilder::new(ResourceEnum::GachaRolls.to_string())
|
||||
.with_id(id.clone())
|
||||
.with_condition("is_deleted = false")
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("item");
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query");
|
||||
let result: Option<GachaRollQueryDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let db = self.state.postgres_db();
|
||||
|
||||
let result = GachaRollsEntity::find()
|
||||
.filter(GachaRollColumn::Id.eq(id))
|
||||
.filter(GachaRollColumn::IsDeleted.eq(false))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_gacha_roll_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
match result {
|
||||
Some(roll) if !roll.is_deleted => Ok(roll),
|
||||
_ => bail!("Gacha Roll not found"),
|
||||
Some(r) => Ok(GachaRollQueryDto {
|
||||
id: r.id.to_string(),
|
||||
item: None,
|
||||
weight: r.weight,
|
||||
quantity: r.quantity,
|
||||
is_deleted: r.is_deleted,
|
||||
created_at: r.created_at.map(|d| d.to_string()),
|
||||
updated_at: r.updated_at.map(|d| d.to_string()),
|
||||
}),
|
||||
None => bail!("Gacha Roll not found"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,43 +61,43 @@ impl<'a> GachaRollRepository<'a> {
|
||||
data: GachaRollSchema,
|
||||
) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
info!(query = "CREATE", "Executing SurrealDB create operation for GachaRolls");
|
||||
let record: Option<GachaRollSchema> = db
|
||||
.create(ResourceEnum::GachaRolls.to_string())
|
||||
.content(data)
|
||||
let db = self.state.postgres_db();
|
||||
|
||||
let active_model = GachaRollActiveModel {
|
||||
id: Set(Uuid::parse_str(&data.id)?),
|
||||
user_id: Set(Uuid::parse_str(&data.user_id)?),
|
||||
gacha_id: Set(data.gacha_id), // gacha_id is String
|
||||
item_id: Set(Uuid::parse_str(&data.item_id)?),
|
||||
quantity: Set(data.quantity),
|
||||
weight: Set(data.weight),
|
||||
is_deleted: Set(false),
|
||||
created_at: Set(Some(Utc::now().naive_utc())),
|
||||
updated_at: Set(Some(Utc::now().naive_utc())),
|
||||
};
|
||||
|
||||
let _ = GachaRollsEntity::insert(active_model)
|
||||
.exec(db)
|
||||
.await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_gacha_roll' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success create Gacha Roll".into()),
|
||||
None => bail!("Failed to create Gacha Roll"),
|
||||
}
|
||||
|
||||
Ok("Success create Gacha Roll".into())
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err)]
|
||||
pub async fn query_all_active_rolls(&self) -> Result<Vec<GachaRollQueryDto>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let table_name = ResourceEnum::GachaRolls.to_string();
|
||||
|
||||
// Use DetailQueryBuilder to properly fetch related item data
|
||||
let builder = DetailQueryBuilder::new(table_name)
|
||||
.with_condition("is_deleted = false AND quantity > 0")
|
||||
.with_select_fields(vec!["*"])
|
||||
.with_fetch("item");
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query for active rolls");
|
||||
|
||||
let mut result = builder.apply_bindings(db.query(sql)).await?;
|
||||
let results = match result.take(0) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Ok(Vec::new()),
|
||||
};
|
||||
let db = self.state.postgres_db();
|
||||
let results = GachaRollsEntity::find()
|
||||
.filter(GachaRollColumn::IsDeleted.eq(false))
|
||||
.filter(GachaRollColumn::Quantity.gt(0))
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
@@ -101,7 +105,16 @@ impl<'a> GachaRollRepository<'a> {
|
||||
{
|
||||
println!("Query 'query_all_active_rolls' took: {elapsed:.2?}");
|
||||
}
|
||||
Ok(results)
|
||||
|
||||
Ok(results.into_iter().map(|r| GachaRollQueryDto {
|
||||
id: r.id.to_string(),
|
||||
item: None,
|
||||
weight: r.weight,
|
||||
quantity: r.quantity,
|
||||
is_deleted: r.is_deleted,
|
||||
created_at: r.created_at.map(|d| d.to_string()),
|
||||
updated_at: r.updated_at.map(|d| d.to_string()),
|
||||
}).collect())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
@@ -117,8 +130,8 @@ impl<'a> GachaRollRepository<'a> {
|
||||
}
|
||||
|
||||
// Simple random selection based on quantity weights
|
||||
let total_weight: f32 = filtered.iter()
|
||||
.map(|r| r.weight * r.quantity as f32)
|
||||
let total_weight: f64 = filtered.iter()
|
||||
.map(|r| f64::from(r.weight) * f64::from(r.quantity))
|
||||
.sum();
|
||||
|
||||
if total_weight <= 0.0 {
|
||||
@@ -134,7 +147,7 @@ impl<'a> GachaRollRepository<'a> {
|
||||
|
||||
let mut cumulative_weight = 0.0;
|
||||
for roll in &filtered {
|
||||
cumulative_weight += roll.weight * roll.quantity as f32;
|
||||
cumulative_weight += f64::from(roll.weight) * f64::from(roll.quantity);
|
||||
if random_value <= cumulative_weight {
|
||||
return Some(roll.clone());
|
||||
}
|
||||
@@ -145,31 +158,33 @@ impl<'a> GachaRollRepository<'a> {
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_soft_delete_gacha_roll(&self, id: String) -> Result<String> {
|
||||
pub async fn query_soft_delete_gacha_roll(&self, id: Uuid) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let roll_id_thing = make_thing(&ResourceEnum::GachaRolls.to_string(), &id);
|
||||
let roll = self.query_gacha_roll_by_id(id.clone()).await?;
|
||||
let db = self.state.postgres_db();
|
||||
|
||||
let roll = self.query_gacha_roll_by_id(id).await?;
|
||||
if roll.is_deleted {
|
||||
bail!("Gacha Roll already deleted");
|
||||
}
|
||||
let record_key = get_id(&roll_id_thing)?;
|
||||
|
||||
let mut patch = Map::new();
|
||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
let mut active_model: GachaRollActiveModel = GachaRollsEntity::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Gacha Roll not found"))?
|
||||
.into();
|
||||
|
||||
active_model.is_deleted = Set(true);
|
||||
active_model.updated_at = Set(Some(Utc::now().naive_utc()));
|
||||
|
||||
let _result = GachaRollActiveModel::update(active_model, db).await?;
|
||||
|
||||
info!(query = "UPDATE", record_key = ?record_key, "Executing SurrealDB update operation for GachaRolls");
|
||||
let record: Option<GachaRollSchema> = db.update(record_key).merge(patch).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_soft_delete_gacha_roll' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success soft delete Gacha Roll".into()),
|
||||
None => bail!("Failed to soft delete Gacha Roll"),
|
||||
}
|
||||
|
||||
Ok("Success soft delete Gacha Roll".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,45 @@
|
||||
use crate::make_thing;
|
||||
use imphnen_iam::get_iso_date;
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::v1::gacha_rolls::gacha_rolls_dto::GachaRollRequestDto;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GachaRollSchema {
|
||||
pub id: Thing,
|
||||
pub item: Thing,
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub gacha_id: String,
|
||||
pub item_id: String,
|
||||
pub weight: f32,
|
||||
pub quantity: i32,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl Default for GachaRollSchema {
|
||||
fn default() -> Self {
|
||||
GachaRollSchema {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaRolls.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
item: make_thing(
|
||||
&ResourceEnum::GachaItems.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
id: Uuid::new_v4().to_string(),
|
||||
user_id: "".to_string(),
|
||||
gacha_id: "".to_string(),
|
||||
item_id: "".to_string(),
|
||||
weight: 0.0,
|
||||
quantity: 0,
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: Some(get_iso_date()),
|
||||
created_at: Some(Utc::now()),
|
||||
updated_at: Some(Utc::now()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GachaRollSchema {
|
||||
pub fn create(dto: GachaRollRequestDto) -> Self {
|
||||
pub fn create(dto: GachaRollRequestDto, user_id: String, gacha_id: String) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
&ResourceEnum::GachaRolls.to_string(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
item: make_thing(&ResourceEnum::GachaItems.to_string(), &dto.item_id),
|
||||
id: Uuid::new_v4().to_string(),
|
||||
user_id,
|
||||
gacha_id,
|
||||
item_id: dto.item_id,
|
||||
weight: dto.weight,
|
||||
quantity: dto.quantity,
|
||||
..Default::default()
|
||||
|
||||
@@ -11,11 +11,12 @@ use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::Response;
|
||||
use imphnen_iam::UsersRepository;
|
||||
use imphnen_utils::extract_email;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct GachaRollService;
|
||||
|
||||
impl GachaRollService {
|
||||
pub async fn get_gacha_roll_by_id(state: &AppState, id: String) -> Response {
|
||||
pub async fn get_gacha_roll_by_id(state: &AppState, id: Uuid) -> Response {
|
||||
let repo = GachaRollRepository::new(state);
|
||||
match repo.query_gacha_roll_by_id(id).await {
|
||||
Ok(roll) => success_response(ResponseSuccessDto {
|
||||
@@ -26,13 +27,23 @@ impl GachaRollService {
|
||||
}
|
||||
|
||||
pub async fn create_gacha_roll(
|
||||
headers: HeaderMap, // Add headers
|
||||
state: &AppState,
|
||||
payload: GachaRollRequestDto,
|
||||
gacha_id: String, // Add gacha_id
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&payload) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let schema = GachaRollSchema::create(payload);
|
||||
let repo_user = UsersRepository::new(state); // Need UsersRepository here
|
||||
let Some(email) = extract_email(&headers) else {
|
||||
return common_response(StatusCode::UNAUTHORIZED, "Unauthorized");
|
||||
};
|
||||
let Ok(user) = repo_user.query_user_by_email(email.to_string()).await else {
|
||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||
};
|
||||
|
||||
let schema = GachaRollSchema::create(payload, user.id.clone(), gacha_id); // Pass user.id and gacha_id
|
||||
let repo = GachaRollRepository::new(state);
|
||||
match repo.query_create_gacha_roll(schema).await {
|
||||
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||
@@ -52,8 +63,13 @@ impl GachaRollService {
|
||||
return common_response(StatusCode::NOT_FOUND, "User not found");
|
||||
};
|
||||
|
||||
let parsed_user_id = match Uuid::parse_str(&user.id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid User ID format: {}", e)),
|
||||
};
|
||||
|
||||
// Check if user has enough credits
|
||||
let credit_opt = repo_credits.query_by_user_id(user.id.id.to_raw()).await;
|
||||
let credit_opt = repo_credits.query_by_user_id(parsed_user_id).await;
|
||||
let has_enough_credits = match credit_opt {
|
||||
Ok(Some(credit)) => credit.available_rolls > 0,
|
||||
Ok(None) => false, // No credit record means no credits
|
||||
@@ -67,10 +83,7 @@ impl GachaRollService {
|
||||
}
|
||||
|
||||
// Consume one credit
|
||||
match repo_credits.query_consume_credit(user.id.id.to_raw()).await {
|
||||
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
_ => {}
|
||||
}
|
||||
if let Err(e) = repo_credits.query_consume_credit(parsed_user_id).await { return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()) }
|
||||
|
||||
// Proceed with the roll
|
||||
match repo.query_all_active_rolls().await {
|
||||
@@ -84,7 +97,7 @@ impl GachaRollService {
|
||||
}),
|
||||
Err(e) => {
|
||||
// Refund the credit if claim creation fails
|
||||
let user_id = user.id.id.to_raw(); // Extract value before potential move
|
||||
let user_id = user.id.clone(); // Extract value before potential move
|
||||
let _ = repo_credits.query_add_credit(crate::v1::gacha_credits::gacha_credits_dto::GachaCreditRequestDto {
|
||||
user_id,
|
||||
amount: 1,
|
||||
@@ -99,7 +112,7 @@ impl GachaRollService {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn soft_delete_gacha_roll(state: &AppState, id: String) -> Response {
|
||||
pub async fn soft_delete_gacha_roll(state: &AppState, id: Uuid) -> Response {
|
||||
let repo = GachaRollRepository::new(state);
|
||||
match repo.query_soft_delete_gacha_roll(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
|
||||
@@ -12,7 +12,6 @@ imphnen-entities.workspace = true
|
||||
imphnen-middleware.workspace = true
|
||||
imphnen-cms.workspace = true
|
||||
imphnen-dimentorin.workspace = true
|
||||
imphnen-hackathon.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -21,7 +20,6 @@ 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
|
||||
|
||||
+1
-131
@@ -26,29 +26,6 @@ use imphnen_gacha::v1::gacha_items::{gacha_items_controller, GachaItemDto};
|
||||
use imphnen_gacha::v1::gacha_items::gacha_items_dto::GachaItemRequestDto;
|
||||
use imphnen_gacha::v1::gacha_rolls::{gacha_rolls_controller, GachaRollItemDto};
|
||||
use imphnen_gacha::v1::gacha_rolls::gacha_rolls_dto::GachaRollRequestDto;
|
||||
use imphnen_hackathon::v1::hackathon::{
|
||||
hackathon_controller,
|
||||
hackathon_dto::{
|
||||
HackathonCreateRequestDto, HackathonDto, HackathonEventCreateRequestDto, HackathonEventDto,
|
||||
HackathonEventUpdateRequestDto, HackathonSubmissionCreateRequestDto,
|
||||
HackathonSubmissionDto, HackathonSubmissionUpdateRequestDto, HackathonTimelineCreateRequestDto,
|
||||
HackathonTimelineDto, HackathonTimelineUpdateRequestDto, HackathonUpdateRequestDto,
|
||||
},
|
||||
};
|
||||
use imphnen_hackathon::v1::registrations::{
|
||||
registration_controller,
|
||||
RegistrationRequestDto, RegistrationResponseDto, RegistrationListResponseDto,
|
||||
RegistrationListItemDto, UpdateRegistrationStatusRequestDto, UpdateRegistrationStatusResponseDto,
|
||||
CheckInResponseDto, RegistrationStatsDto, UserHackathonsResponseDto, UserHackathonDto,
|
||||
RegistrationStatus, ParticipantRole,
|
||||
};
|
||||
use imphnen_hackathon::v1::notifications::{
|
||||
notification_controller,
|
||||
notification_dto::{
|
||||
NotificationDto, NotificationListResponseDto, MarkAsReadResponseDto,
|
||||
MarkAllAsReadResponseDto, DeleteNotificationResponseDto, UnreadCountResponseDto,
|
||||
},
|
||||
};
|
||||
use imphnen_entities::{PermissionsItemDto, RolesDetailItemDto};
|
||||
use imphnen_entities::{MessageResponseDto, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto, ResponseSuccessDto};
|
||||
use imphnen_iam::v1::auth::auth_dto::{AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, TokenDto};
|
||||
@@ -57,8 +34,7 @@ use imphnen_iam::v1::roles::RolesListItemDto;
|
||||
use imphnen_iam::v1::roles::roles_dto::{RolesRequestCreateDto, RolesRequestUpdateDto};
|
||||
use imphnen_iam::v1::users::UsersDetailItemDto;
|
||||
use imphnen_iam::v1::users::users_dto::{UsersCreateRequestDto, UsersListItemDto, UsersUpdateRequestDto};
|
||||
use imphnen_iam::v1::teams::teams_dto::{TeamsCreateRequestDto, TeamsUpdateRequestDto, TeamInviteRequestDto, TeamAcceptInvitationRequestDto, TeamsDetailItemDto, TeamsListItemDto, TeamMemberDto, TeamInvitationDto, TeamsSearchQueryDto};
|
||||
use imphnen_iam::v1::{auth, permissions, roles, users, teams};
|
||||
use imphnen_iam::v1::{auth, permissions, roles, users};
|
||||
use imphnen_iam::v1::users::users_controller::FileUploadSchema;
|
||||
use utoipa::{
|
||||
Modify, OpenApi,
|
||||
@@ -95,16 +71,6 @@ use utoipa::{
|
||||
permissions::permissions_controller::post_create_permission,
|
||||
permissions::permissions_controller::put_update_permission,
|
||||
permissions::permissions_controller::delete_permission,
|
||||
teams::teams_controller::get_team_list,
|
||||
teams::teams_controller::get_team_by_id,
|
||||
teams::teams_controller::post_create_team,
|
||||
teams::teams_controller::put_update_team,
|
||||
teams::teams_controller::delete_team,
|
||||
teams::teams_controller::post_invite_team_members,
|
||||
teams::teams_controller::post_accept_invitation,
|
||||
teams::teams_controller::get_public_team_search,
|
||||
teams::teams_controller::get_team_members,
|
||||
teams::teams_controller::post_leave_team,
|
||||
gacha_claims_controller::get_detail_gacha_claim,
|
||||
gacha_claims_controller::post_create_gacha_claim,
|
||||
gacha_items_controller::get_gacha_item_list,
|
||||
@@ -140,35 +106,6 @@ use utoipa::{
|
||||
sessions_controller::put_update_session_status,
|
||||
sessions_controller::post_submit_feedback,
|
||||
sessions_controller::get_my_sessions,
|
||||
hackathon_controller::create_hackathon,
|
||||
hackathon_controller::get_hackathon,
|
||||
hackathon_controller::list_hackathons,
|
||||
hackathon_controller::update_hackathon,
|
||||
hackathon_controller::delete_hackathon,
|
||||
hackathon_controller::create_hackathon_event,
|
||||
hackathon_controller::list_hackathon_events,
|
||||
hackathon_controller::update_hackathon_event,
|
||||
hackathon_controller::delete_hackathon_event,
|
||||
hackathon_controller::create_hackathon_timeline,
|
||||
hackathon_controller::list_hackathon_timeline,
|
||||
hackathon_controller::update_hackathon_timeline,
|
||||
hackathon_controller::delete_hackathon_timeline,
|
||||
hackathon_controller::create_hackathon_submission,
|
||||
hackathon_controller::list_hackathon_submissions,
|
||||
hackathon_controller::update_hackathon_submission,
|
||||
hackathon_controller::submit_hackathon_submission,
|
||||
hackathon_controller::delete_hackathon_submission,
|
||||
registration_controller::post_register_hackathon,
|
||||
registration_controller::get_hackathon_registrations,
|
||||
registration_controller::get_my_hackathons,
|
||||
registration_controller::put_update_registration_status,
|
||||
registration_controller::post_check_in_participant,
|
||||
registration_controller::get_registration_stats,
|
||||
notification_controller::get_notifications_handler,
|
||||
notification_controller::mark_as_read_handler,
|
||||
notification_controller::mark_all_as_read_handler,
|
||||
notification_controller::delete_notification_handler,
|
||||
notification_controller::get_unread_count_handler,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
@@ -244,66 +181,6 @@ use utoipa::{
|
||||
ResponseSuccessDto<MentorAvailabilityDto>,
|
||||
ResponseSuccessDto<UpdateSessionStatusResponseDto>,
|
||||
ResponseSuccessDto<SessionFeedbackResponseDto>,
|
||||
TeamsCreateRequestDto,
|
||||
TeamsUpdateRequestDto,
|
||||
TeamInviteRequestDto,
|
||||
TeamAcceptInvitationRequestDto,
|
||||
TeamsDetailItemDto,
|
||||
TeamsListItemDto,
|
||||
TeamMemberDto,
|
||||
TeamInvitationDto,
|
||||
TeamsSearchQueryDto,
|
||||
ResponseListSuccessDto<Vec<TeamsListItemDto>>,
|
||||
ResponseSuccessDto<TeamsDetailItemDto>,
|
||||
HackathonCreateRequestDto,
|
||||
HackathonDto,
|
||||
HackathonEventCreateRequestDto,
|
||||
HackathonEventDto,
|
||||
HackathonEventUpdateRequestDto,
|
||||
HackathonSubmissionCreateRequestDto,
|
||||
HackathonSubmissionDto,
|
||||
HackathonSubmissionUpdateRequestDto,
|
||||
HackathonTimelineCreateRequestDto,
|
||||
HackathonTimelineDto,
|
||||
HackathonTimelineUpdateRequestDto,
|
||||
HackathonUpdateRequestDto,
|
||||
RegistrationRequestDto,
|
||||
RegistrationResponseDto,
|
||||
RegistrationListResponseDto,
|
||||
RegistrationListItemDto,
|
||||
UpdateRegistrationStatusRequestDto,
|
||||
UpdateRegistrationStatusResponseDto,
|
||||
CheckInResponseDto,
|
||||
RegistrationStatsDto,
|
||||
UserHackathonsResponseDto,
|
||||
UserHackathonDto,
|
||||
RegistrationStatus,
|
||||
ParticipantRole,
|
||||
NotificationDto,
|
||||
NotificationListResponseDto,
|
||||
MarkAsReadResponseDto,
|
||||
MarkAllAsReadResponseDto,
|
||||
DeleteNotificationResponseDto,
|
||||
UnreadCountResponseDto,
|
||||
ResponseSuccessDto<RegistrationResponseDto>,
|
||||
ResponseSuccessDto<RegistrationListResponseDto>,
|
||||
ResponseSuccessDto<UpdateRegistrationStatusResponseDto>,
|
||||
ResponseSuccessDto<CheckInResponseDto>,
|
||||
ResponseSuccessDto<RegistrationStatsDto>,
|
||||
ResponseSuccessDto<UserHackathonsResponseDto>,
|
||||
ResponseSuccessDto<NotificationListResponseDto>,
|
||||
ResponseSuccessDto<MarkAsReadResponseDto>,
|
||||
ResponseSuccessDto<MarkAllAsReadResponseDto>,
|
||||
ResponseSuccessDto<DeleteNotificationResponseDto>,
|
||||
ResponseSuccessDto<UnreadCountResponseDto>,
|
||||
ResponseListSuccessDto<Vec<HackathonDto>>,
|
||||
ResponseSuccessDto<HackathonDto>,
|
||||
ResponseListSuccessDto<Vec<HackathonEventDto>>,
|
||||
ResponseSuccessDto<HackathonEventDto>,
|
||||
ResponseListSuccessDto<Vec<HackathonSubmissionDto>>,
|
||||
ResponseSuccessDto<HackathonSubmissionDto>,
|
||||
ResponseListSuccessDto<Vec<HackathonTimelineDto>>,
|
||||
ResponseSuccessDto<HackathonTimelineDto>,
|
||||
)
|
||||
),
|
||||
info(
|
||||
@@ -331,13 +208,6 @@ use utoipa::{
|
||||
(name = "Mentors - Admin", description = "Mentor Admin Management Endpoints (Admin Access Required)"),
|
||||
(name = "sessions", description = "Mentoring Sessions Management API"),
|
||||
(name = "Gacha", description = "Gacha System Endpoints"),
|
||||
(name = "Hackathons", description = "Hackathon Management Endpoints"),
|
||||
(name = "Hackathon Events", description = "Hackathon Event Management Endpoints"),
|
||||
(name = "Hackathon Timeline", description = "Hackathon Timeline Management Endpoints"),
|
||||
(name = "Hackathon Submissions", description = "Hackathon Submission Management Endpoints"),
|
||||
(name = "registrations", description = "Hackathon Registration Management API"),
|
||||
(name = "notifications", description = "User Notifications Management API"),
|
||||
(name = "Teams", description = "Team Management Endpoints"),
|
||||
)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
@@ -13,14 +13,13 @@ use imphnen_cms::{
|
||||
};
|
||||
use imphnen_dimentorin::dimentorin_router;
|
||||
use imphnen_gacha::gacha_router;
|
||||
use imphnen_hackathon::v1::{hackathon_protected_routes, hackathon_public_routes};
|
||||
use imphnen_iam::{
|
||||
iam_protected_routes,
|
||||
iam_public_routes,
|
||||
v1::users::users_service::UsersService,
|
||||
v1::auth::auth_repository::AuthRepoImpl,
|
||||
};
|
||||
use imphnen_libs::{AppState, SurrealMemClient, SurrealWsClient};
|
||||
use imphnen_libs::PostgresUserLookupService;
|
||||
use imphnen_libs::{AppState, axum::PostgresClients};
|
||||
use imphnen_middleware::{auth_middleware, cors_middleware, rate_limiting_middleware, security_headers_middleware};
|
||||
use std::sync::Arc;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
@@ -29,19 +28,16 @@ pub mod docs;
|
||||
pub use docs::{ApiDoc, SecurityAddon, docs_router};
|
||||
|
||||
pub async fn gateway_service(
|
||||
surrealdb_ws: SurrealWsClient,
|
||||
surrealdb_mem: SurrealMemClient,
|
||||
postgres_clients: PostgresClients,
|
||||
) -> Router {
|
||||
let state = AppState {
|
||||
surrealdb_ws,
|
||||
surrealdb_mem: surrealdb_mem.clone(),
|
||||
user_lookup_service: Arc::new(UsersService),
|
||||
auth_repository: Arc::new(AuthRepoImpl { db: surrealdb_mem }),
|
||||
postgres_connection: postgres_clients.main.clone(),
|
||||
user_lookup_service: Arc::new(PostgresUserLookupService::new()),
|
||||
auth_repository: Arc::new(AuthRepoImpl::new()),
|
||||
};
|
||||
|
||||
let public_routes = Router::new()
|
||||
.merge(iam_public_routes().layer(from_fn(rate_limiting_middleware)))
|
||||
.merge(hackathon_public_routes())
|
||||
.merge(testimonials_public_routes())
|
||||
.merge(events_public_routes());
|
||||
|
||||
@@ -50,7 +46,6 @@ pub async fn gateway_service(
|
||||
.merge(events_protected_routes())
|
||||
.merge(testimonials_protected_routes())
|
||||
.merge(dimentorin_router())
|
||||
.merge(hackathon_protected_routes())
|
||||
.nest("/gacha", gacha_router())
|
||||
.layer(from_fn(auth_middleware));
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
[package]
|
||||
name = "imphnen-hackathon"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
async-trait.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json = { workspace = true }
|
||||
oauth2 = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
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
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
axum-extra.workspace = true
|
||||
tower.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dotenvy.workspace = true
|
||||
tokio-test = { workspace = true }
|
||||
mockall = { workspace = true }
|
||||
http-body-util.workspace = true
|
||||
@@ -1,21 +0,0 @@
|
||||
pub mod v1;
|
||||
|
||||
// Re-export core entity types used across the hackathon system
|
||||
pub use imphnen_entities::{
|
||||
CountResult,
|
||||
Error,
|
||||
ErrorDto,
|
||||
MessageResponseDto,
|
||||
MetaRequestDto,
|
||||
MetaResponseDto,
|
||||
ResponseListSuccessDto,
|
||||
ResponseSuccessDto,
|
||||
};
|
||||
|
||||
// Explicitly import only what we need from libs and utils to avoid pollution
|
||||
pub use imphnen_libs::{
|
||||
AppState,
|
||||
};
|
||||
|
||||
// Re-export public v1 API
|
||||
pub use v1::hackathon::hackathon_controller::hackathon_routes;
|
||||
@@ -1,277 +0,0 @@
|
||||
use super::hackathon_dto::{
|
||||
HackathonCreateRequestDto, HackathonDto, HackathonEventCreateRequestDto,
|
||||
HackathonTimelineCreateRequestDto,
|
||||
};
|
||||
use super::hackathon_repository::HackathonRepository;
|
||||
use super::hackathon_audit_schema::{AuditAction, HackathonAuditLogSchema};
|
||||
use super::hackathon_audit_repository::HackathonAuditRepository;
|
||||
use super::hackathon_validation::{validate_timeline_phases, validate_dates, validate_organizers, validate_prizes, MAX_EVENTS_PER_HACKATHON};
|
||||
use crate::{AppState, ResponseSuccessDto, ErrorDto};
|
||||
use axum::http::StatusCode;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
/// Request DTO for atomic hackathon creation with timeline and events
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct HackathonCompleteSetupRequestDto {
|
||||
#[validate(nested)]
|
||||
pub hackathon: HackathonCreateRequestDto,
|
||||
|
||||
#[validate(length(min = 1, message = "At least one timeline phase is required"))]
|
||||
pub timelines: Vec<HackathonTimelineCreateRequestDto>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub events: Option<Vec<HackathonEventCreateRequestDto>>,
|
||||
|
||||
/// Actor ID for audit logging
|
||||
pub actor_id: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub actor_email: Option<String>,
|
||||
}
|
||||
|
||||
/// Response DTO for complete hackathon setup
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct HackathonCompleteSetupResponseDto {
|
||||
pub hackathon: HackathonDto,
|
||||
pub timelines: Vec<super::hackathon_dto::HackathonTimelineDto>,
|
||||
pub events: Option<Vec<super::hackathon_dto::HackathonEventDto>>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Service for atomic hackathon operations
|
||||
pub struct HackathonAtomicService;
|
||||
|
||||
impl HackathonAtomicService {
|
||||
/// Create hackathon with timeline and events atomically
|
||||
/// This ensures all-or-nothing creation - if any step fails, nothing is created
|
||||
pub async fn create_hackathon_complete(
|
||||
payload: HackathonCompleteSetupRequestDto,
|
||||
state: &AppState,
|
||||
) -> Result<ResponseSuccessDto<HackathonCompleteSetupResponseDto>, ErrorDto> {
|
||||
// 1. Validate all inputs before any database operations
|
||||
if let Err((_, error_message)) = imphnen_utils::validator::validate_request(&payload) {
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||
message: "Validation failed".to_string(),
|
||||
details: Some(serde_json::json!({ "validation_errors": error_message })),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Validate dates
|
||||
if let Err(e) = validate_dates(
|
||||
&payload.hackathon.start_date,
|
||||
&payload.hackathon.end_date,
|
||||
&payload.hackathon.registration_deadline,
|
||||
) {
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||
message: e.to_string(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Validate organizers
|
||||
if let Err(e) = validate_organizers(&payload.hackathon.organizers) {
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||
message: e.to_string(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Validate prizes if provided
|
||||
if let Some(ref prizes) = payload.hackathon.prizes {
|
||||
let prize_schemas: Vec<super::hackathon_schema::Prize> = prizes
|
||||
.iter()
|
||||
.map(|p| super::hackathon_schema::Prize {
|
||||
position: p.position,
|
||||
title: p.title.clone(),
|
||||
description: p.description.clone(),
|
||||
value: p.value.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Err(e) = validate_prizes(&prize_schemas) {
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||
message: e.to_string(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Validate events count if provided
|
||||
if let Some(ref events) = payload.events {
|
||||
if events.len() > MAX_EVENTS_PER_HACKATHON {
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||
message: format!(
|
||||
"Maximum {} events allowed per hackathon",
|
||||
MAX_EVENTS_PER_HACKATHON
|
||||
),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let repo = HackathonRepository::new(state);
|
||||
let audit_repo = HackathonAuditRepository::new(state);
|
||||
|
||||
// 6. Create hackathon first
|
||||
let hackathon = match repo.create_hackathon(payload.hackathon.clone()).await {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create hackathon: {}", e);
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
message: "Failed to create hackathon".to_string(),
|
||||
details: Some(serde_json::json!({ "error": e.to_string() })),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let hackathon_id = hackathon.id.id.to_string();
|
||||
|
||||
// 7. Create timelines - if this fails, we should ideally rollback hackathon
|
||||
let mut created_timelines = Vec::new();
|
||||
for timeline_dto in &payload.timelines {
|
||||
match repo.create_hackathon_timeline(hackathon_id.clone(), timeline_dto.clone()).await {
|
||||
Ok(timeline) => created_timelines.push(timeline),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create timeline, attempting cleanup: {}", e);
|
||||
// Attempt to delete hackathon and created timelines
|
||||
let _ = Self::cleanup_failed_creation(
|
||||
&hackathon_id,
|
||||
&created_timelines.iter().map(|t| t.id.id.to_string()).collect::<Vec<_>>(),
|
||||
&[],
|
||||
&repo,
|
||||
)
|
||||
.await;
|
||||
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
message: "Failed to create timeline, changes rolled back".to_string(),
|
||||
details: Some(serde_json::json!({ "error": e.to_string() })),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Validate timeline phases after all are created
|
||||
if let Err(e) = validate_timeline_phases(&hackathon, &created_timelines) {
|
||||
tracing::error!("Timeline validation failed, attempting cleanup: {}", e);
|
||||
let _ = Self::cleanup_failed_creation(
|
||||
&hackathon_id,
|
||||
&created_timelines.iter().map(|t| t.id.id.to_string()).collect::<Vec<_>>(),
|
||||
&[],
|
||||
&repo,
|
||||
)
|
||||
.await;
|
||||
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||
message: format!("Timeline validation failed: {}", e),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
|
||||
// 9. Create events if provided
|
||||
let mut created_events = Vec::new();
|
||||
if let Some(ref events) = payload.events {
|
||||
for event_dto in events {
|
||||
match repo.create_hackathon_event(hackathon_id.clone(), event_dto.clone()).await {
|
||||
Ok(event) => created_events.push(event),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create event, attempting cleanup: {}", e);
|
||||
let _ = Self::cleanup_failed_creation(
|
||||
&hackathon_id,
|
||||
&created_timelines.iter().map(|t| t.id.id.to_string()).collect::<Vec<_>>(),
|
||||
&created_events.iter().map(|e| e.id.id.to_string()).collect::<Vec<_>>(),
|
||||
&repo,
|
||||
)
|
||||
.await;
|
||||
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
message: "Failed to create event, changes rolled back".to_string(),
|
||||
details: Some(serde_json::json!({ "error": e.to_string() })),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Log audit trail
|
||||
let audit_log = HackathonAuditLogSchema::new(
|
||||
Some(hackathon.id.clone()),
|
||||
AuditAction::HackathonCreated,
|
||||
payload.actor_id.clone(),
|
||||
"hackathon".to_string(),
|
||||
Some(hackathon_id.clone()),
|
||||
)
|
||||
.with_changes(serde_json::to_value(&payload).unwrap_or_default())
|
||||
.with_request_info(None, None, payload.actor_email.clone());
|
||||
|
||||
if let Err(e) = audit_repo.log(audit_log).await {
|
||||
tracing::error!("Failed to create audit log: {}", e);
|
||||
// Don't fail the request if audit logging fails
|
||||
}
|
||||
|
||||
// 11. Return success response
|
||||
let response = HackathonCompleteSetupResponseDto {
|
||||
hackathon: super::hackathon_dto::HackathonDto::from(hackathon),
|
||||
timelines: created_timelines
|
||||
.into_iter()
|
||||
.map(super::hackathon_dto::HackathonTimelineDto::from)
|
||||
.collect(),
|
||||
events: if created_events.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
created_events
|
||||
.into_iter()
|
||||
.map(super::hackathon_dto::HackathonEventDto::from)
|
||||
.collect(),
|
||||
)
|
||||
},
|
||||
message: "Hackathon created successfully with timeline and events".to_string(),
|
||||
};
|
||||
|
||||
Ok(ResponseSuccessDto { data: response })
|
||||
}
|
||||
|
||||
/// Cleanup failed creation by deleting created resources
|
||||
async fn cleanup_failed_creation(
|
||||
hackathon_id: &str,
|
||||
timeline_ids: &[String],
|
||||
event_ids: &[String],
|
||||
repo: &HackathonRepository<'_>,
|
||||
) -> Result<()> {
|
||||
tracing::info!("Starting cleanup for failed hackathon creation");
|
||||
|
||||
// Delete events
|
||||
for event_id in event_ids {
|
||||
if let Err(e) = repo.delete_hackathon_event(event_id.to_string()).await {
|
||||
tracing::error!("Failed to cleanup event {}: {}", event_id, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete timelines
|
||||
for timeline_id in timeline_ids {
|
||||
if let Err(e) = repo.delete_hackathon_timeline(timeline_id.to_string()).await {
|
||||
tracing::error!("Failed to cleanup timeline {}: {}", timeline_id, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete hackathon
|
||||
if let Err(e) = repo.delete_hackathon(hackathon_id.to_string()).await {
|
||||
tracing::error!("Failed to cleanup hackathon {}: {}", hackathon_id, e);
|
||||
}
|
||||
|
||||
tracing::info!("Cleanup completed");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
use super::hackathon_audit_schema::{AuditAction, HackathonAuditLogSchema};
|
||||
use anyhow::Result;
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto};
|
||||
use surrealdb::sql::Thing;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HackathonAuditRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> HackathonAuditRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, log), err)]
|
||||
pub async fn log(&self, log: HackathonAuditLogSchema) -> Result<HackathonAuditLogSchema> {
|
||||
let table = "app_hackathon_audit_logs";
|
||||
let id = log.id.id.to_string();
|
||||
|
||||
info!(
|
||||
action = %log.action,
|
||||
actor_id = %log.actor_id,
|
||||
resource_type = %log.resource_type,
|
||||
"Creating audit log entry"
|
||||
);
|
||||
|
||||
let record: Option<HackathonAuditLogSchema> = self
|
||||
.state
|
||||
.surrealdb_ws
|
||||
.create((table, id.clone()))
|
||||
.content(log.clone())
|
||||
.await?;
|
||||
|
||||
record.ok_or_else(|| anyhow::anyhow!("Failed to create audit log"))
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err)]
|
||||
pub async fn get_logs_by_hackathon(
|
||||
&self,
|
||||
hackathon_id: &Thing,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<HackathonAuditLogSchema>>> {
|
||||
let table = "app_hackathon_audit_logs";
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let per_page = meta.per_page.unwrap_or(50);
|
||||
let start = (page - 1) * per_page;
|
||||
|
||||
let condition = format!("hackathon_id = {}", hackathon_id);
|
||||
|
||||
let query = format!(
|
||||
"SELECT * FROM {} WHERE {} ORDER BY timestamp DESC LIMIT {} START {}",
|
||||
table, condition, per_page, start
|
||||
);
|
||||
|
||||
let count_query = format!(
|
||||
"SELECT count() as count FROM {} WHERE {} GROUP ALL",
|
||||
table, condition
|
||||
);
|
||||
|
||||
info!(query = %query, "Executing query to get audit logs");
|
||||
|
||||
let logs: Vec<HackathonAuditLogSchema> = self.state.surrealdb_ws.query(&query).await?.take(0)?;
|
||||
let count_result: Vec<imphnen_entities::common_dto::CountResult> =
|
||||
self.state.surrealdb_ws.query(&count_query).await?.take(0)?;
|
||||
|
||||
let total = count_result.first().map(|r| r.count).unwrap_or(0);
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: logs,
|
||||
meta: Some(imphnen_libs::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total: Some(total),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err)]
|
||||
pub async fn get_logs_by_actor(
|
||||
&self,
|
||||
actor_id: &str,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<HackathonAuditLogSchema>>> {
|
||||
let table = "app_hackathon_audit_logs";
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let per_page = meta.per_page.unwrap_or(50);
|
||||
let start = (page - 1) * per_page;
|
||||
|
||||
let condition = format!("actor_id = '{}'", actor_id);
|
||||
|
||||
let query = format!(
|
||||
"SELECT * FROM {} WHERE {} ORDER BY timestamp DESC LIMIT {} START {}",
|
||||
table, condition, per_page, start
|
||||
);
|
||||
|
||||
let count_query = format!(
|
||||
"SELECT count() as count FROM {} WHERE {} GROUP ALL",
|
||||
table, condition
|
||||
);
|
||||
|
||||
let logs: Vec<HackathonAuditLogSchema> = self.state.surrealdb_ws.query(&query).await?.take(0)?;
|
||||
let count_result: Vec<imphnen_entities::common_dto::CountResult> =
|
||||
self.state.surrealdb_ws.query(&count_query).await?.take(0)?;
|
||||
|
||||
let total = count_result.first().map(|r| r.count).unwrap_or(0);
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: logs,
|
||||
meta: Some(imphnen_libs::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total: Some(total),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err)]
|
||||
pub async fn get_logs_by_action(
|
||||
&self,
|
||||
action: AuditAction,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<HackathonAuditLogSchema>>> {
|
||||
let table = "app_hackathon_audit_logs";
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let per_page = meta.per_page.unwrap_or(50);
|
||||
let start = (page - 1) * per_page;
|
||||
|
||||
let condition = format!("action = '{}'", action.to_string());
|
||||
|
||||
let query = format!(
|
||||
"SELECT * FROM {} WHERE {} ORDER BY timestamp DESC LIMIT {} START {}",
|
||||
table, condition, per_page, start
|
||||
);
|
||||
|
||||
let count_query = format!(
|
||||
"SELECT count() as count FROM {} WHERE {} GROUP ALL",
|
||||
table, condition
|
||||
);
|
||||
|
||||
let logs: Vec<HackathonAuditLogSchema> = self.state.surrealdb_ws.query(&query).await?.take(0)?;
|
||||
let count_result: Vec<imphnen_entities::common_dto::CountResult> =
|
||||
self.state.surrealdb_ws.query(&count_query).await?.take(0)?;
|
||||
|
||||
let total = count_result.first().map(|r| r.count).unwrap_or(0);
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: logs,
|
||||
meta: Some(imphnen_libs::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total: Some(total),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err)]
|
||||
pub async fn get_all_logs(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<HackathonAuditLogSchema>>> {
|
||||
let table = "app_hackathon_audit_logs";
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let per_page = meta.per_page.unwrap_or(50);
|
||||
let start = (page - 1) * per_page;
|
||||
|
||||
let query = format!(
|
||||
"SELECT * FROM {} ORDER BY timestamp DESC LIMIT {} START {}",
|
||||
table, per_page, start
|
||||
);
|
||||
|
||||
let count_query = format!(
|
||||
"SELECT count() as count FROM {} GROUP ALL",
|
||||
table
|
||||
);
|
||||
|
||||
let logs: Vec<HackathonAuditLogSchema> = self.state.surrealdb_ws.query(&query).await?.take(0)?;
|
||||
let count_result: Vec<imphnen_entities::common_dto::CountResult> =
|
||||
self.state.surrealdb_ws.query(&count_query).await?.take(0)?;
|
||||
|
||||
let total = count_result.first().map(|r| r.count).unwrap_or(0);
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: logs,
|
||||
meta: Some(imphnen_libs::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total: Some(total),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
|
||||
/// Audit log schema for tracking all hackathon-related changes
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HackathonAuditLogSchema {
|
||||
pub id: Thing,
|
||||
pub hackathon_id: Option<Thing>, // None for system-wide events
|
||||
pub action: AuditAction,
|
||||
pub actor_id: String, // User who performed the action
|
||||
pub actor_email: Option<String>, // For better traceability
|
||||
pub resource_type: String, // hackathon, timeline, event, submission
|
||||
pub resource_id: Option<String>, // ID of the affected resource
|
||||
pub changes: Option<serde_json::Value>, // JSON of what changed
|
||||
pub old_value: Option<serde_json::Value>,
|
||||
pub new_value: Option<serde_json::Value>,
|
||||
pub ip_address: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AuditAction {
|
||||
// Hackathon actions
|
||||
HackathonCreated,
|
||||
HackathonUpdated,
|
||||
HackathonDeleted,
|
||||
HackathonStatusChanged,
|
||||
|
||||
// Timeline actions
|
||||
TimelineCreated,
|
||||
TimelineUpdated,
|
||||
TimelineDeleted,
|
||||
TimelineActivated,
|
||||
|
||||
// Event actions
|
||||
EventCreated,
|
||||
EventUpdated,
|
||||
EventDeleted,
|
||||
|
||||
// Submission actions
|
||||
SubmissionCreated,
|
||||
SubmissionUpdated,
|
||||
SubmissionDeleted,
|
||||
SubmissionStatusChanged,
|
||||
|
||||
// Participant actions
|
||||
ParticipantRegistered,
|
||||
ParticipantRemoved,
|
||||
|
||||
// Organizer actions
|
||||
OrganizerAdded,
|
||||
OrganizerRemoved,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AuditAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AuditAction::HackathonCreated => write!(f, "hackathon_created"),
|
||||
AuditAction::HackathonUpdated => write!(f, "hackathon_updated"),
|
||||
AuditAction::HackathonDeleted => write!(f, "hackathon_deleted"),
|
||||
AuditAction::HackathonStatusChanged => write!(f, "hackathon_status_changed"),
|
||||
AuditAction::TimelineCreated => write!(f, "timeline_created"),
|
||||
AuditAction::TimelineUpdated => write!(f, "timeline_updated"),
|
||||
AuditAction::TimelineDeleted => write!(f, "timeline_deleted"),
|
||||
AuditAction::TimelineActivated => write!(f, "timeline_activated"),
|
||||
AuditAction::EventCreated => write!(f, "event_created"),
|
||||
AuditAction::EventUpdated => write!(f, "event_updated"),
|
||||
AuditAction::EventDeleted => write!(f, "event_deleted"),
|
||||
AuditAction::SubmissionCreated => write!(f, "submission_created"),
|
||||
AuditAction::SubmissionUpdated => write!(f, "submission_updated"),
|
||||
AuditAction::SubmissionDeleted => write!(f, "submission_deleted"),
|
||||
AuditAction::SubmissionStatusChanged => write!(f, "submission_status_changed"),
|
||||
AuditAction::ParticipantRegistered => write!(f, "participant_registered"),
|
||||
AuditAction::ParticipantRemoved => write!(f, "participant_removed"),
|
||||
AuditAction::OrganizerAdded => write!(f, "organizer_added"),
|
||||
AuditAction::OrganizerRemoved => write!(f, "organizer_removed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HackathonAuditLogSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
"app_hackathon_audit_logs",
|
||||
&surrealdb::Uuid::new_v4().to_string(),
|
||||
),
|
||||
hackathon_id: None,
|
||||
action: AuditAction::HackathonCreated,
|
||||
actor_id: String::new(),
|
||||
actor_email: None,
|
||||
resource_type: String::new(),
|
||||
resource_id: None,
|
||||
changes: None,
|
||||
old_value: None,
|
||||
new_value: None,
|
||||
ip_address: None,
|
||||
user_agent: None,
|
||||
timestamp: Utc::now(),
|
||||
created_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HackathonAuditLogSchema {
|
||||
pub fn new(
|
||||
hackathon_id: Option<Thing>,
|
||||
action: AuditAction,
|
||||
actor_id: String,
|
||||
resource_type: String,
|
||||
resource_id: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
"app_hackathon_audit_logs",
|
||||
&surrealdb::Uuid::new_v4().to_string(),
|
||||
),
|
||||
hackathon_id,
|
||||
action,
|
||||
actor_id,
|
||||
actor_email: None,
|
||||
resource_type,
|
||||
resource_id,
|
||||
changes: None,
|
||||
old_value: None,
|
||||
new_value: None,
|
||||
ip_address: None,
|
||||
user_agent: None,
|
||||
timestamp: Utc::now(),
|
||||
created_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_changes(mut self, changes: serde_json::Value) -> Self {
|
||||
self.changes = Some(changes);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_old_new_values(
|
||||
mut self,
|
||||
old_value: serde_json::Value,
|
||||
new_value: serde_json::Value,
|
||||
) -> Self {
|
||||
self.old_value = Some(old_value);
|
||||
self.new_value = Some(new_value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_request_info(
|
||||
mut self,
|
||||
ip_address: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
actor_email: Option<String>,
|
||||
) -> Self {
|
||||
self.ip_address = ip_address;
|
||||
self.user_agent = user_agent;
|
||||
self.actor_email = actor_email;
|
||||
self
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user