Skip to content

Commit b36b232

Browse files
[Sprint 8][BE] Create table user_setting and add crud for table
1 parent 30ccb1e commit b36b232

File tree

12 files changed

+227
-3
lines changed

12 files changed

+227
-3
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package controllers
2+
3+
import javax.inject.{Inject, Singleton}
4+
import play.api.mvc._
5+
import play.api.libs.json._
6+
import services.UserProfileService
7+
import models.entities.UserProfile
8+
9+
import scala.concurrent.{ExecutionContext, Future}
10+
11+
@Singleton
12+
class UserProfileController @Inject()(
13+
val controllerComponents: ControllerComponents,
14+
userProfileService: UserProfileService,
15+
authenticatedAction: AuthenticatedActionWithUser
16+
)(implicit ec: ExecutionContext) extends BaseController {
17+
18+
implicit val userProfileWrites: OWrites[UserProfile] = Json.writes[UserProfile]
19+
20+
// Request DTO for partial updates
21+
private case class UpdateUserProfileRequest(userLanguage: Option[String], themeMode: Option[String])
22+
private object UpdateUserProfileRequest {
23+
implicit val reads: Reads[UpdateUserProfileRequest] = Json.reads[UpdateUserProfileRequest]
24+
}
25+
26+
def getUserProfile: Action[AnyContent] = authenticatedAction.async { request =>
27+
userProfileService.getUserProfile(request.userToken.userId).map {
28+
case Some(profile) => Ok(Json.toJson(profile))
29+
case None => NotFound
30+
}
31+
}
32+
33+
def updateProfile: Action[JsValue] = authenticatedAction.async(parse.json) { request =>
34+
request.body.validate[UpdateUserProfileRequest].fold(
35+
errors => Future.successful(BadRequest(JsError.toJson(errors))),
36+
dto => {
37+
userProfileService.getUserProfile(request.userToken.userId).flatMap {
38+
case None => Future.successful(NotFound)
39+
case Some(existing) =>
40+
val updated = existing.copy(
41+
userLanguage = dto.userLanguage.getOrElse(existing.userLanguage),
42+
themeMode = dto.themeMode.getOrElse(existing.themeMode),
43+
updatedAt = java.time.LocalDateTime.now()
44+
)
45+
userProfileService.updateProfile(updated).map {
46+
case Some(profile) => Ok(Json.toJson(profile))
47+
case None => InternalServerError(Json.obj("error" -> "Failed to update profile"))
48+
}
49+
}
50+
}
51+
)
52+
}
53+
}

backend/app/exception/AppException.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package exception
22

33
import play.api.http.Status
44

