Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/migrations/006_create_project_likes_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS project_likes (
project_id UUID NOT NULL,
user_address VARCHAR(42) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (project_id, user_address),
CONSTRAINT fk_project_likes_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_project_likes_project_id ON project_likes(project_id);
CREATE INDEX IF NOT EXISTS idx_project_likes_user_address ON project_likes(user_address);
46 changes: 46 additions & 0 deletions backend/src/application/commands/create_project_like.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::sync::Arc;

use crate::{
application::dtos::like_dtos::ProjectLikeCreatedResponse,
domain::{
entities::projects::ProjectId,
repositories::{ProjectLikeRepository, ProjectRepository},
value_objects::WalletAddress,
},
};

pub async fn create_project_like(
project_repository: Arc<dyn ProjectRepository>,
like_repository: Arc<dyn ProjectLikeRepository>,
user_address: String,
project_id: String,
) -> Result<ProjectLikeCreatedResponse, String> {
let user_address = WalletAddress::new(user_address.to_lowercase())
.map_err(|e| format!("Invalid wallet address: {}", e))?;

let uuid = uuid::Uuid::parse_str(&project_id).map_err(|_| "Invalid project id".to_string())?;
let project_id = ProjectId::from_uuid(uuid);

let exists = project_repository
.exists(&project_id)
.await
.map_err(|e| e.to_string())?;
if !exists {
return Err("Project not found".to_string());
}

let created = like_repository
.create(&project_id, &user_address)
.await
.map_err(|e| e.to_string())?;

if !created {
return Err("Like already exists".to_string());
}

Ok(ProjectLikeCreatedResponse {
project_id: project_id.value().to_string(),
user_address: user_address.to_string(),
})
}

40 changes: 40 additions & 0 deletions backend/src/application/commands/delete_project_like.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use std::sync::Arc;

use crate::domain::{
entities::projects::ProjectId,
repositories::{ProjectLikeRepository, ProjectRepository},
value_objects::WalletAddress,
};

pub async fn delete_project_like(
project_repository: Arc<dyn ProjectRepository>,
like_repository: Arc<dyn ProjectLikeRepository>,
user_address: String,
project_id: String,
) -> Result<(), String> {
let user_address = WalletAddress::new(user_address.to_lowercase())
.map_err(|e| format!("Invalid wallet address: {}", e))?;

let uuid = uuid::Uuid::parse_str(&project_id).map_err(|_| "Invalid project id".to_string())?;
let project_id = ProjectId::from_uuid(uuid);

let exists = project_repository
.exists(&project_id)
.await
.map_err(|e| e.to_string())?;
if !exists {
return Err("Project not found".to_string());
}

let deleted = like_repository
.delete(&project_id, &user_address)
.await
.map_err(|e| e.to_string())?;

if !deleted {
return Err("Like not found".to_string());
}

Ok(())
}

2 changes: 2 additions & 0 deletions backend/src/application/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod create_profile;
pub mod create_project_like;
pub mod create_project;
pub mod delete_project_like;
pub mod delete_project;
pub mod login;
pub mod update_profile;
Expand Down
21 changes: 21 additions & 0 deletions backend/src/application/dtos/like_dtos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectLikeResponse {
pub user_address: String,
pub created_at: DateTime<Utc>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectLikesResponse {
pub project_id: String,
pub total: i64,
pub likes: Vec<ProjectLikeResponse>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectLikeCreatedResponse {
pub project_id: String,
pub user_address: String,
}
2 changes: 2 additions & 0 deletions backend/src/application/dtos/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub mod auth_dtos;
pub mod like_dtos;
pub mod profile_dtos;
pub mod project_dtos;
pub use auth_dtos::*;
pub use like_dtos::*;
pub use profile_dtos::*;
pub use project_dtos::*;
53 changes: 53 additions & 0 deletions backend/src/application/queries/get_project_likes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::sync::Arc;

use crate::{
application::dtos::like_dtos::{ProjectLikeResponse, ProjectLikesResponse},
domain::{
entities::projects::ProjectId,
repositories::{ProjectLikeRepository, ProjectRepository},
},
};

pub async fn get_project_likes(
project_repository: Arc<dyn ProjectRepository>,
like_repository: Arc<dyn ProjectLikeRepository>,
project_id: String,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<ProjectLikesResponse, String> {
let uuid = uuid::Uuid::parse_str(&project_id).map_err(|_| "Invalid project id".to_string())?;
let project_id = ProjectId::from_uuid(uuid);

let exists = project_repository
.exists(&project_id)
.await
.map_err(|e| e.to_string())?;
if !exists {
return Err("Project not found".to_string());
}

let limit = limit.unwrap_or(50).clamp(1, 100);
let offset = offset.unwrap_or(0).max(0);

let total = like_repository
.count_by_project(&project_id)
.await
.map_err(|e| e.to_string())?;
let likes = like_repository
.list_by_project(&project_id, limit, offset)
.await
.map_err(|e| e.to_string())?
.into_iter()
.map(|l| ProjectLikeResponse {
user_address: l.user_address.to_string(),
created_at: l.created_at,
})
.collect();

Ok(ProjectLikesResponse {
project_id: project_id.value().to_string(),
total,
likes,
})
}

1 change: 1 addition & 0 deletions backend/src/application/queries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pub mod get_profile;
pub mod get_projects_by_creator;

pub mod get_project;
pub mod get_project_likes;
1 change: 1 addition & 0 deletions backend/src/domain/entities/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod profile;
pub mod projects;
pub mod project_like;

pub use profile::Profile;
pub use projects::{Project, ProjectId, ProjectStatus};
10 changes: 10 additions & 0 deletions backend/src/domain/entities/project_like.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use chrono::{DateTime, Utc};

use crate::domain::{entities::projects::ProjectId, value_objects::WalletAddress};

#[derive(Debug, Clone)]
pub struct ProjectLike {
pub project_id: ProjectId,
pub user_address: WalletAddress,
pub created_at: DateTime<Utc>,
}
2 changes: 2 additions & 0 deletions backend/src/domain/repositories/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod profile_repository;
pub mod project_like_repository;
pub mod project_repository;

pub use profile_repository::ProfileRepository;
pub use project_like_repository::ProjectLikeRepository;
pub use project_repository::ProjectRepository;
39 changes: 39 additions & 0 deletions backend/src/domain/repositories/project_like_repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use async_trait::async_trait;

use crate::domain::{
entities::{project_like::ProjectLike, projects::ProjectId},
value_objects::WalletAddress,
};

#[async_trait]
pub trait ProjectLikeRepository: Send + Sync {
async fn create(
&self,
project_id: &ProjectId,
user_address: &WalletAddress,
) -> Result<bool, Box<dyn std::error::Error>>;

async fn delete(
&self,
project_id: &ProjectId,
user_address: &WalletAddress,
) -> Result<bool, Box<dyn std::error::Error>>;

async fn list_by_project(
&self,
project_id: &ProjectId,
limit: i64,
offset: i64,
) -> Result<Vec<ProjectLike>, Box<dyn std::error::Error>>;

async fn count_by_project(
&self,
project_id: &ProjectId,
) -> Result<i64, Box<dyn std::error::Error>>;

async fn exists(
&self,
project_id: &ProjectId,
user_address: &WalletAddress,
) -> Result<bool, Box<dyn std::error::Error>>;
}
2 changes: 2 additions & 0 deletions backend/src/infrastructure/repositories/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod postgres_profile_repository;
pub mod postgres_project_like_repository;
pub mod postgres_project_repository;

pub use postgres_profile_repository::PostgresProfileRepository;
pub use postgres_project_like_repository::PostgresProjectLikeRepository;
pub use postgres_project_repository::PostgresProjectRepository;
Loading
Loading