From 81e126013bc06b0f03127bbace3e3b6118b29c55 Mon Sep 17 00:00:00 2001 From: Artemiy Solopov Date: Tue, 25 Aug 2026 23:38:29 +0300 Subject: [PATCH] Auctions schema and creation --- app/controllers/AuctionsController.scala | 45 ++++++++++++++++++++++++ app/dao/AuctionsDAO.scala | 33 +++++++++++++++++ app/models/models.scala | 7 +++- app/schemas/schemas.scala | 26 +++++++++++--- conf/evolutions/default/3.sql | 17 +++++++++ conf/routes | 3 ++ 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 app/controllers/AuctionsController.scala create mode 100644 app/dao/AuctionsDAO.scala create mode 100644 conf/evolutions/default/3.sql diff --git a/app/controllers/AuctionsController.scala b/app/controllers/AuctionsController.scala new file mode 100644 index 0000000..9ce8b33 --- /dev/null +++ b/app/controllers/AuctionsController.scala @@ -0,0 +1,45 @@ +package controllers + +import controllers.authentication.AuthenticatedAction +import dao.{AuctionsDAO, AuthTokensDAO} +import models.Auction +import play.api.libs.functional.syntax.toFunctionalBuilderOps +import play.api.libs.json.{JsError, JsPath, JsValue, Json, Reads, Writes} +import play.api.mvc.{BaseController, ControllerComponents} + +import java.security.SecureRandom +import javax.inject.{Inject, Singleton} +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class AuctionsController @Inject()(val controllerComponents: ControllerComponents, val auctionsDAO: AuctionsDAO, val authTokensDAO: AuthTokensDAO, implicit val ec: ExecutionContext) extends BaseController { + def create = Action.andThen(AuthenticatedAction(authTokensDAO)).async(parse.json) { implicit request => + val auctionCreateDataResult = request.body.validate[AuctionCreateData] + auctionCreateDataResult.fold( + errors => Future.successful(BadRequest(JsError.toJson(errors))), + auctionCreateData => { + for { + auction <- auctionsDAO.create(auctionCreateData.name, auctionCreateData.startingPrice, request.user) + json = Json.toJson(auction) + response = Created(json) + } yield response + } + ) + } + + case class AuctionCreateData(name: String, startingPrice: BigDecimal) + private implicit val auctionCreateDataReads: Reads[AuctionCreateData] = ( + (JsPath \ "name").read[String] and (JsPath \ "starting_price").read[BigDecimal] + )(AuctionCreateData.apply) + private implicit val auctionWrites: Writes[Auction] = new Writes[Auction] { + override def writes(a: Auction): JsValue = Json.obj( + "id" -> a.id, + "name" -> a.name, + "gid" -> a.gid, + "starting_price" -> a.startingPrice, + "current_bid" -> a.currentBid, + "current_bidder_id" -> a.currentBidderId, + "current_bid_at" -> a.currentBidAt + ) + } +} diff --git a/app/dao/AuctionsDAO.scala b/app/dao/AuctionsDAO.scala new file mode 100644 index 0000000..2c1d68d --- /dev/null +++ b/app/dao/AuctionsDAO.scala @@ -0,0 +1,33 @@ +package dao + +import models.Auction +import schemas.Auctions +import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider} +import slick.jdbc.PostgresProfile +import slick.jdbc.PostgresProfile.api.* +import utils.Random + +import java.time.Instant +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} + +class AuctionsDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)(implicit val ec: ExecutionContext) + extends HasDatabaseConfigProvider[PostgresProfile] { + + def create(name: String, startingPrice: BigDecimal, author: models.User): Future[Auction] = { + val auction = Auction( + id = None, name = name, + gid = Random.randomBase64(24), + authorId = author.id.get, + startingPrice = startingPrice, + currentBid = None, currentBidderId = None, currentBidAt = None, + createdAt = Instant.now() + ) + + create(auction) + } + + def create(auction: Auction) : Future[Auction] = { + db.run((Auctions returning Auctions) += auction) + } +} diff --git a/app/models/models.scala b/app/models/models.scala index c52eab3..c4fcda8 100644 --- a/app/models/models.scala +++ b/app/models/models.scala @@ -4,4 +4,9 @@ import java.time.Instant case class User(id: Option[Int], username: String, gid: String, passwordDigest: Option[String]) -case class AuthToken(id: Option[Int], userId: Int, token: String, createdAt: Instant, expiresAt: Instant) \ No newline at end of file +case class AuthToken(id: Option[Int], userId: Int, token: String, createdAt: Instant, expiresAt: Instant) + +case class Auction(id: Option[Int], name: String, gid: String, authorId: Int, + startingPrice: BigDecimal, currentBid: Option[BigDecimal], + currentBidderId: Option[Int], currentBidAt: Option[Instant], + createdAt: Instant) \ No newline at end of file diff --git a/app/schemas/schemas.scala b/app/schemas/schemas.scala index e3816a6..3396f54 100644 --- a/app/schemas/schemas.scala +++ b/app/schemas/schemas.scala @@ -1,9 +1,9 @@ package schemas -import slick.jdbc.PostgresProfile.api._ -import models._ +import slick.jdbc.PostgresProfile.api.* +import models.* import slick.lifted.ProvenShape -import slick.model.ForeignKeyAction.Cascade +import slick.model.ForeignKeyAction.{Cascade, Restrict} import java.time.Instant @@ -31,4 +31,22 @@ class AuthTokensTable(tag: Tag) extends Table[AuthToken](tag, "auth_tokens") { def user = foreignKey("users", userId, Users)(_.id, onDelete = Cascade) } -lazy val AuthTokens = TableQuery[AuthTokensTable] \ No newline at end of file +lazy val AuthTokens = TableQuery[AuthTokensTable] + +class AuctionsTable(tag: Tag) extends Table[Auction](tag, "auctions") { + def id = column[Int]("id", O.PrimaryKey, O.AutoInc) + def name = column[String]("name") + def gid = column[String]("gid", O.Unique) + def authorId = column[Int]("author_id") + def startingPrice = column[BigDecimal]("starting_price") + def currentBid = column[Option[BigDecimal]]("current_bid") + def currentBidderId = column[Option[Int]]("current_bidder_id") + def currentBidAt = column[Option[Instant]]("current_bid_at") + def createdAt = column[Instant]("created_at") + + override def * : ProvenShape[Auction] = (id.?, name, gid, authorId, startingPrice, currentBid, currentBidderId, currentBidAt, createdAt).mapTo[Auction] + + def author = foreignKey("users", authorId, Users)(_.id, onDelete = Restrict) +} + +lazy val Auctions = TableQuery[AuctionsTable] \ No newline at end of file diff --git a/conf/evolutions/default/3.sql b/conf/evolutions/default/3.sql new file mode 100644 index 0000000..deed1c5 --- /dev/null +++ b/conf/evolutions/default/3.sql @@ -0,0 +1,17 @@ +-- !Ups +CREATE TABLE auctions +( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + name VARCHAR NOT NULL, + gid VARCHAR UNIQUE NOT NULL, + author_id BIGINT REFERENCES users (id) NOT NULL, + starting_price DECIMAL(20, 3) NOT NULL, + current_bid DECIMAL(20, 3), + current_bidder_id BIGINT REFERENCES users (id), + current_bid_at TIMESTAMP, + created_at TIMESTAMP NOT NULL +); + +-- !Downs + +DROP TABLE auctions \ No newline at end of file diff --git a/conf/routes b/conf/routes index caacc98..da008e8 100644 --- a/conf/routes +++ b/conf/routes @@ -13,3 +13,6 @@ DELETE /logout controllers.authentication.SessionsControlle # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) + ++nocsrf +POST /auctions controllers.AuctionsController.create() \ No newline at end of file