From ced781bba21ba22d7fd06718d19465824feaff78 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Mon, 17 Jan 2022 04:51:29 +0300 Subject: [PATCH] migrate to anorm I'm tired of ORM and would like to write clear sql requests. The following wrappers were checked: * doobie - cats api which is useless in this project * scalike - can't work with sqlite at all * anorm - awful api * something also Anorm fits more than any other my criteria so I migrated to it with native hikaricp usage --- libraries.sbt | 11 +- .../postgresql/V7_0__Unique_bis_type.sql | 2 + .../sqlite/V7_0__Unique_bis_type.sql | 2 + src/main/resources/html/bis.html | 2 + src/main/resources/html/loot.html | 15 +- src/main/resources/html/party.html | 2 + src/main/resources/html/users.html | 2 + src/main/resources/logback.xml | 5 +- src/main/resources/reference.conf | 25 +-- .../me/arcanis/ffxivbis/Application.scala | 3 +- .../ffxivbis/http/helpers/BiSHelper.scala | 8 + .../http/helpers/BisProviderHelper.scala | 8 + .../ffxivbis/http/helpers/LootHelper.scala | 19 ++- .../ffxivbis/http/helpers/PlayerHelper.scala | 8 + .../ffxivbis/http/helpers/UserHelper.scala | 8 + .../ffxivbis/messages/DatabaseMessage.scala | 3 +- .../me/arcanis/ffxivbis/models/Loot.scala | 2 + .../ffxivbis/service/database/Database.scala | 6 +- .../database}/Migration.scala | 11 +- .../database/impl/DatabaseLootHandler.scala | 4 +- .../arcanis/ffxivbis/storage/BiSProfile.scala | 96 ++++++------ .../ffxivbis/storage/DatabaseConnection.scala | 58 +++++++ .../ffxivbis/storage/DatabaseProfile.scala | 49 +++--- .../ffxivbis/storage/LootProfile.scala | 121 +++++++------- .../ffxivbis/storage/PartyProfile.scala | 62 ++++---- .../ffxivbis/storage/PlayersProfile.scala | 147 ++++++++++-------- .../ffxivbis/storage/UsersProfile.scala | 105 +++++++------ .../me/arcanis/ffxivbis/utils/Implicits.scala | 8 + .../scala/me/arcanis/ffxivbis/Settings.scala | 4 +- .../http/api/v1/BiSEndpointTest.scala | 3 +- .../http/api/v1/LootEndpointTest.scala | 3 +- .../http/api/v1/PartyEndpointTest.scala | 3 +- .../http/api/v1/PlayerEndpointTest.scala | 3 +- .../http/api/v1/UserEndpointTest.scala | 3 +- .../database/DatabaseBiSHandlerTest.scala | 5 +- .../database/DatabaseLootHandlerTest.scala | 22 ++- .../database/DatabasePartyHandlerTest.scala | 1 - .../database/DatabaseUserHandlerTest.scala | 1 - 38 files changed, 495 insertions(+), 345 deletions(-) create mode 100644 src/main/resources/db/migration/postgresql/V7_0__Unique_bis_type.sql create mode 100644 src/main/resources/db/migration/sqlite/V7_0__Unique_bis_type.sql rename src/main/scala/me/arcanis/ffxivbis/{storage => service/database}/Migration.scala (78%) create mode 100644 src/main/scala/me/arcanis/ffxivbis/storage/DatabaseConnection.scala diff --git a/libraries.sbt b/libraries.sbt index cc09bee..90a60bc 100644 --- a/libraries.sbt +++ b/libraries.sbt @@ -1,9 +1,9 @@ -val AkkaVersion = "2.6.17" +val AkkaVersion = "2.6.18" val AkkaHttpVersion = "10.2.7" val ScalaTestVersion = "3.2.10" val SlickVersion = "3.3.3" -libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.9" +libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.10" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4" libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion @@ -15,16 +15,15 @@ libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0 libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" -libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion -libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion -libraryDependencies += "org.flywaydb" % "flyway-core" % "8.2.2" +libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.10" +libraryDependencies += "com.zaxxer" % "HikariCP" % "5.0.1" exclude("org.slf4j", "slf4j-api") +libraryDependencies += "org.flywaydb" % "flyway-core" % "8.4.1" libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3" libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1" libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre" - // testing libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test" libraryDependencies += "org.scalatest" %% "scalatest" % ScalaTestVersion % "test" diff --git a/src/main/resources/db/migration/postgresql/V7_0__Unique_bis_type.sql b/src/main/resources/db/migration/postgresql/V7_0__Unique_bis_type.sql new file mode 100644 index 0000000..c17e076 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V7_0__Unique_bis_type.sql @@ -0,0 +1,2 @@ +drop index bis_piece_type_player_id_idx; +create unique index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type); \ No newline at end of file diff --git a/src/main/resources/db/migration/sqlite/V7_0__Unique_bis_type.sql b/src/main/resources/db/migration/sqlite/V7_0__Unique_bis_type.sql new file mode 100644 index 0000000..c17e076 --- /dev/null +++ b/src/main/resources/db/migration/sqlite/V7_0__Unique_bis_type.sql @@ -0,0 +1,2 @@ +drop index bis_piece_type_player_id_idx; +create unique index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type); \ No newline at end of file diff --git a/src/main/resources/html/bis.html b/src/main/resources/html/bis.html index 28032d5..ad14632 100644 --- a/src/main/resources/html/bis.html +++ b/src/main/resources/html/bis.html @@ -68,6 +68,8 @@ data-show-search-clear-button="true" data-single-select="true" data-sortable="true" + data-sort-name="nick" + data-sort-order="desc" data-sort-reset="true" data-toolbar="#toolbar"> diff --git a/src/main/resources/html/loot.html b/src/main/resources/html/loot.html index 8996a2b..95c9af8 100644 --- a/src/main/resources/html/loot.html +++ b/src/main/resources/html/loot.html @@ -68,6 +68,8 @@ data-show-search-clear-button="true" data-single-select="true" data-sortable="true" + data-sort-name="timestamp" + data-sort-order="desc" data-sort-reset="true" data-toolbar="#toolbar"> @@ -117,6 +119,15 @@ +
+
+
+
+ + +
+
+
@@ -133,10 +144,6 @@ diff --git a/src/main/resources/html/users.html b/src/main/resources/html/users.html index 3e28c9f..cf5aeef 100644 --- a/src/main/resources/html/users.html +++ b/src/main/resources/html/users.html @@ -68,6 +68,8 @@ data-show-search-clear-button="true" data-single-select="true" data-sortable="true" + data-sort-name="username" + data-sort-order="desc" data-sort-reset="true" data-toolbar="#toolbar"> diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 71ccb7b..3a4ce88 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -3,15 +3,14 @@ - + - - + diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 7701121..d43d85f 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -15,26 +15,17 @@ me.arcanis.ffxivbis { mode = "sqlite" sqlite { - profile = "slick.jdbc.SQLiteProfile$" - db { - url = "jdbc:sqlite:ffxivbis.db" - #user = "user" - #password = "password" - } - numThreads = 10 + driverClassName = "org.sqlite.JDBC" + jdbcUrl = "jdbc:sqlite:ffxivbis.db" + #username = "user" + #password = "password" } postgresql { - profile = "slick.jdbc.PostgresProfile$" - db { - url = "jdbc:postgresql://localhost/ffxivbis" - #user = "ffxivbis" - #password = "ffxivbis" - - connectionPool = disabled - keepAliveConnection = yes - } - numThreads = 10 + driverClassName = "org.postgresql.Driver" + jdbcUrl = "jdbc:postgresql://localhost/ffxivbis" + #username = "ffxivbis" + #password = "ffxivbis" } } diff --git a/src/main/scala/me/arcanis/ffxivbis/Application.scala b/src/main/scala/me/arcanis/ffxivbis/Application.scala index 951b91d..3a5e00c 100644 --- a/src/main/scala/me/arcanis/ffxivbis/Application.scala +++ b/src/main/scala/me/arcanis/ffxivbis/Application.scala @@ -17,8 +17,7 @@ import com.typesafe.scalalogging.StrictLogging import me.arcanis.ffxivbis.http.RootEndpoint import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider -import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.service.database.{Database, Migration} import scala.concurrent.ExecutionContext import scala.jdk.CollectionConverters._ diff --git a/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala index 5089dbe..3a8131d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable diff --git a/src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala index 2a28b0a..5322132 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable diff --git a/src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala index ad65a72..aa93be7 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable @@ -25,8 +33,8 @@ trait LootHelper { ): Future[Unit] = (action, maybeFree) match { case (ApiAction.add, Some(isFreeLoot)) => addPieceLoot(playerId, piece, isFreeLoot) - case (ApiAction.remove, _) => removePieceLoot(playerId, piece) - case _ => throw new IllegalArgumentException(s"Invalid combinantion of action $action and fee loot $maybeFree") + case (ApiAction.remove, Some(isFreeLoot)) => removePieceLoot(playerId, piece, isFreeLoot) + case _ => throw new IllegalArgumentException("Loot modification must always contain `isFreeLoot` field") } def loot(partyId: String, playerId: Option[PlayerId])(implicit @@ -35,8 +43,11 @@ trait LootHelper { ): Future[Seq[Player]] = storage.ask(GetLoot(partyId, playerId, _)) - def removePieceLoot(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = - storage.ask(RemovePieceFrom(playerId, piece, _)) + def removePieceLoot(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean)(implicit + timeout: Timeout, + scheduler: Scheduler + ): Future[Unit] = + storage.ask(RemovePieceFrom(playerId, piece, isFreeLoot, _)) def suggestPiece(partyId: String, piece: Piece)(implicit executionContext: ExecutionContext, diff --git a/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala index 80a3647..496c63f 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable diff --git a/src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala index a2b6be0..5b330a1 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala index 72ff50e..9041ffe 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala @@ -51,7 +51,8 @@ case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, rep case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends LootDatabaseMessage -case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends LootDatabaseMessage { +case class RemovePieceFrom(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit]) + extends LootDatabaseMessage { override def partyId: String = playerId.partyId } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala index afcedd2..589b053 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala @@ -13,4 +13,6 @@ import java.time.Instant case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) { def isFreeLootToString: String = if (isFreeLoot) "yes" else "no" + + def isFreeLootToInt: Int = if (isFreeLoot) 1 else 0 } diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala index 19f5832..ad7bcf3 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala @@ -23,7 +23,9 @@ import scala.util.{Failure, Success} trait Database extends StrictLogging { implicit def executionContext: ExecutionContext + def config: Config + def profile: DatabaseProfile def filterParty(party: Party, maybePlayerId: Option[PlayerId]): Seq[Player] = @@ -36,8 +38,8 @@ trait Database extends StrictLogging { for { partyDescription <- profile.getPartyDescription(partyId) players <- profile.getParty(partyId) - bis <- if (withBiS) profile.getPiecesBiS(partyId) else Future(Seq.empty) - loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty) + bis <- if (withBiS) profile.getPiecesBiS(partyId) else Future.successful(Seq.empty) + loot <- if (withLoot) profile.getPieces(partyId) else Future.successful(Seq.empty) } yield Party(partyDescription, config, players, bis, loot) protected def run[T](fn: => Future[T])(onSuccess: T => Unit): Unit = diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/Migration.scala similarity index 78% rename from src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala rename to src/main/scala/me/arcanis/ffxivbis/service/database/Migration.scala index 8c2b782..61c1f09 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/Migration.scala @@ -6,9 +6,10 @@ * * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause */ -package me.arcanis.ffxivbis.storage +package me.arcanis.ffxivbis.service.database import com.typesafe.config.Config +import me.arcanis.ffxivbis.storage.DatabaseProfile import org.flywaydb.core.Flyway import org.flywaydb.core.api.configuration.ClassicConfiguration import org.flywaydb.core.api.output.MigrateResult @@ -17,12 +18,14 @@ import scala.util.Try class Migration(config: Config) { + import me.arcanis.ffxivbis.utils.Implicits._ + def performMigration(): Try[MigrateResult] = { val section = DatabaseProfile.getSection(config) - val url = section.getString("db.url") - val username = Try(section.getString("db.user")).toOption.filter(_.nonEmpty).orNull - val password = Try(section.getString("db.password")).toOption.filter(_.nonEmpty).orNull + val url = section.getString("jdbcUrl") + val username = section.getOptString("username").orNull + val password = section.getOptString("password").orNull val provider = url match { case s"jdbc:$p:$_" => p diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala index 4752d0a..2fd0b54 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala @@ -30,8 +30,8 @@ trait DatabaseLootHandler { this: Database => }(client ! _) Behaviors.same - case RemovePieceFrom(playerId, piece, client) => - run(profile.deletePiece(playerId, piece))(_ => client ! ()) + case RemovePieceFrom(playerId, piece, isFreeLoot, client) => + run(profile.deletePiece(playerId, piece, isFreeLoot))(_ => client ! ()) Behaviors.same case SuggestLoot(partyId, piece, client) => diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala index ffbfa4b..20c6676 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala @@ -8,66 +8,72 @@ */ package me.arcanis.ffxivbis.storage +import anorm.SqlParser._ +import anorm._ import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType} -import slick.lifted.ForeignKeyQuery import java.time.Instant import scala.concurrent.Future -trait BiSProfile { this: DatabaseProfile => - import dbConfig.profile.api._ +trait BiSProfile extends DatabaseConnection { - case class BiSRep(playerId: Long, created: Long, piece: String, pieceType: String, job: String) { - - def toLoot: Loot = Loot( - playerId, - Piece(piece, PieceType.withName(pieceType), Job.withName(job)), - Instant.ofEpochMilli(created), - isFreeLoot = false - ) - } - - object BiSRep { - - def fromPiece(playerId: Long, piece: Piece): BiSRep = - BiSRep(playerId, DatabaseProfile.now, piece.piece, piece.pieceType.toString, piece.job.toString) - } - - class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") { - def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey) - def created: Rep[Long] = column[Long]("created") - def piece: Rep[String] = column[String]("piece", O.PrimaryKey) - def pieceType: Rep[String] = column[String]("piece_type") - def job: Rep[String] = column[String]("job") - - def * = - (playerId, created, piece, pieceType, job) <> ((BiSRep.apply _).tupled, BiSRep.unapply) - - def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] = - foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade) - } + private val loot: RowParser[Loot] = + (long("player_id") ~ str("piece") ~ str("piece_type") + ~ str("job") ~ long("created")) + .map { case playerId ~ piece ~ pieceType ~ job ~ created => + Loot( + playerId = playerId, + piece = Piece( + piece = piece, + pieceType = PieceType.withName(pieceType), + job = Job.withName(job) + ), + timestamp = Instant.ofEpochMilli(created), + isFreeLoot = false, + ) + } def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] = - db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete) + withConnection { implicit conn => + SQL("""delete from bis + | where player_id = {player_id} + | and piece = {piece} + | and piece_type = {piece_type}""".stripMargin) + .on("player_id" -> playerId, "piece" -> piece.piece, "piece_type" -> piece.pieceType.toString) + .executeUpdate() + } def deletePiecesBiSById(playerId: Long): Future[Int] = - db.run(piecesBiS(Seq(playerId)).delete) + withConnection { implicit conn => + SQL("""delete from bis where player_id = {player_id}""") + .on("player_id" -> playerId) + .executeUpdate() + } def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId)) def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] = - db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot)) + withConnection { implicit conn => + SQL("""select * from bis where player_id in ({player_ids})""") + .on("player_ids" -> playerIds) + .executeQuery() + .as(loot.*) + } def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] = - getPiecesBiSById(playerId).flatMap { - case pieces if pieces.exists(loot => loot.piece.strictEqual(piece)) => Future.successful(0) - case _ => db.run(bisTable.insertOrUpdate(BiSRep.fromPiece(playerId, piece))) + withConnection { implicit conn => + SQL("""insert into bis + | (player_id, piece, piece_type, job, created) + | values + | ({player_id}, {piece}, {piece_type}, {job}, {created}) + | on conflict (player_id, piece, piece_type) do nothing""".stripMargin) + .on( + "player_id" -> playerId, + "piece" -> piece.piece, + "piece_type" -> piece.pieceType.toString, + "job" -> piece.job.toString, + "created" -> DatabaseProfile.now + ) + .executeUpdate() } - - private def pieceBiS(piece: BiSRep) = - piecesBiS(Seq(piece.playerId)).filter { stored => - (stored.piece === piece.piece) && (stored.pieceType === piece.pieceType) - } - private def piecesBiS(playerIds: Seq[Long]) = - bisTable.filter(_.playerId.inSet(playerIds.toSet)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseConnection.scala b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseConnection.scala new file mode 100644 index 0000000..5e5b957 --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseConnection.scala @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ +package me.arcanis.ffxivbis.storage + +import com.typesafe.config.Config +import com.zaxxer.hikari.HikariConfig + +import java.sql.Connection +import java.util.Properties +import javax.sql.DataSource +import scala.concurrent.{ExecutionContext, Future, Promise} +import scala.jdk.CollectionConverters._ +import scala.util.control.NonFatal +import scala.util.{Failure, Success, Try} + +trait DatabaseConnection { + + def datasource: DataSource + + def executionContext: ExecutionContext + + def withConnection[T](fn: Connection => T): Future[T] = { + val promise = Promise[T]() + + executionContext.execute { () => + Try(datasource.getConnection) match { + case Success(conn) => + try { + val result = fn(conn) + promise.trySuccess(result) + } catch { + case NonFatal(exception) => promise.tryFailure(exception) + } finally + conn.close() + case Failure(exception) => promise.tryFailure(exception) + } + } + + promise.future + } +} + +object DatabaseConnection { + + def getDataSourceConfig(config: Config): HikariConfig = { + val properties = new Properties() + config.entrySet().asScala.map(_.getKey).foreach { key => + properties.setProperty(key, config.getString(key)) + } + new HikariConfig(properties) + } +} diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala index 2ee01cc..bfc53c1 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala @@ -9,32 +9,32 @@ package me.arcanis.ffxivbis.storage import com.typesafe.config.Config +import com.typesafe.scalalogging.StrictLogging +import com.zaxxer.hikari.HikariDataSource import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId} -import slick.basic.DatabaseConfig -import slick.jdbc.JdbcProfile import java.time.Instant +import javax.sql.DataSource import scala.concurrent.{ExecutionContext, Future} -class DatabaseProfile(context: ExecutionContext, config: Config) - extends BiSProfile +class DatabaseProfile(override val executionContext: ExecutionContext, config: Config) + extends StrictLogging + with BiSProfile with LootProfile with PartyProfile with PlayersProfile with UsersProfile { - implicit val executionContext: ExecutionContext = context - - val dbConfig: DatabaseConfig[JdbcProfile] = - DatabaseConfig.forConfig[JdbcProfile]("", DatabaseProfile.getSection(config)) - import dbConfig.profile.api._ - val db = dbConfig.db - - val bisTable: TableQuery[BiSPieces] = TableQuery[BiSPieces] - val lootTable: TableQuery[LootPieces] = TableQuery[LootPieces] - val partiesTable: TableQuery[Parties] = TableQuery[Parties] - val playersTable: TableQuery[Players] = TableQuery[Players] - val usersTable: TableQuery[Users] = TableQuery[Users] + override val datasource: DataSource = + try { + val profile = DatabaseProfile.getSection(config) + val dataSourceConfig = DatabaseConnection.getDataSourceConfig(profile) + new HikariDataSource(dataSourceConfig) + } catch { + case exception: Exception => + logger.error("exception during storage initialization", exception) + throw exception + } // generic bis api def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] = @@ -53,9 +53,8 @@ class DatabaseProfile(context: ExecutionContext, config: Config) byPlayerId(playerId, insertPieceBiSById(piece)) // generic loot api - def deletePiece(playerId: PlayerId, piece: Piece): Future[Int] = { - // we don't really care here about loot - val loot = Loot(-1, piece, Instant.now, isFreeLoot = false) + def deletePiece(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean): Future[Int] = { + val loot = Loot(-1, piece, Instant.now, isFreeLoot) byPlayerId(playerId, deletePieceById(loot)) } @@ -69,21 +68,23 @@ class DatabaseProfile(context: ExecutionContext, config: Config) byPlayerId(playerId, insertPieceById(loot)) private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] = - getPlayers(partyId).flatMap(callback) + getPlayers(partyId).flatMap(callback)(executionContext) private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] = getPlayer(playerId).flatMap { case Some(id) => callback(id) - case None => Future.failed(new Error(s"Could not find player $playerId")) - } + case None => Future.failed(DatabaseProfile.PlayerNotFound(playerId)) + }(executionContext) } object DatabaseProfile { - def now: Long = Instant.now.toEpochMilli + case class PlayerNotFound(playerId: PlayerId) extends Exception(s"Could not find player $playerId") def getSection(config: Config): Config = { val section = config.getString("me.arcanis.ffxivbis.database.mode") - config.getConfig("me.arcanis.ffxivbis.database").getConfig(section) + config.getConfig(s"me.arcanis.ffxivbis.database.$section") } + + def now: Long = Instant.now.toEpochMilli } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala index 5b7d775..a873faa 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala @@ -8,81 +8,78 @@ */ package me.arcanis.ffxivbis.storage +import anorm.SqlParser._ +import anorm._ import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType} -import slick.lifted.{ForeignKeyQuery, Index} import java.time.Instant import scala.concurrent.Future -trait LootProfile { this: DatabaseProfile => - import dbConfig.profile.api._ +trait LootProfile extends DatabaseConnection { - case class LootRep( - lootId: Option[Long], - playerId: Long, - created: Long, - piece: String, - pieceType: String, - job: String, - isFreeLoot: Int - ) { - - def toLoot: Loot = Loot( - playerId, - Piece(piece, PieceType.withName(pieceType), Job.withName(job)), - Instant.ofEpochMilli(created), - isFreeLoot == 1 - ) - } - - object LootRep { - def fromLoot(playerId: Long, loot: Loot): LootRep = - LootRep( - None, - playerId, - loot.timestamp.toEpochMilli, - loot.piece.piece, - loot.piece.pieceType.toString, - loot.piece.job.toString, - if (loot.isFreeLoot) 1 else 0 - ) - } - - class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") { - def lootId: Rep[Long] = column[Long]("loot_id", O.AutoInc, O.PrimaryKey) - def playerId: Rep[Long] = column[Long]("player_id") - def created: Rep[Long] = column[Long]("created") - def piece: Rep[String] = column[String]("piece") - def pieceType: Rep[String] = column[String]("piece_type") - def job: Rep[String] = column[String]("job") - def isFreeLoot: Rep[Int] = column[Int]("is_free_loot") - - def * = - (lootId.?, playerId, created, piece, pieceType, job, isFreeLoot) <> ((LootRep.apply _).tupled, LootRep.unapply) - - def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] = - foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade) - def lootOwnerIdx: Index = - index("loot_owner_idx", playerId, unique = false) - } + private val loot: RowParser[Loot] = + (long("player_id") ~ str("piece") ~ str("piece_type") + ~ str("job") ~ long("created") ~ int("is_free_loot")) + .map { case playerId ~ piece ~ pieceType ~ job ~ created ~ isFreeLoot => + Loot( + playerId = playerId, + piece = Piece( + piece = piece, + pieceType = PieceType.withName(pieceType), + job = Job.withName(job) + ), + timestamp = Instant.ofEpochMilli(created), + isFreeLoot = isFreeLoot == 1, + ) + } def deletePieceById(loot: Loot)(playerId: Long): Future[Int] = - db.run(pieceLoot(LootRep.fromLoot(playerId, loot)).map(_.lootId).max.result).flatMap { - case Some(id) => db.run(lootTable.filter(_.lootId === id).delete) - case _ => throw new IllegalArgumentException(s"Could not find piece $loot belong to $playerId") + withConnection { implicit conn => + SQL("""delete from loot + | where loot_id in + | ( + | select loot_id from loot + | where player_id = {player_id} + | and piece = {piece} + | and piece_type = {piece_type} + | and job = {job} + | and is_free_loot = {is_free_loot} + | limit 1 + | )""".stripMargin) + .on( + "player_id" -> playerId, + "piece" -> loot.piece.piece, + "piece_type" -> loot.piece.pieceType.toString, + "job" -> loot.piece.job.toString, + "is_free_loot" -> loot.isFreeLootToInt + ) + .executeUpdate() } def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId)) def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] = - db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot)) + withConnection { implicit conn => + SQL("""select * from loot where player_id in ({player_ids})""") + .on("player_ids" -> playerIds) + .executeQuery() + .as(loot.*) + } def insertPieceById(loot: Loot)(playerId: Long): Future[Int] = - db.run(lootTable.insertOrUpdate(LootRep.fromLoot(playerId, loot))) - - private def pieceLoot(piece: LootRep) = - piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece) - - private def piecesLoot(playerIds: Seq[Long]) = - lootTable.filter(_.playerId.inSet(playerIds.toSet)) + withConnection { implicit conn => + SQL("""insert into loot + | (player_id, piece, piece_type, job, created, is_free_loot) + | values + | ({player_id}, {piece}, {piece_type}, {job}, {created}, {is_free_loot})""".stripMargin) + .on( + "player_id" -> playerId, + "piece" -> loot.piece.piece, + "piece_type" -> loot.piece.pieceType.toString, + "job" -> loot.piece.job.toString, + "created" -> DatabaseProfile.now, + "is_free_loot" -> loot.isFreeLootToInt + ) + .executeUpdate() + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala index 7c8ae75..b2cf383 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala @@ -8,47 +8,41 @@ */ package me.arcanis.ffxivbis.storage +import anorm.SqlParser._ +import anorm._ import me.arcanis.ffxivbis.models.PartyDescription import scala.concurrent.Future -trait PartyProfile { this: DatabaseProfile => - import dbConfig.profile.api._ +trait PartyProfile extends DatabaseConnection { - case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) { - - def toDescription: PartyDescription = PartyDescription(partyName, partyAlias) - } - - object PartyRep { - - def fromDescription(party: PartyDescription, id: Option[Long]): PartyRep = - PartyRep(id, party.partyId, party.partyAlias) - } - - class Parties(tag: Tag) extends Table[PartyRep](tag, "parties") { - def partyId: Rep[Long] = column[Long]("party_id", O.AutoInc, O.PrimaryKey) - def partyName: Rep[String] = column[String]("party_name") - def partyAlias: Rep[Option[String]] = column[Option[String]]("party_alias") - - def * = - (partyId.?, partyName, partyAlias) <> ((PartyRep.apply _).tupled, PartyRep.unapply) - } + private val description: RowParser[PartyDescription] = + (str("party_name") ~ str("party_alias").?) + .map { case partyName ~ partyAlias => + PartyDescription( + partyId = partyName, + partyAlias = partyAlias, + ) + } def getPartyDescription(partyId: String): Future[PartyDescription] = - db.run( - partyDescription(partyId).result.headOption.map(_.map(_.toDescription).getOrElse(PartyDescription.empty(partyId))) - ) - - def getUniquePartyId(partyId: String): Future[Option[Long]] = - db.run(partyDescription(partyId).map(_.partyId).result.headOption) - - def insertPartyDescription(partyDescription: PartyDescription): Future[Int] = - getUniquePartyId(partyDescription.partyId).flatMap { - case Some(id) => db.run(partiesTable.update(PartyRep.fromDescription(partyDescription, Some(id)))) - case _ => db.run(partiesTable.insertOrUpdate(PartyRep.fromDescription(partyDescription, None))) + withConnection { implicit conn => + SQL("""select * from parties where party_name = {party_name}""") + .on("party_name" -> partyId) + .executeQuery() + .as(description.singleOpt) + .getOrElse(PartyDescription.empty(partyId)) } - private def partyDescription(partyId: String) = - partiesTable.filter(_.partyName === partyId) + def insertPartyDescription(partyDescription: PartyDescription): Future[Int] = + withConnection { implicit conn => + SQL("""insert into parties + | (party_name, party_alias) + | values + | ({party_name}, {party_alias}) + | on conflict (party_name) do update set + | party_alias = {party_alias}""".stripMargin) + .on("party_name" -> partyDescription.partyId, "party_alias" -> partyDescription.partyAlias) + .executeUpdate() + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala index a660662..4b0758c 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala @@ -8,76 +8,97 @@ */ package me.arcanis.ffxivbis.storage +import anorm.SqlParser._ +import anorm._ import me.arcanis.ffxivbis.models.{BiS, Job, Player, PlayerId} import scala.concurrent.Future -trait PlayersProfile { this: DatabaseProfile => - import dbConfig.profile.api._ +trait PlayersProfile extends DatabaseConnection { - case class PlayerRep( - partyId: String, - playerId: Option[Long], - created: Long, - nick: String, - job: String, - link: Option[String], - priority: Int - ) { + private val player: RowParser[Player] = + (long("player_id") ~ str("party_id") ~ str("job") + ~ str("nick") ~ str("bis_link").? ~ int("priority").?) + .map { case playerId ~ partyId ~ job ~ nick ~ link ~ priority => + Player( + id = playerId, + partyId = partyId, + job = Job.withName(job), + nick = nick, + bis = BiS.empty, + loot = Seq.empty, + link = link, + priority = priority.getOrElse(0), + ) + } - def toPlayer: Player = - Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS.empty, Seq.empty, link, priority) - } - - object PlayerRep { - - def fromPlayer(player: Player, id: Option[Long]): PlayerRep = - PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick, player.job.toString, player.link, player.priority) - } - - class Players(tag: Tag) extends Table[PlayerRep](tag, "players") { - def partyId: Rep[String] = column[String]("party_id") - def playerId: Rep[Long] = column[Long]("player_id", O.AutoInc, O.PrimaryKey) - def created: Rep[Long] = column[Long]("created") - def nick: Rep[String] = column[String]("nick") - def job: Rep[String] = column[String]("job") - def bisLink: Rep[Option[String]] = column[Option[String]]("bis_link") - def priority: Rep[Int] = column[Int]("priority", O.Default(1)) - - def * = - (partyId, playerId.?, created, nick, job, bisLink, priority) <> ((PlayerRep.apply _).tupled, PlayerRep.unapply) - } - - def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete) - - def getParty(partyId: String): Future[Map[Long, Player]] = - db.run(players(partyId).result) - .map(_.foldLeft(Map.empty[Long, Player]) { - case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer) - case (acc, _) => acc - }) - - def getPlayer(playerId: PlayerId): Future[Option[Long]] = - db.run(player(playerId).map(_.playerId).result.headOption) - - def getPlayerFull(playerId: PlayerId): Future[Option[Player]] = - db.run(player(playerId).result.headOption.map(_.map(_.toPlayer))) - - def getPlayers(partyId: String): Future[Seq[Long]] = - db.run(players(partyId).map(_.playerId).result) - - def insertPlayer(playerObj: Player): Future[Int] = - getPlayer(playerObj.playerId).flatMap { - case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id)))) - case _ => db.run(playersTable.insertOrUpdate(PlayerRep.fromPlayer(playerObj, None))) + def deletePlayer(playerId: PlayerId): Future[Int] = + withConnection { implicit conn => + SQL("""delete from players + | where party_id = {party_id} + | and nick = {nick} + | and job = {job}""".stripMargin) + .on("party_id" -> playerId.partyId, "nick" -> playerId.nick, "job" -> playerId.job.toString) + .executeUpdate() } - private def player(playerId: PlayerId) = - playersTable - .filter(_.partyId === playerId.partyId) - .filter(_.job === playerId.job.toString) - .filter(_.nick === playerId.nick) + def getParty(partyId: String): Future[Map[Long, Player]] = + withConnection { implicit conn => + SQL("""select * from players where party_id = {party_id}""") + .on("party_id" -> partyId) + .executeQuery() + .as(player.*) + .map(p => p.id -> p) + .toMap + } + + def getPlayer(playerId: PlayerId): Future[Option[Long]] = + withConnection { implicit conn => + SQL("""select player_id from players + | where party_id = {party_id} + | and nick = {nick} + | and job = {job}""".stripMargin) + .on("party_id" -> playerId.partyId, "nick" -> playerId.nick, "job" -> playerId.job.toString) + .executeQuery() + .as(scalar[Long].singleOpt) + } + + def getPlayerFull(playerId: PlayerId): Future[Option[Player]] = + withConnection { implicit conn => + SQL("""select * from players + | where party_id = {party_id} + | and nick = {nick} + | and job = {job}""".stripMargin) + .on("party_id" -> playerId.partyId, "nick" -> playerId.nick, "job" -> playerId.job.toString) + .executeQuery() + .as(player.singleOpt) + } + + def getPlayers(partyId: String): Future[Seq[Long]] = + withConnection { implicit conn => + SQL("""select player_id from players where party_id = {party_id}""") + .on("party_id" -> partyId) + .executeQuery() + .as(scalar[Long].*) + } + + def insertPlayer(player: Player): Future[Int] = + withConnection { implicit conn => + SQL("""insert into players + | (party_id, created, job, nick, bis_link, priority) + | values + | ({party_id}, {created}, {job}, {nick}, {link}, {priority}) + | on conflict (party_id, nick, job) do update set + | bis_link = {link}, priority = {priority}""".stripMargin) + .on( + "party_id" -> player.partyId, + "created" -> DatabaseProfile.now, + "job" -> player.job.toString, + "nick" -> player.nick, + "link" -> player.link, + "priority" -> player.priority + ) + .executeUpdate() + } - private def players(partyId: String) = - playersTable.filter(_.partyId === partyId) } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala index 788775d..ac7c3de 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala @@ -8,64 +8,67 @@ */ package me.arcanis.ffxivbis.storage +import anorm.SqlParser._ +import anorm._ import me.arcanis.ffxivbis.models.{Permission, User} -import slick.lifted.{Index, PrimaryKey} import scala.concurrent.Future -trait UsersProfile { this: DatabaseProfile => - import dbConfig.profile.api._ +trait UsersProfile extends DatabaseConnection { - case class UserRep(partyId: String, userId: Option[Long], username: String, password: String, permission: String) { - - def toUser: User = User(partyId, username, password, Permission.withName(permission)) - } - - object UserRep { - - def fromUser(user: User, id: Option[Long]): UserRep = - UserRep(user.partyId, id, user.username, user.password, user.permission.toString) - } - - class Users(tag: Tag) extends Table[UserRep](tag, "users") { - def partyId: Rep[String] = column[String]("party_id") - def userId: Rep[Long] = column[Long]("user_id", O.AutoInc, O.PrimaryKey) - def username: Rep[String] = column[String]("username") - def password: Rep[String] = column[String]("password") - def permission: Rep[String] = column[String]("permission") - - def * = - (partyId, userId.?, username, password, permission) <> ((UserRep.apply _).tupled, UserRep.unapply) - - def pk: PrimaryKey = primaryKey("users_username_idx", (partyId, username)) - def usersUsernameIdx: Index = - index("users_username_idx", (partyId, username), unique = true) - } + private val user: RowParser[User] = + (str("party_id") ~ str("username") ~ str("password") ~ str("permission")) + .map { case partyId ~ username ~ password ~ permission => + User( + partyId = partyId, + username = username, + password = password, + permission = Permission.withName(permission), + ) + } def deleteUser(partyId: String, username: String): Future[Int] = - db.run( - user(partyId, Some(username)) - .filter(_.permission =!= Permission.admin.toString) // we do not allow to remove admins - .delete - ) - - def exists(partyId: String): Future[Boolean] = - db.run(user(partyId, None).exists.result) - - def getUser(partyId: String, username: String): Future[Option[User]] = - db.run(user(partyId, Some(username)).result.headOption).map(_.map(_.toUser)) - - def getUsers(partyId: String): Future[Seq[User]] = - db.run(user(partyId, None).result).map(_.map(_.toUser)) - - def insertUser(userObj: User): Future[Int] = - db.run(user(userObj.partyId, Some(userObj.username)).map(_.userId).result.headOption).flatMap { - case Some(id) => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, Some(id)))) - case _ => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, None))) + withConnection { implicit conn => + SQL("""delete from users + | where party_id = {party_id} + | and username = {username} + | and permission <> {admin}""".stripMargin) + .on("party_id" -> partyId, "username" -> username, "admin" -> Permission.admin.toString) + .executeUpdate() } - private def user(partyId: String, username: Option[String]) = - usersTable - .filter(_.partyId === partyId) - .filterIf(username.isDefined)(_.username === username.orNull) + def exists(partyId: String): Future[Boolean] = getUsers(partyId).map(_.nonEmpty)(executionContext) + + def getUser(partyId: String, username: String): Future[Option[User]] = + withConnection { implicit conn => + SQL("""select * from users where party_id = {party_id} and username = {username}""") + .on("party_id" -> partyId, "username" -> username) + .executeQuery() + .as(user.singleOpt) + } + + def getUsers(partyId: String): Future[Seq[User]] = + withConnection { implicit conn => + SQL("""select * from users where party_id = {party_id}""") + .on("party_id" -> partyId) + .executeQuery() + .as(user.*) + } + + def insertUser(user: User): Future[Int] = + withConnection { implicit conn => + SQL("""insert into users + | (party_id, username, password, permission) + | values + | ({party_id}, {username}, {password}, {permission}) + | on conflict (party_id, username) do update set + | password = {password}, permission = {permission}""".stripMargin) + .on( + "party_id" -> user.partyId, + "username" -> user.username, + "password" -> user.password, + "permission" -> user.permission.toString + ) + .executeUpdate() + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala index e79b6ef..ed312b4 100644 --- a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala +++ b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala @@ -9,11 +9,13 @@ package me.arcanis.ffxivbis.utils import akka.util.Timeout +import com.typesafe.config.Config import java.time.Duration import java.util.concurrent.TimeUnit import scala.concurrent.duration.FiniteDuration import scala.language.implicitConversions +import scala.util.Try object Implicits { @@ -27,4 +29,10 @@ object Implicits { implicit def getTimeout(duration: Duration): Timeout = FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS) + + implicit class ConfigExtension(config: Config) { + + def getOptString(path: String): Option[String] = + Try(config.getString(path)).toOption.filter(_.nonEmpty) + } } diff --git a/src/test/scala/me/arcanis/ffxivbis/Settings.scala b/src/test/scala/me/arcanis/ffxivbis/Settings.scala index cea846e..28c657f 100644 --- a/src/test/scala/me/arcanis/ffxivbis/Settings.scala +++ b/src/test/scala/me/arcanis/ffxivbis/Settings.scala @@ -17,7 +17,7 @@ object Settings { } def clearDatabase(config: Config): Unit = - config.getString("me.arcanis.ffxivbis.database.sqlite.db.url").split(":") + config.getString("me.arcanis.ffxivbis.database.sqlite.jdbcUrl").split(":") .lastOption.foreach { databasePath => val databaseFile = new File(databasePath) if (databaseFile.exists) @@ -25,5 +25,5 @@ object Settings { } def randomDatabasePath: String = File.createTempFile("ffxivdb-",".db").toPath.toString def withRandomDatabase: Config = - config(Map("me.arcanis.ffxivbis.database.sqlite.db.url" -> s"jdbc:sqlite:$randomDatabasePath")) + config(Map("me.arcanis.ffxivbis.database.sqlite.jdbcUrl" -> s"jdbc:sqlite:$randomDatabasePath")) } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala index 2238443..f9111b3 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala @@ -12,8 +12,7 @@ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.models.{BiS, Job} import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider -import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.service.database.{Database, Migration} import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala index e45bf35..03d7716 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala @@ -10,8 +10,7 @@ import com.typesafe.config.Config import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.service.PartyService -import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.service.database.{Database, Migration} import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala index 9b4aae1..189bc42 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala @@ -12,8 +12,7 @@ import me.arcanis.ffxivbis.messages.AddUser import me.arcanis.ffxivbis.models.PartyDescription import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider -import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.service.database.{Database, Migration} import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala index cd1ccfd..3ef4dd1 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala @@ -11,8 +11,7 @@ import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider -import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.service.database.{Database, Migration} import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala index 26143e4..70c85d3 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala @@ -8,8 +8,7 @@ import akka.testkit.TestKit import com.typesafe.config.Config import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.service.PartyService -import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.service.database.{Database, Migration} import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike diff --git a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseBiSHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseBiSHandlerTest.scala index 35eef8b..1bcb616 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseBiSHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseBiSHandlerTest.scala @@ -4,7 +4,6 @@ import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetBiS, RemovePieceFromBiS} import me.arcanis.ffxivbis.models._ -import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.wordspec.AnyWordSpecLike @@ -85,6 +84,6 @@ class DatabaseBiSHandlerTest extends ScalaTestWithActorTestKit(Settings.withRand } - private def partyBiSCompare[T](party: Seq[T], bis: Seq[Piece]): Boolean = - Compare.seqEquals(party.foldLeft(Seq.empty[Piece]){ case (acc, player) => acc ++ player.asInstanceOf[Player].bis.pieces }, bis) + private def partyBiSCompare(party: Seq[Player], bis: Seq[Piece]): Boolean = + Compare.seqEquals(party.foldLeft(Seq.empty[Piece]){ case (acc, player) => acc ++ player.bis.pieces }, bis) } diff --git a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseLootHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseLootHandlerTest.scala index 2821e4d..bc3e1d4 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseLootHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseLootHandlerTest.scala @@ -2,9 +2,9 @@ package me.arcanis.ffxivbis.service.database import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable +import ch.qos.logback.core.util.FixedDelay import me.arcanis.ffxivbis.messages.{AddPieceTo, AddPlayer, GetLoot, RemovePieceFrom} import me.arcanis.ffxivbis.models._ -import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.wordspec.AnyWordSpecLike @@ -58,7 +58,7 @@ class DatabaseLootHandlerTest extends ScalaTestWithActorTestKit(Settings.withRan "remove loot" in { val updateProbe = testKit.createTestProbe[Unit]() - database ! RemovePieceFrom(Fixtures.playerEmpty.playerId, Fixtures.lootBody, updateProbe.ref) + database ! RemovePieceFrom(Fixtures.playerEmpty.playerId, Fixtures.lootBody, isFreeLoot = false, updateProbe.ref) updateProbe.expectMessage(askTimeout, ()) val newLoot = Fixtures.loot.filterNot(_ == Fixtures.lootBody) @@ -87,10 +87,24 @@ class DatabaseLootHandlerTest extends ScalaTestWithActorTestKit(Settings.withRan partyLootCompare(party, Fixtures.loot ++ Fixtures.loot) shouldEqual true } + "remove only one piece" in { + val updateProbe = testKit.createTestProbe[Unit]() + database ! RemovePieceFrom(Fixtures.playerEmpty.playerId, Fixtures.lootBody, isFreeLoot = false, updateProbe.ref) + updateProbe.expectMessage(askTimeout, ()) + + val probe = testKit.createTestProbe[Seq[Player]]() + database ! GetLoot(Fixtures.playerEmpty.partyId, None, probe.ref) + + val party = probe.expectMessageType[Seq[Player]](askTimeout) + val player = party.filter(_.playerId == Fixtures.playerEmpty.playerId) + player should not be empty + player.flatMap(_.loot).map(_.piece) should contain (Fixtures.lootBody) + } + } - private def partyLootCompare[T](party: Seq[T], loot: Seq[Piece]): Boolean = + private def partyLootCompare(party: Seq[Player], loot: Seq[Piece]): Boolean = Compare.seqEquals(party.foldLeft(Seq.empty[Piece]){ case (acc, player) => - acc ++ player.asInstanceOf[Player].loot.map(_.piece) + acc ++ player.loot.map(_.piece) }, loot) } diff --git a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala index eea1f4a..1cbc548 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala @@ -3,7 +3,6 @@ package me.arcanis.ffxivbis.service.database import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import me.arcanis.ffxivbis.messages.{AddPlayer, GetParty, GetPlayer, RemovePlayer} import me.arcanis.ffxivbis.models._ -import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.wordspec.AnyWordSpecLike diff --git a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseUserHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseUserHandlerTest.scala index fb9d9ef..7ed77dc 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseUserHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabaseUserHandlerTest.scala @@ -3,7 +3,6 @@ package me.arcanis.ffxivbis.service.database import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetUser, GetUsers} import me.arcanis.ffxivbis.models.User -import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.wordspec.AnyWordSpecLike