5-
case class AppException(message: String, statusCode: Int = Status.BAD_REQUEST) extends RuntimeException {
6-
7-
}
5+
/**
6+
* Base application exception with an associated HTTP status code.
7+
* Specific exceptions extend this so handlers can map them to proper HTTP responses.
8+
*/
9+
sealed abstract class AppException(message: String, val statusCode: Int = Status.BAD_REQUEST) extends RuntimeException(message)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package exception
2+
3+
import play.api.http.Status
4+
5+
case class BadRequestException(message: String = "Bad Request", statusCode: Int = Status.BAD_REQUEST)
6+
extends RuntimeException(message)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package exception
2+
3+
import play.api.http.Status
4+
5+
case class InternalErrorException(message: String = "Internal Server Error", statusCode: Int = Status.INTERNAL_SERVER_ERROR)
6+
extends RuntimeException(message)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package exception
2+
3+
import play.api.http.Status
4+
5+
case class NotFoundException(message: String = "Not Found", statusCode: Int = Status.NOT_FOUND)
6+
extends RuntimeException(message)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package exception
2+
3+
import play.api.http.Status
4+
5+
case class ResourceInactiveException(message: String = "Resource Inactive", statusCode: Int = Status.CONFLICT)
6+
extends RuntimeException(message)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package models.entities
2+
3+
import java.time.LocalDateTime
4+
5+
case class UserProfile(
6+
id: Int,
7+
userId: Int,
8+
userLanguage: String,
9+
themeMode: String,
10+
createdAt: LocalDateTime = LocalDateTime.now(),
11+
updatedAt: LocalDateTime = LocalDateTime.now(),
12+
createdBy: Option[Int] = None,
13+
updatedBy: Option[Int] = None,
14+
)
15+
16+
object UserProfile {
17+
def apply(id: Int, userId: Int, userLanguage: String, themeMode: String, createdAt: LocalDateTime, updatedAt: LocalDateTime): UserProfile =
18+
UserProfile(id, userId, userLanguage, themeMode, createdAt, updatedAt, None, None)
19+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package models.tables
2+
3+
import models.entities.UserProfile
4+
import play.api.db.slick.HasDatabaseConfig
5+
import slick.jdbc.PostgresProfile
6+
import java.time.LocalDateTime
7+
8+
trait UserProfileTable { self: HasDatabaseConfig[PostgresProfile] =>
9+
import profile.api._
10+
11+
class UserProfileTable(tag: Tag) extends Table[UserProfile](tag, "user_profiles") {
12+
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
13+
def userId = column[Int]("user_id")
14+
def userLanguage = column[String]("user_language")
15+
def themeMode = column[String]("theme_mode")
16+
def createdAt = column[LocalDateTime]("created_at")
17+
def updatedAt = column[LocalDateTime]("updated_at")
18+
19+
def * = (
20+
id,
21+
userId,
22+
userLanguage,
23+
themeMode,
24+
createdAt,
25+
updatedAt
26+
) <> (
27+
{ case (id, userId, userLanguage, themeMode, createdAt, updatedAt) =>
28+
UserProfile(id, userId, userLanguage, themeMode, createdAt, updatedAt)
29+
},
30+
(up: UserProfile) => Some((up.id, up.userId, up.userLanguage, up.themeMode, up.createdAt, up.updatedAt))
31+
)
32+
33+
private def userFk = foreignKey(
34+
"user_profile_user_fk",
35+
userId,
36+
TableQuery[UserTable]
37+
)(_.id, onDelete = ForeignKeyAction.Cascade)
38+
39+
private def userIdIdx = index("user_profile_user_id_idx", userId, unique = true)
40+
}
41+
42+
lazy val userProfiles = TableQuery[UserProfileTable]
43+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package repositories
2+
3+
import javax.inject.{Inject, Singleton}
4+
import models.entities.UserProfile
5+
import models.tables.UserProfileTable
6+
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfig}
7+
import slick.jdbc.PostgresProfile
8+
import scala.concurrent.{ExecutionContext, Future}
9+
10+
@Singleton
11+
class UserProfileRepository @Inject()(
12+
protected val dbConfigProvider: DatabaseConfigProvider
13+
)(implicit ec: ExecutionContext) extends HasDatabaseConfig[PostgresProfile] with UserProfileTable {
14+
15+
import profile.api._
16+
17+
def create(userProfile: UserProfile): Future[UserProfile] = {
18+
val query = userProfiles returning userProfiles.map(_.id) into ((profile, id) => profile.copy(id = id))
19+
db.run(query += userProfile)
20+
}
21+
22+
def update(userProfile: UserProfile): Future[Int] = {
23+
val query = userProfiles.filter(_.id === userProfile.id).update(userProfile)
24+
db.run(query)
25+
}
26+
27+
def findByUserId(userId: Int): Future[Option[UserProfile]] = {
28+
val query = userProfiles.filter(_.userId === userId)
29+
db.run(query.result.headOption)
30+
}
31+
32+
def findById(id: Int): Future[Option[UserProfile]] = {
33+
val query = userProfiles.filter(_.id === id)
34+
db.run(query.result.headOption)
35+
}
36+
37+
def delete(id: Int): Future[Int] = {
38+
val query = userProfiles.filter(_.id === id).delete
39+
db.run(query)
40+
}
41+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package services
2+
3+
import javax.inject.{Inject, Singleton}
4+
import models.entities.UserProfile
5+
import repositories.UserProfileRepository
6+
import scala.concurrent.{ExecutionContext, Future}
7+
8+
@Singleton
9+
class UserProfileService @Inject()(
10+
userProfileRepository: UserProfileRepository
11+
)(implicit ec: ExecutionContext) {
12+
13+
def createProfile(newProfile: UserProfile): Future[UserProfile] = {
14+
userProfileRepository.create(newProfile)
15+
}
16+
17+
def updateProfile(userProfile: UserProfile): Future[Option[UserProfile]] = {
18+
userProfileRepository.update(userProfile).flatMap {
19+
case 0 => Future.successful(None)
20+
case _ => userProfileRepository.findById(userProfile.id)
21+
}
22+
}
23+
24+
def getUserProfile(userId: Int): Future[Option[UserProfile]] = {
25+
userProfileRepository.findByUserId(userId)
26+
}
27+
}

0 commit comments

Comments
 (0)