Skip to content

Commit 80fc0c5

Browse files
authored
feat/comment: CRUD comment APIs (#56)
* create * update * delete * get comments by task id
1 parent 6830a69 commit 80fc0c5

20 files changed

+442
-25
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package controllers
2+
3+
import dto.request.comment.CreateUpdateCommentRequest
4+
import dto.response.ApiResponse
5+
import play.api.i18n.I18nSupport.RequestWithMessagesApi
6+
import play.api.i18n.Messages
7+
import play.api.libs.json.Format.GenericFormat
8+
import play.api.libs.json.{JsValue, Json}
9+
import play.api.mvc.{Action, AnyContent, MessagesAbstractController, MessagesControllerComponents}
10+
import services.CommentService
11+
import utils.WritesExtras.unitWrites
12+
import validations.ValidationHandler
13+
14+
import javax.inject.Inject
15+
import scala.concurrent.ExecutionContext
16+
17+
class CommentController @Inject()(
18+
cc: MessagesControllerComponents,
19+
commentService: CommentService,
20+
authenticatedActionWithUser: AuthenticatedActionWithUser
21+
)(implicit ec: ExecutionContext)
22+
extends MessagesAbstractController(cc)
23+
with ValidationHandler {
24+
25+
def create(taskId: Int): Action[JsValue] =
26+
authenticatedActionWithUser.async(parse.json) { request =>
27+
implicit val messages: Messages = request.messages
28+
val createdBy = request.userToken.userId
29+
30+
handleJsonValidation[CreateUpdateCommentRequest](request.body) { createCommentDto =>
31+
commentService
32+
.createComment(taskId, createCommentDto, createdBy)
33+
.map { commentId =>
34+
Created(
35+
Json.toJson(
36+
ApiResponse[Unit](s"Comment created successfully with ID: $commentId")
37+
)
38+
)
39+
}
40+
}
41+
}
42+
43+
def update(commentId: Int): Action[JsValue] =
44+
authenticatedActionWithUser.async(parse.json) { request =>
45+
implicit val messages: Messages = request.messages
46+
val updatedBy = request.userToken.userId
47+
48+
handleJsonValidation[CreateUpdateCommentRequest](request.body) { updateCommentDto =>
49+
commentService
50+
.updateComment(commentId, updateCommentDto, updatedBy)
51+
.map { _ =>
52+
Ok(
53+
Json.toJson(
54+
ApiResponse[Unit](s"Comment updated successfully")
55+
)
56+
)
57+
}
58+
}
59+
}
60+
61+
def getCommentsByTaskId(taskId: Int): Action[AnyContent] =
62+
authenticatedActionWithUser.async { request =>
63+
val userId = request.userToken.userId
64+
commentService.getCommentsByTaskId(taskId, userId).map { comments =>
65+
Ok(
66+
Json.toJson(
67+
ApiResponse.success(
68+
s"Comments retrieved successfully for task ID: $taskId",
69+
comments
70+
)
71+
)
72+
)
73+
}
74+
}
75+
76+
def delete(commentId: Int): Action[AnyContent] =
77+
authenticatedActionWithUser.async { request =>
78+
val deletedBy = request.userToken.userId
79+
commentService
80+
.deleteComment(commentId, deletedBy)
81+
.map { _ =>
82+
Ok(Json.toJson(ApiResponse[Unit](s"Comment deleted successfully")))
83+
}
84+
}
85+
86+
87+
}

backend/app/db/MyPostgresProfile.scala

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package db
22

33
import com.github.tminglei.slickpg._
44
import models.Enums._
5-
import slick.jdbc.{JdbcType, PostgresProfile}
5+
import slick.jdbc.JdbcType
6+
67

78
/**
89
* MyPostgresProfile is a custom Slick profile for working with PostgreSQL,
@@ -44,19 +45,23 @@ import slick.jdbc.{JdbcType, PostgresProfile}
4445
*
4546
* 5. When Slick runs queries, it automatically converts between Scala Enumeration and PostgreSQL enum.
4647
*/
47-
trait MyPostgresProfile extends PostgresProfile with PgEnumSupport {
48+
trait MyPostgresProfile extends ExPostgresProfile
49+
with PgEnumSupport
50+
with PgPlayJsonSupport { // support JsValue
51+
override val pgjson = "jsonb"
4852

4953
/**
5054
* Override `api` to return a custom API instance.
5155
* Whenever you import `MyPostgresProfile.api._`, all the implicit mappers below will be available.
5256
*/
53-
override val api: API = new API {}
57+
override val api = MyAPI
5458

5559
/**
5660
* Custom API containing implicit mappers for PostgreSQL enums.
5761
* Each mapper uses slick-pg's `createEnumJdbcType` to map Scala Enumeration ↔ Postgres enum.
5862
*/
59-
trait API extends JdbcAPI {
63+
object MyAPI extends ExtPostgresAPI
64+
with JsonImplicits { // JSON support from PgPlayJsonSupport
6065

6166
/** Mapper for enum workspace_status */
6267
implicit val workspaceStatusTypeMapper: JdbcType[WorkspaceStatus.Value] =
@@ -96,6 +101,7 @@ trait MyPostgresProfile extends PostgresProfile with PgEnumSupport {
96101

97102
implicit val taskStatusTypeMapper: JdbcType[TaskStatus.Value] =
98103
createEnumJdbcType("task_status", TaskStatus)
104+
99105
}
100106
}
101107

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package dto.request.comment
2+
3+
import play.api.i18n.Messages
4+
import play.api.libs.json.{JsError, Json, OFormat, Reads, Writes}
5+
import utils.ErrorMessages
6+
import validations.CustomValidators.validateRequiredField
7+
8+
sealed trait CommentChunk {
9+
def commentType: String
10+
}
11+
12+
case class TextChunk(text: String) extends CommentChunk {
13+
val commentType = "text"
14+
}
15+
16+
object TextChunk {
17+
import play.api.libs.json._
18+
19+
implicit def reads(implicit messages: Messages): Reads[TextChunk] =
20+
validateRequiredField[String](
21+
"text",
22+
ErrorMessages.required("Text"),
23+
Seq()
24+
).map(TextChunk.apply)
25+
26+
implicit val writes: Writes[TextChunk] = Json.writes[TextChunk]
27+
}
28+
29+
case class MentionChunk(userId: Int) extends CommentChunk {
30+
val commentType = "mention"
31+
}
32+
33+
object MentionChunk {
34+
import play.api.libs.json._
35+
36+
implicit def reads(implicit messages: Messages): Reads[MentionChunk] =
37+
validateRequiredField[Int](
38+
"userId",
39+
ErrorMessages.required("UserId"),
40+
Seq()
41+
).map(MentionChunk.apply)
42+
43+
implicit val writes: Writes[MentionChunk] = Json.writes[MentionChunk]
44+
}
45+
46+
object CommentChunk {
47+
// Formats for the concrete classes
48+
implicit val textChunkFormat: OFormat[TextChunk] = Json.format[TextChunk]
49+
implicit val mentionChunkFormat: OFormat[MentionChunk] = Json.format[MentionChunk]
50+
51+
// Reads for the sealed trait
52+
implicit val commentChunkReads: Reads[CommentChunk] = Reads { json =>
53+
(json \ "commentType").as[String] match {
54+
case "text" => json.validate[TextChunk]
55+
case "mention" => json.validate[MentionChunk]
56+
case other => JsError(s"Unknown chunk type: $other")
57+
}
58+
}
59+
60+
// Writes for the sealed trait
61+
implicit val commentChunkWrites: Writes[CommentChunk] = Writes {
62+
case t: TextChunk =>
63+
Json.obj(
64+
"commentType" -> t.commentType,
65+
"text" -> t.text
66+
)
67+
case m: MentionChunk =>
68+
Json.obj(
69+
"commentType" -> m.commentType,
70+
"userId" -> m.userId
71+
)
72+
}
73+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dto.request.comment
2+
3+
import play.api.libs.json.{Format, Json, OFormat}
4+
5+
case class CreateUpdateCommentRequest(
6+
content: Seq[CommentChunk]
7+
)
8+
9+
object CreateUpdateCommentRequest {
10+
implicit val format: OFormat[CreateUpdateCommentRequest] = Json.format[CreateUpdateCommentRequest]
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package dto.response.comment
2+
3+
import dto.request.comment.CommentChunk
4+
import play.api.libs.json.{JsValue, Json, OFormat}
5+
6+
import java.time.Instant
7+
8+
case class CommentResponse(commentId: Int,
9+
userId: Int,
10+
name: String,
11+
taskId: Int,
12+
content: JsValue,
13+
createdAt: Instant,
14+
updatedAt: Instant)
15+
16+
object CommentResponse {
17+
implicit val format: OFormat[CommentResponse] = Json.format[CommentResponse]
18+
}

backend/app/models/entities/Task.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package models.entities
33
import models.Enums.TaskPriority.TaskPriority
44
import models.Enums.TaskStatus
55
import models.Enums.TaskStatus.TaskStatus
6-
import play.api.libs.json.{Json, OFormat}
6+
import play.api.libs.json.{JsValue, Json, OFormat}
77

88
import java.time.{Instant, LocalDateTime}
99

@@ -50,11 +50,11 @@ case class ChecklistItem(id: Option[Int] = None,
5050
updatedAt: Option[LocalDateTime] = None)
5151

5252
case class TaskComment(id: Option[Int] = None,
53-
taskId: Option[Int] = None,
54-
userId: Option[Int] = None,
55-
content: Option[String] = None,
56-
createdAt: Option[LocalDateTime] = None,
57-
updatedAt: Option[LocalDateTime] = None)
53+
taskId: Int,
54+
userId: Int,
55+
content: JsValue,
56+
createdAt: Instant = Instant.now(),
57+
updatedAt: Instant = Instant.now())
5858

5959
case class Tag(id: Option[Int] = None,
6060
projectId: Option[Int] = None,

backend/app/models/tables/ActivityLogTable.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package models.tables
22

33
import models.entities.ActivityLog
4-
import slick.jdbc.PostgresProfile.api._
4+
import db.MyPostgresProfile.api._
55
import slick.lifted.Tag
66
import java.time.LocalDateTime
77

backend/app/models/tables/ColumnTable.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package models.tables
22

3+
import db.MyPostgresProfile.Table
34
import models.entities.Column
45
import db.MyPostgresProfile.api._
56
import models.Enums.ColumnStatus.ColumnStatus

backend/app/models/tables/ProjectTable.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package models.tables
22

3+
import db.MyPostgresProfile.Table
34
import models.entities.Project
45
import db.MyPostgresProfile.api._
56
import dto.response.project.ProjectSummariesResponse

backend/app/models/tables/RoleTable.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package models.tables
22

33
import models.entities.Role
4-
import slick.jdbc.PostgresProfile.api._
4+
import db.MyPostgresProfile.api._
55
import slick.lifted.Tag
66

77
class RoleTable(tag: Tag) extends Table[Role](tag, "roles") {

0 commit comments

Comments
 (0)