feat: v0.3.0 — standardize codebase, centralize infra, merge QR into CMS

- Enforce axum best practices across all 13 workspace crates
  (max 200 LOC/file, no comments, no unwrap, clean architecture)
- Fix domain→infrastructure dependency inversions in imphnen-iam and imphnen-dimentorin
- Extract imphnen-storage (MinIO) and imphnen-email (Lettre) as standalone crates
- Centralize all config in ENV struct: CDN_URL, CORS_ALLOWED_ORIGINS
- Centralize SMTP through imphnen-email; remove dead HackathonConfig
- Centralize database: QR crate now shares main DB pool (single DATABASE_URL)
- Rename QR users table to qr_users to avoid collision with main users table
- Merge imphnen-qr into imphnen-cms/src/qr (13 crates, down from 14)
- Restructure imphnen-hackathon flat modules into clean architecture
- Remove all stale env vars from .env.example (SurrealDB, QR_JWT, Hackathon infra)
- Fix Dockerfile to include all current workspace crates
- Bump all crate versions 0.2.0 → 0.3.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-02 22:29:08 +07:00
co-authored by Claude Sonnet 4.6
parent 2ae43b3bcc
commit 331a4a4e88
442 changed files with 22226 additions and 18700 deletions
+14 -16
View File
@@ -1,25 +1,23 @@
//! 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,
#[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)]
@@ -0,0 +1,8 @@
use super::enums::ResourceEnum;
use std::fmt;
impl fmt::Display for ResourceEnum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
+101 -206
View File
@@ -1,206 +1,101 @@
//! Enum definitions for SeaORM entities
//! Provides resource type enumerations matching SurrealDB ResourceEnum
use std::fmt;
use serde::{Deserialize, Serialize};
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,
/// User cache table for user session data
UsersCache,
/// Gacha items table
GachaItems,
/// Gacha claims table for user item claims
GachaClaims,
/// Gacha rolls table for user roll history
GachaRolls,
/// Gacha credits table for user currency
GachaCredits,
/// Users table for user accounts
Users,
/// Roles table for user roles
Roles,
/// Permissions table for system permissions
Permissions,
/// Role-permission relationships table
RolesPermissions,
/// Events table for application events
Events,
/// Testimonials table for user testimonials
Testimonials,
/// Mentors table for mentor profiles
Mentors,
/// Notifications table for user notifications
Notifications,
/// Rate limiting table for IP-based rate limiting
RateLimit,
/// Audit log table for admin action tracking
AuditLog,
/// Sessions table for mentoring sessions
Sessions,
/// Migration status tracking table
MigrationStatus,
}
impl fmt::Display for ResourceEnum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let table_name = match self {
ResourceEnum::Users => "app_users",
ResourceEnum::UsersCache => "app_users_cache",
ResourceEnum::OtpCache => "app_otp_cache",
ResourceEnum::Roles => "app_roles",
ResourceEnum::Permissions => "app_permissions",
ResourceEnum::RolesPermissions => "app_roles_permissions",
ResourceEnum::GachaItems => "app_gacha_items",
ResourceEnum::GachaClaims => "app_gacha_claims",
ResourceEnum::GachaRolls => "app_gacha_rolls",
ResourceEnum::GachaCredits => "app_gacha_credits",
ResourceEnum::Events => "app_events",
ResourceEnum::Testimonials => "app_testimonials",
ResourceEnum::Mentors => "app_mentors",
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)
}
}
impl ResourceEnum {
/// Get the table name as a string slice.
///
/// # Returns
/// The PostgreSQL table name for this resource
pub fn as_str(&self) -> &'static str {
match self {
ResourceEnum::Users => "app_users",
ResourceEnum::UsersCache => "app_users_cache",
ResourceEnum::OtpCache => "app_otp_cache",
ResourceEnum::Roles => "app_roles",
ResourceEnum::Permissions => "app_permissions",
ResourceEnum::RolesPermissions => "app_roles_permissions",
ResourceEnum::GachaItems => "app_gacha_items",
ResourceEnum::GachaClaims => "app_gacha_claims",
ResourceEnum::GachaRolls => "app_gacha_rolls",
ResourceEnum::GachaCredits => "app_gacha_credits",
ResourceEnum::Events => "app_events",
ResourceEnum::Testimonials => "app_testimonials",
ResourceEnum::Mentors => "app_mentors",
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
/// true if the resource is used for caching, false otherwise
pub fn is_cache(&self) -> bool {
matches!(self, ResourceEnum::OtpCache | ResourceEnum::UsersCache)
}
/// Check if this resource is gacha-related.
///
/// # Returns
/// true if the resource is part of the gacha system, false otherwise
pub fn is_gacha(&self) -> bool {
matches!(
self,
ResourceEnum::GachaItems
| ResourceEnum::GachaClaims
| ResourceEnum::GachaRolls
| ResourceEnum::GachaCredits
)
}
/// Check if this resource is user-related.
///
/// # Returns
/// true if the resource contains user data, false otherwise
pub fn is_user_related(&self) -> bool {
matches!(
self,
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");
}
}
use super::types::PgUuid;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ResourceEnum {
OtpCache,
UsersCache,
GachaItems,
GachaClaims,
GachaRolls,
GachaCredits,
Users,
Roles,
Permissions,
RolesPermissions,
Events,
Testimonials,
Mentors,
Notifications,
RateLimit,
AuditLog,
Sessions,
MigrationStatus,
}
impl ResourceEnum {
pub fn as_str(&self) -> &'static str {
match self {
ResourceEnum::Users => "app_users",
ResourceEnum::UsersCache => "app_users_cache",
ResourceEnum::OtpCache => "app_otp_cache",
ResourceEnum::Roles => "app_roles",
ResourceEnum::Permissions => "app_permissions",
ResourceEnum::RolesPermissions => "app_roles_permissions",
ResourceEnum::GachaItems => "app_gacha_items",
ResourceEnum::GachaClaims => "app_gacha_claims",
ResourceEnum::GachaRolls => "app_gacha_rolls",
ResourceEnum::GachaCredits => "app_gacha_credits",
ResourceEnum::Events => "app_events",
ResourceEnum::Testimonials => "app_testimonials",
ResourceEnum::Mentors => "app_mentors",
ResourceEnum::Notifications => "app_notifications",
ResourceEnum::RateLimit => "app_rate_limit",
ResourceEnum::AuditLog => "app_audit_log",
ResourceEnum::Sessions => "app_sessions",
ResourceEnum::MigrationStatus => "app_migration_status",
}
}
pub fn schema(&self) -> &'static str {
"public"
}
pub fn to_entity_name(&self) -> String {
self.as_str().replace("app_", "").to_pascal_case()
}
pub fn is_cache(&self) -> bool {
matches!(self, ResourceEnum::OtpCache | ResourceEnum::UsersCache)
}
pub fn is_gacha(&self) -> bool {
matches!(
self,
ResourceEnum::GachaItems
| ResourceEnum::GachaClaims
| ResourceEnum::GachaRolls
| ResourceEnum::GachaCredits
)
}
pub fn is_user_related(&self) -> bool {
matches!(
self,
ResourceEnum::Users | ResourceEnum::UsersCache | ResourceEnum::Mentors
)
}
pub fn generate_ref_id(&self, uuid: &PgUuid) -> String {
format!("{}_{}", self.as_str().replace("app_", ""), uuid.0)
}
}
trait ToPascalCase {
fn to_pascal_case(&self) -> String;
}
impl ToPascalCase for str {
fn to_pascal_case(&self) -> String {
self
.split('_')
.map(|s| {
let mut chars = s.chars();
chars
.next()
.map(|c| c.to_uppercase().collect::<String>() + chars.as_str())
.unwrap_or_default()
})
.collect()
}
}
+49 -51
View File
@@ -1,51 +1,49 @@
//! 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 {}
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 {}
+6 -5
View File
@@ -1,11 +1,12 @@
pub mod audit_log;
pub mod enum_impls;
pub mod enums;
pub mod events;
pub mod rate_limit;
pub mod testimonials;
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};
pub use utils::{current_timestamp, generate_uuid};
@@ -1,18 +1,16 @@
//! 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,
#[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)]
@@ -1,51 +1,49 @@
//! 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 {}
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 = "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 {}
+23 -34
View File
@@ -1,73 +1,63 @@
//! 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)
}
fn from(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl From<PgUuid> for Uuid {
fn from(pg_uuid: PgUuid) -> Self {
pg_uuid.0
}
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()
}
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)
}
fn from(timestamp: DateTime<Utc>) -> Self {
Self(timestamp)
}
}
impl From<PgTimestamp> for DateTime<Utc> {
fn from(pg_timestamp: PgTimestamp) -> Self {
pg_timestamp.0
}
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,
T: serde::Serialize,
{
fn from(value: T) -> Self {
Self(value)
}
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>,
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 {
() => {
@@ -86,4 +76,3 @@ macro_rules! common_fields {
.default(None),
};
}
+74 -90
View File
@@ -1,90 +1,74 @@
//! 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());
}
}
use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::types::{PgTimestamp, PgUuid};
pub fn generate_uuid() -> Uuid {
Uuid::new_v4()
}
pub fn generate_timestamp() -> PgTimestamp {
PgTimestamp(Utc::now())
}
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}"))
}
pub fn uuid_to_string(uuid: &uuid::Uuid) -> String {
uuid.to_string()
}
pub fn current_timestamp() -> DateTime<Utc> {
Utc::now()
}
pub fn format_timestamp(timestamp: &PgTimestamp) -> String {
timestamp.0.format("%Y-%m-%d %H:%M:%S UTC").to_string()
}
pub fn create_deleted_at() -> Option<DateTime<Utc>> {
Some(current_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();
let diff = ts2.0.signed_duration_since(ts1.0).num_milliseconds();
assert!(diff >= 0);
assert!(diff < 1000);
}
#[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();
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());
}
}