From eb6f97b4cac548999cbf192ee83d9ba9a253b7c8 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 12 Sep 2018 17:30:33 -0700 Subject: Move database-related functionality to separate project This committ includes a breaking change. The database-specific utility "Converters" trait threw an exception "DatabaseException" defined in the rest package, thus breaking the dependency graph. The solution was to move the DatabaseException class from rest to database and not inherit ServiceExceptio any more. Unfortunately, the rest classes also require the database exception in propagating errors so this funtionality has been removed. The rationale is: 1. Database exceptions are rare and result in 500 errors anyway making the status code opaque to what actual error caused it. 2. In core 2.0, an improved tracing framework will make diagnosing and following database errors easier, thereby attenuating the need to forward details on service exceptions in responses. --- build.sbt | 9 +- .../xyz/driver/core/database/Converters.scala | 25 +++ .../core/database/PatchedHsqldbProfile.scala | 16 ++ .../xyz/driver/core/database/Repository.scala | 74 +++++++++ .../core/database/SlickGetResultSupport.scala | 30 ++++ .../scala/xyz/driver/core/database/database.scala | 178 +++++++++++++++++++++ .../scala/xyz/driver/core/database/package.scala | 61 +++++++ .../xyz/driver/core/database/DatabaseTest.scala | 42 +++++ .../src/main/scala/xyz/driver/core/json.scala | 2 - .../scala/xyz/driver/core/rest/DriverRoute.scala | 3 - .../driver/core/rest/errors/serviceException.scala | 3 - .../xyz/driver/core/database/Converters.scala | 26 --- .../driver/core/database/MdcAsyncExecutor.scala | 53 ------ .../core/database/PatchedHsqldbProfile.scala | 16 -- .../xyz/driver/core/database/Repository.scala | 74 --------- .../core/database/SlickGetResultSupport.scala | 30 ---- .../scala/xyz/driver/core/database/database.scala | 178 --------------------- .../scala/xyz/driver/core/database/package.scala | 61 ------- .../xyz/driver/core/database/DatabaseTest.scala | 42 ----- 19 files changed, 433 insertions(+), 490 deletions(-) create mode 100644 core-database/src/main/scala/xyz/driver/core/database/Converters.scala create mode 100644 core-database/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala create mode 100644 core-database/src/main/scala/xyz/driver/core/database/Repository.scala create mode 100644 core-database/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala create mode 100644 core-database/src/main/scala/xyz/driver/core/database/database.scala create mode 100644 core-database/src/main/scala/xyz/driver/core/database/package.scala create mode 100644 core-database/src/test/scala/xyz/driver/core/database/DatabaseTest.scala delete mode 100644 src/main/scala/xyz/driver/core/database/Converters.scala delete mode 100644 src/main/scala/xyz/driver/core/database/MdcAsyncExecutor.scala delete mode 100644 src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala delete mode 100644 src/main/scala/xyz/driver/core/database/Repository.scala delete mode 100644 src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala delete mode 100644 src/main/scala/xyz/driver/core/database/database.scala delete mode 100644 src/main/scala/xyz/driver/core/database/package.scala delete mode 100644 src/test/scala/xyz/driver/core/database/DatabaseTest.scala diff --git a/build.sbt b/build.sbt index 4f32af7..c57a7ca 100644 --- a/build.sbt +++ b/build.sbt @@ -72,9 +72,14 @@ lazy val `core-messaging` = project .dependsOn(`core-reporting`) .settings(testdeps) +lazy val `core-database` = project + .enablePlugins(LibraryPlugin) + .dependsOn(`core-types`) + .settings(testdeps) + lazy val `core-init` = project .enablePlugins(LibraryPlugin) - .dependsOn(`core-reporting`, `core-storage`, `core-messaging`, `core-rest`) + .dependsOn(`core-reporting`, `core-storage`, `core-messaging`, `core-rest`, `core-database`) .settings(testdeps) lazy val core = project @@ -91,5 +96,5 @@ lazy val core = project s"https://github.com/drivergroup/driver-core/blob/master€{FILE_PATH}.scala" ) ) - .dependsOn(`core-types`, `core-rest`, `core-reporting`, `core-storage`, `core-messaging`, `core-init`) + .dependsOn(`core-types`, `core-rest`, `core-reporting`, `core-storage`, `core-messaging`, `core-database`, `core-init`) .settings(testdeps) diff --git a/core-database/src/main/scala/xyz/driver/core/database/Converters.scala b/core-database/src/main/scala/xyz/driver/core/database/Converters.scala new file mode 100644 index 0000000..b0054ad --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/Converters.scala @@ -0,0 +1,25 @@ +package xyz.driver.core.database + +import scala.reflect.ClassTag + +/** + * Helper methods for converting between table rows and Scala objects + */ +trait Converters { + def fromStringOrThrow[ADT](entityStr: String, mapper: (String => Option[ADT]), entityName: String): ADT = + mapper(entityStr).getOrElse(throw DatabaseException(s"Invalid $entityName in database: $entityStr")) + + def expectValid[ADT](mapper: String => Option[ADT], query: String)(implicit ct: ClassTag[ADT]): ADT = + fromStringOrThrow[ADT](query, mapper, ct.toString()) + + def expectExistsAndValid[ADT](mapper: String => Option[ADT], query: Option[String], contextMsg: String = "")( + implicit ct: ClassTag[ADT]): ADT = { + expectValid[ADT](mapper, query.getOrElse(throw DatabaseException(contextMsg))) + } + + def expectValidOrEmpty[ADT](mapper: String => Option[ADT], query: Option[String], contextMsg: String = "")( + implicit ct: ClassTag[ADT]): Option[ADT] = { + query.map(expectValid[ADT](mapper, _)) + } +} +final case class DatabaseException(message: String = "Database access error") extends RuntimeException(message) diff --git a/core-database/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala b/core-database/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala new file mode 100644 index 0000000..e2efd32 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala @@ -0,0 +1,16 @@ +package xyz.driver.core.database + +import slick.jdbc.{HsqldbProfile, JdbcType} +import slick.ast.FieldSymbol +import slick.relational.RelationalProfile + +trait PatchedHsqldbProfile extends HsqldbProfile { + override def defaultSqlTypeName(tmd: JdbcType[_], sym: Option[FieldSymbol]): String = tmd.sqlType match { + case java.sql.Types.VARCHAR => + val size = sym.flatMap(_.findColumnOption[RelationalProfile.ColumnOption.Length]) + size.fold("LONGVARCHAR")(l => if (l.varying) s"VARCHAR(${l.length})" else s"CHAR(${l.length})") + case _ => super.defaultSqlTypeName(tmd, sym) + } +} + +object PatchedHsqldbProfile extends PatchedHsqldbProfile diff --git a/core-database/src/main/scala/xyz/driver/core/database/Repository.scala b/core-database/src/main/scala/xyz/driver/core/database/Repository.scala new file mode 100644 index 0000000..5d7f787 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/Repository.scala @@ -0,0 +1,74 @@ +package xyz.driver.core.database + +import scalaz.std.scalaFuture._ +import scalaz.{ListT, Monad, OptionT} +import slick.lifted.{AbstractTable, CanBeQueryCondition, RunnableCompiled} +import slick.{lifted => sl} + +import scala.concurrent.{ExecutionContext, Future} +import scala.language.higherKinds + +trait Repository { + type T[D] + implicit def monadT: Monad[T] + + def execute[D](operations: T[D]): Future[D] + def noAction[V](v: V): T[V] + def customAction[R](action: => Future[R]): T[R] + + def customAction[R](action: => OptionT[Future, R]): OptionT[T, R] = + OptionT[T, R](customAction(action.run)) +} + +class FutureRepository(executionContext: ExecutionContext) extends Repository { + implicit val exec: ExecutionContext = executionContext + override type T[D] = Future[D] + implicit val monadT: Monad[Future] = implicitly[Monad[Future]] + + def execute[D](operations: T[D]): Future[D] = operations + def noAction[V](v: V): T[V] = Future.successful(v) + def customAction[R](action: => Future[R]): T[R] = action +} + +class SlickRepository(database: Database, executionContext: ExecutionContext) extends Repository { + import database.profile.api._ + implicit val exec: ExecutionContext = executionContext + + override type T[D] = slick.dbio.DBIO[D] + + implicit protected class QueryOps[+E, U](query: Query[E, U, Seq]) { + def resultT: ListT[T, U] = ListT[T, U](query.result.map(_.toList)) + + def maybeFilter[V, R: CanBeQueryCondition](data: Option[V])(f: V => E => R): sl.Query[E, U, Seq] = + data.map(v => query.withFilter(f(v))).getOrElse(query) + } + + implicit protected class CompiledQueryOps[U](compiledQuery: RunnableCompiled[_, Seq[U]]) { + def resultT: ListT[T, U] = ListT.listT[T](compiledQuery.result.map(_.toList)) + } + + private val dbioMonad = new Monad[T] { + override def point[A](a: => A): T[A] = DBIO.successful(a) + + override def bind[A, B](fa: T[A])(f: A => T[B]): T[B] = fa.flatMap(f) + } + + override implicit def monadT: Monad[T] = dbioMonad + + override def execute[D](readOperations: T[D]): Future[D] = { + database.database.run(readOperations.transactionally) + } + + override def noAction[V](v: V): T[V] = DBIO.successful(v) + + override def customAction[R](action: => Future[R]): T[R] = DBIO.from(action) + + def affectsRows(updatesCount: Int): Option[Unit] = { + if (updatesCount > 0) Some(()) else None + } + + def insertReturning[AT <: AbstractTable[_], V](table: TableQuery[AT])( + row: AT#TableElementType): slick.dbio.DBIO[AT#TableElementType] = { + table.returning(table) += row + } +} diff --git a/core-database/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala b/core-database/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala new file mode 100644 index 0000000..8293371 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala @@ -0,0 +1,30 @@ +package xyz.driver.core.database + +import slick.jdbc.GetResult +import xyz.driver.core.date.Date +import xyz.driver.core.time.Time +import xyz.driver.core.{Id, Name} + +trait SlickGetResultSupport { + implicit def GetId[U]: GetResult[Id[U]] = + GetResult(r => Id[U](r.nextString())) + implicit def GetIdOption[U]: GetResult[Option[Id[U]]] = + GetResult(_.nextStringOption().map(Id.apply[U])) + + implicit def GetName[U]: GetResult[Name[U]] = + GetResult(r => Name[U](r.nextString())) + implicit def GetNameOption[U]: GetResult[Option[Name[U]]] = + GetResult(_.nextStringOption().map(Name.apply[U])) + + implicit val GetTime: GetResult[Time] = + GetResult(r => Time(r.nextTimestamp.getTime)) + implicit val GetTimeOption: GetResult[Option[Time]] = + GetResult(_.nextTimestampOption().map(t => Time(t.getTime))) + + implicit val GetDate: GetResult[Date] = + GetResult(r => sqlDateToDate(r.nextDate())) + implicit val GetDateOption: GetResult[Option[Date]] = + GetResult(_.nextDateOption().map(sqlDateToDate)) +} + +object SlickGetResultSupport extends SlickGetResultSupport diff --git a/core-database/src/main/scala/xyz/driver/core/database/database.scala b/core-database/src/main/scala/xyz/driver/core/database/database.scala new file mode 100644 index 0000000..bd20b54 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/database.scala @@ -0,0 +1,178 @@ +package xyz.driver.core + +import slick.basic.DatabaseConfig +import slick.jdbc.JdbcProfile +import xyz.driver.core.date.Date +import xyz.driver.core.time.Time + +import scala.concurrent.Future +import com.typesafe.config.Config + +package database { + + import java.sql.SQLDataException + import java.time.{Instant, LocalDate} + + import eu.timepit.refined.api.{Refined, Validate} + import eu.timepit.refined.refineV + + trait Database { + val profile: JdbcProfile + val database: JdbcProfile#Backend#Database + } + + object Database { + def fromConfig(config: Config, databaseName: String): Database = { + val dbConfig: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(databaseName, config) + + new Database { + val profile: JdbcProfile = dbConfig.profile + val database: JdbcProfile#Backend#Database = dbConfig.db + } + } + + def fromConfig(databaseName: String): Database = { + fromConfig(com.typesafe.config.ConfigFactory.load(), databaseName) + } + } + + trait ColumnTypes { + val profile: JdbcProfile + } + + trait NameColumnTypes extends ColumnTypes { + import profile.api._ + implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] + } + + object NameColumnTypes { + trait StringName extends NameColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] = + MappedColumnType.base[Name[T], String](_.value, Name[T]) + } + } + + trait DateColumnTypes extends ColumnTypes { + import profile.api._ + implicit def `xyz.driver.core.time.Date.columnType`: BaseColumnType[Date] + implicit def `java.time.LocalDate.columnType`: BaseColumnType[LocalDate] + } + + object DateColumnTypes { + trait SqlDate extends DateColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.time.Date.columnType`: BaseColumnType[Date] = + MappedColumnType.base[Date, java.sql.Date](dateToSqlDate, sqlDateToDate) + + override implicit def `java.time.LocalDate.columnType`: BaseColumnType[LocalDate] = + MappedColumnType.base[LocalDate, java.sql.Date](java.sql.Date.valueOf, _.toLocalDate) + } + } + + trait RefinedColumnTypes[T, Predicate] extends ColumnTypes { + import profile.api._ + implicit def `eu.timepit.refined.api.Refined`( + implicit columnType: BaseColumnType[T], + validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate] + } + + object RefinedColumnTypes { + trait RefinedValue[T, Predicate] extends RefinedColumnTypes[T, Predicate] { + import profile.api._ + override implicit def `eu.timepit.refined.api.Refined`( + implicit columnType: BaseColumnType[T], + validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate] = + MappedColumnType.base[T Refined Predicate, T]( + _.value, { dbValue => + refineV[Predicate](dbValue) match { + case Left(refinementError) => + throw new SQLDataException( + s"Value in the database doesn't match the refinement constraints: $refinementError") + case Right(refinedValue) => + refinedValue + } + } + ) + } + } + + trait IdColumnTypes extends ColumnTypes { + import profile.api._ + implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] + } + + object IdColumnTypes { + trait UUID extends IdColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.Id.columnType`[T] = + MappedColumnType + .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) + } + trait SerialId extends IdColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.Id.columnType`[T] = + MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) + } + trait NaturalId extends IdColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.Id.columnType`[T] = + MappedColumnType.base[Id[T], String](_.value, Id[T]) + } + } + + trait TimestampColumnTypes extends ColumnTypes { + import profile.api._ + implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] + implicit def `java.time.Instant.columnType`: BaseColumnType[Instant] + } + + object TimestampColumnTypes { + trait SqlTimestamp extends TimestampColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = + MappedColumnType.base[Time, java.sql.Timestamp]( + time => new java.sql.Timestamp(time.millis), + timestamp => Time(timestamp.getTime)) + + override implicit def `java.time.Instant.columnType`: BaseColumnType[Instant] = + MappedColumnType.base[Instant, java.sql.Timestamp](java.sql.Timestamp.from, _.toInstant) + } + + trait PrimitiveTimestamp extends TimestampColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = + MappedColumnType.base[Time, Long](_.millis, Time.apply) + + override implicit def `java.time.Instant.columnType`: BaseColumnType[Instant] = + MappedColumnType.base[Instant, Long](_.toEpochMilli, Instant.ofEpochMilli) + } + } + + trait KeyMappers extends ColumnTypes { + import profile.api._ + + def uuidKeyMapper[T] = + MappedColumnType + .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) + def serialKeyMapper[T] = MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) + def naturalKeyMapper[T] = MappedColumnType.base[Id[T], String](_.value, Id[T]) + } + + trait DatabaseObject extends ColumnTypes { + def createTables(): Future[Unit] + def disconnect(): Unit + } + + abstract class DatabaseObjectAdapter extends DatabaseObject { + def createTables(): Future[Unit] = Future.successful(()) + def disconnect(): Unit = {} + } +} diff --git a/core-database/src/main/scala/xyz/driver/core/database/package.scala b/core-database/src/main/scala/xyz/driver/core/database/package.scala new file mode 100644 index 0000000..aee14c6 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/package.scala @@ -0,0 +1,61 @@ +package xyz.driver.core + +import java.sql.{Date => SqlDate} +import java.util.Calendar + +import date.{Date, Month} +import slick.dbio._ +import slick.jdbc.JdbcProfile +import slick.relational.RelationalProfile + +package object database { + + type Schema = { + def create: DBIOAction[Unit, NoStream, Effect.Schema] + def drop: DBIOAction[Unit, NoStream, Effect.Schema] + } + + @deprecated( + "sbt-slick-codegen 0.11.0+ no longer needs to generate these methods. Please use the new `CodegenTables` trait when upgrading.", + "driver-core 1.8.12") + type GeneratedTables = { + // structure of Slick data model traits generated by sbt-slick-codegen + val profile: JdbcProfile + def schema: profile.SchemaDescription + + def createNamespaceSchema: StreamingDBIO[Vector[Unit], Unit] + def dropNamespaceSchema: StreamingDBIO[Vector[Unit], Unit] + } + + /** A structural type for schema traits generated by sbt-slick-codegen. + * This will compile with codegen versions before 0.11.0, but note + * that methods in [[GeneratedTables]] are no longer generated. + */ + type CodegenTables[Profile <: RelationalProfile] = { + val profile: Profile + def schema: profile.SchemaDescription + } + + private[database] def sqlDateToDate(sqlDate: SqlDate): Date = { + // NOTE: SQL date does not have a time component, so this date + // should only be interpreted in the running JVMs timezone. + val cal = Calendar.getInstance() + cal.setTime(sqlDate) + Date(cal.get(Calendar.YEAR), Month(cal.get(Calendar.MONTH)), cal.get(Calendar.DAY_OF_MONTH)) + } + + private[database] def dateToSqlDate(date: Date): SqlDate = { + val cal = Calendar.getInstance() + cal.set(date.year, date.month, date.day, 0, 0, 0) + new SqlDate(cal.getTime.getTime) + } + + @deprecated("Dal is deprecated. Please use Repository trait instead!", "1.8.26") + type Dal = Repository + + @deprecated("SlickDal is deprecated. Please use SlickRepository class instead!", "1.8.26") + type SlickDal = SlickRepository + + @deprecated("FutureDal is deprecated. Please use FutureRepository class instead!", "1.8.26") + type FutureDal = FutureRepository +} diff --git a/core-database/src/test/scala/xyz/driver/core/database/DatabaseTest.scala b/core-database/src/test/scala/xyz/driver/core/database/DatabaseTest.scala new file mode 100644 index 0000000..8d2a4ac --- /dev/null +++ b/core-database/src/test/scala/xyz/driver/core/database/DatabaseTest.scala @@ -0,0 +1,42 @@ +package xyz.driver.core.database + +import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.prop.Checkers +import xyz.driver.core.rest.errors.DatabaseException + +class DatabaseTest extends FlatSpec with Matchers with Checkers { + import xyz.driver.core.generators._ + "Date SQL converter" should "correctly convert back and forth to SQL dates" in { + for (date <- 1 to 100 map (_ => nextDate())) { + sqlDateToDate(dateToSqlDate(date)) should be(date) + } + } + + "Converter helper methods" should "work correctly" in { + object TestConverter extends Converters + + val validLength = nextInt(10) + val valid = nextToken(validLength) + val validOp = Some(valid) + val invalid = nextToken(validLength + nextInt(10, 1)) + val invalidOp = Some(invalid) + def mapper(s: String): Option[String] = if (s.length == validLength) Some(s) else None + + TestConverter.fromStringOrThrow(valid, mapper, valid) should be(valid) + + TestConverter.expectValid(mapper, valid) should be(valid) + + TestConverter.expectExistsAndValid(mapper, validOp) should be(valid) + + TestConverter.expectValidOrEmpty(mapper, validOp) should be(Some(valid)) + TestConverter.expectValidOrEmpty(mapper, None) should be(None) + + an[DatabaseException] should be thrownBy TestConverter.fromStringOrThrow(invalid, mapper, invalid) + + an[DatabaseException] should be thrownBy TestConverter.expectValid(mapper, invalid) + + an[DatabaseException] should be thrownBy TestConverter.expectExistsAndValid(mapper, invalidOp) + + an[DatabaseException] should be thrownBy TestConverter.expectValidOrEmpty(mapper, invalidOp) + } +} diff --git a/core-rest/src/main/scala/xyz/driver/core/json.scala b/core-rest/src/main/scala/xyz/driver/core/json.scala index edc2347..40888aa 100644 --- a/core-rest/src/main/scala/xyz/driver/core/json.scala +++ b/core-rest/src/main/scala/xyz/driver/core/json.scala @@ -383,7 +383,6 @@ object json extends PathMatchers with Unmarshallers { case _: ResourceNotFoundException => "ResourceNotFoundException" case _: ExternalServiceException => "ExternalServiceException" case _: ExternalServiceTimeoutException => "ExternalServiceTimeoutException" - case _: DatabaseException => "DatabaseException" } { case "InvalidInputException" => jsonFormat(InvalidInputException, "message") case "InvalidActionException" => jsonFormat(InvalidActionException, "message") @@ -392,7 +391,6 @@ object json extends PathMatchers with Unmarshallers { case "ExternalServiceException" => jsonFormat(ExternalServiceException, "serviceName", "serviceMessage", "serviceException") case "ExternalServiceTimeoutException" => jsonFormat(ExternalServiceTimeoutException, "message") - case "DatabaseException" => jsonFormat(DatabaseException, "message") } } diff --git a/core-rest/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/core-rest/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index 911e306..b94f611 100644 --- a/core-rest/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/core-rest/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -88,9 +88,6 @@ trait DriverRoute { case e: ExternalServiceTimeoutException => log.error("Service timeout error", e) StatusCodes.GatewayTimeout - case e: DatabaseException => - log.error("Database error", e) - StatusCodes.InternalServerError } { (ctx: RequestContext) => diff --git a/core-rest/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala b/core-rest/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala index f2962c9..b43a1d3 100644 --- a/core-rest/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala +++ b/core-rest/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala @@ -22,6 +22,3 @@ final case class ExternalServiceException( final case class ExternalServiceTimeoutException(serviceName: String) extends ServiceException(s"$serviceName took too long to respond") - -final case class DatabaseException(override val message: String = "Database access error") - extends ServiceException(message) diff --git a/src/main/scala/xyz/driver/core/database/Converters.scala b/src/main/scala/xyz/driver/core/database/Converters.scala deleted file mode 100644 index ad79abf..0000000 --- a/src/main/scala/xyz/driver/core/database/Converters.scala +++ /dev/null @@ -1,26 +0,0 @@ -package xyz.driver.core.database - -import xyz.driver.core.rest.errors.DatabaseException - -import scala.reflect.ClassTag - -/** - * Helper methods for converting between table rows and Scala objects - */ -trait Converters { - def fromStringOrThrow[ADT](entityStr: String, mapper: (String => Option[ADT]), entityName: String): ADT = - mapper(entityStr).getOrElse(throw DatabaseException(s"Invalid $entityName in database: $entityStr")) - - def expectValid[ADT](mapper: String => Option[ADT], query: String)(implicit ct: ClassTag[ADT]): ADT = - fromStringOrThrow[ADT](query, mapper, ct.toString()) - - def expectExistsAndValid[ADT](mapper: String => Option[ADT], query: Option[String], contextMsg: String = "")( - implicit ct: ClassTag[ADT]): ADT = { - expectValid[ADT](mapper, query.getOrElse(throw DatabaseException(contextMsg))) - } - - def expectValidOrEmpty[ADT](mapper: String => Option[ADT], query: Option[String], contextMsg: String = "")( - implicit ct: ClassTag[ADT]): Option[ADT] = { - query.map(expectValid[ADT](mapper, _)) - } -} diff --git a/src/main/scala/xyz/driver/core/database/MdcAsyncExecutor.scala b/src/main/scala/xyz/driver/core/database/MdcAsyncExecutor.scala deleted file mode 100644 index 5939efb..0000000 --- a/src/main/scala/xyz/driver/core/database/MdcAsyncExecutor.scala +++ /dev/null @@ -1,53 +0,0 @@ -/** Code ported from "de.geekonaut" %% "slickmdc" % "1.0.0" - * License: @see https://github.com/AVGP/slickmdc/blob/master/LICENSE - * Blog post: @see http://50linesofco.de/post/2016-07-01-slick-and-slf4j-mdc-logging-in-scala.html - */ -package xyz.driver.core -package database - -import java.util.concurrent._ -import java.util.concurrent.atomic.AtomicInteger - -import scala.concurrent._ -import com.typesafe.scalalogging.StrictLogging -import slick.util.AsyncExecutor - -import logging.MdcExecutionContext - -/** Taken from the original Slick AsyncExecutor and simplified - * @see https://github.com/slick/slick/blob/3.1/slick/src/main/scala/slick/util/AsyncExecutor.scala - */ -object MdcAsyncExecutor extends StrictLogging { - - /** Create an AsyncExecutor with a fixed-size thread pool. - * - * @param name The name for the thread pool. - * @param numThreads The number of threads in the pool. - */ - def apply(name: String, numThreads: Int): AsyncExecutor = { - new AsyncExecutor { - val tf = new DaemonThreadFactory(name + "-") - - lazy val executionContext = { - new MdcExecutionContext(ExecutionContext.fromExecutor(Executors.newFixedThreadPool(numThreads, tf))) - } - - def close(): Unit = {} - } - } - - def default(name: String = "AsyncExecutor.default"): AsyncExecutor = apply(name, 20) - - private class DaemonThreadFactory(namePrefix: String) extends ThreadFactory { - private[this] val group = - Option(System.getSecurityManager).fold(Thread.currentThread.getThreadGroup)(_.getThreadGroup) - private[this] val threadNumber = new AtomicInteger(1) - - def newThread(r: Runnable): Thread = { - val t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement, 0) - if (!t.isDaemon) t.setDaemon(true) - if (t.getPriority != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY) - t - } - } -} diff --git a/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala b/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala deleted file mode 100644 index e2efd32..0000000 --- a/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala +++ /dev/null @@ -1,16 +0,0 @@ -package xyz.driver.core.database - -import slick.jdbc.{HsqldbProfile, JdbcType} -import slick.ast.FieldSymbol -import slick.relational.RelationalProfile - -trait PatchedHsqldbProfile extends HsqldbProfile { - override def defaultSqlTypeName(tmd: JdbcType[_], sym: Option[FieldSymbol]): String = tmd.sqlType match { - case java.sql.Types.VARCHAR => - val size = sym.flatMap(_.findColumnOption[RelationalProfile.ColumnOption.Length]) - size.fold("LONGVARCHAR")(l => if (l.varying) s"VARCHAR(${l.length})" else s"CHAR(${l.length})") - case _ => super.defaultSqlTypeName(tmd, sym) - } -} - -object PatchedHsqldbProfile extends PatchedHsqldbProfile diff --git a/src/main/scala/xyz/driver/core/database/Repository.scala b/src/main/scala/xyz/driver/core/database/Repository.scala deleted file mode 100644 index 5d7f787..0000000 --- a/src/main/scala/xyz/driver/core/database/Repository.scala +++ /dev/null @@ -1,74 +0,0 @@ -package xyz.driver.core.database - -import scalaz.std.scalaFuture._ -import scalaz.{ListT, Monad, OptionT} -import slick.lifted.{AbstractTable, CanBeQueryCondition, RunnableCompiled} -import slick.{lifted => sl} - -import scala.concurrent.{ExecutionContext, Future} -import scala.language.higherKinds - -trait Repository { - type T[D] - implicit def monadT: Monad[T] - - def execute[D](operations: T[D]): Future[D] - def noAction[V](v: V): T[V] - def customAction[R](action: => Future[R]): T[R] - - def customAction[R](action: => OptionT[Future, R]): OptionT[T, R] = - OptionT[T, R](customAction(action.run)) -} - -class FutureRepository(executionContext: ExecutionContext) extends Repository { - implicit val exec: ExecutionContext = executionContext - override type T[D] = Future[D] - implicit val monadT: Monad[Future] = implicitly[Monad[Future]] - - def execute[D](operations: T[D]): Future[D] = operations - def noAction[V](v: V): T[V] = Future.successful(v) - def customAction[R](action: => Future[R]): T[R] = action -} - -class SlickRepository(database: Database, executionContext: ExecutionContext) extends Repository { - import database.profile.api._ - implicit val exec: ExecutionContext = executionContext - - override type T[D] = slick.dbio.DBIO[D] - - implicit protected class QueryOps[+E, U](query: Query[E, U, Seq]) { - def resultT: ListT[T, U] = ListT[T, U](query.result.map(_.toList)) - - def maybeFilter[V, R: CanBeQueryCondition](data: Option[V])(f: V => E => R): sl.Query[E, U, Seq] = - data.map(v => query.withFilter(f(v))).getOrElse(query) - } - - implicit protected class CompiledQueryOps[U](compiledQuery: RunnableCompiled[_, Seq[U]]) { - def resultT: ListT[T, U] = ListT.listT[T](compiledQuery.result.map(_.toList)) - } - - private val dbioMonad = new Monad[T] { - override def point[A](a: => A): T[A] = DBIO.successful(a) - - override def bind[A, B](fa: T[A])(f: A => T[B]): T[B] = fa.flatMap(f) - } - - override implicit def monadT: Monad[T] = dbioMonad - - override def execute[D](readOperations: T[D]): Future[D] = { - database.database.run(readOperations.transactionally) - } - - override def noAction[V](v: V): T[V] = DBIO.successful(v) - - override def customAction[R](action: => Future[R]): T[R] = DBIO.from(action) - - def affectsRows(updatesCount: Int): Option[Unit] = { - if (updatesCount > 0) Some(()) else None - } - - def insertReturning[AT <: AbstractTable[_], V](table: TableQuery[AT])( - row: AT#TableElementType): slick.dbio.DBIO[AT#TableElementType] = { - table.returning(table) += row - } -} diff --git a/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala b/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala deleted file mode 100644 index 8293371..0000000 --- a/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala +++ /dev/null @@ -1,30 +0,0 @@ -package xyz.driver.core.database - -import slick.jdbc.GetResult -import xyz.driver.core.date.Date -import xyz.driver.core.time.Time -import xyz.driver.core.{Id, Name} - -trait SlickGetResultSupport { - implicit def GetId[U]: GetResult[Id[U]] = - GetResult(r => Id[U](r.nextString())) - implicit def GetIdOption[U]: GetResult[Option[Id[U]]] = - GetResult(_.nextStringOption().map(Id.apply[U])) - - implicit def GetName[U]: GetResult[Name[U]] = - GetResult(r => Name[U](r.nextString())) - implicit def GetNameOption[U]: GetResult[Option[Name[U]]] = - GetResult(_.nextStringOption().map(Name.apply[U])) - - implicit val GetTime: GetResult[Time] = - GetResult(r => Time(r.nextTimestamp.getTime)) - implicit val GetTimeOption: GetResult[Option[Time]] = - GetResult(_.nextTimestampOption().map(t => Time(t.getTime))) - - implicit val GetDate: GetResult[Date] = - GetResult(r => sqlDateToDate(r.nextDate())) - implicit val GetDateOption: GetResult[Option[Date]] = - GetResult(_.nextDateOption().map(sqlDateToDate)) -} - -object SlickGetResultSupport extends SlickGetResultSupport diff --git a/src/main/scala/xyz/driver/core/database/database.scala b/src/main/scala/xyz/driver/core/database/database.scala deleted file mode 100644 index bd20b54..0000000 --- a/src/main/scala/xyz/driver/core/database/database.scala +++ /dev/null @@ -1,178 +0,0 @@ -package xyz.driver.core - -import slick.basic.DatabaseConfig -import slick.jdbc.JdbcProfile -import xyz.driver.core.date.Date -import xyz.driver.core.time.Time - -import scala.concurrent.Future -import com.typesafe.config.Config - -package database { - - import java.sql.SQLDataException - import java.time.{Instant, LocalDate} - - import eu.timepit.refined.api.{Refined, Validate} - import eu.timepit.refined.refineV - - trait Database { - val profile: JdbcProfile - val database: JdbcProfile#Backend#Database - } - - object Database { - def fromConfig(config: Config, databaseName: String): Database = { - val dbConfig: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(databaseName, config) - - new Database { - val profile: JdbcProfile = dbConfig.profile - val database: JdbcProfile#Backend#Database = dbConfig.db - } - } - - def fromConfig(databaseName: String): Database = { - fromConfig(com.typesafe.config.ConfigFactory.load(), databaseName) - } - } - - trait ColumnTypes { - val profile: JdbcProfile - } - - trait NameColumnTypes extends ColumnTypes { - import profile.api._ - implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] - } - - object NameColumnTypes { - trait StringName extends NameColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] = - MappedColumnType.base[Name[T], String](_.value, Name[T]) - } - } - - trait DateColumnTypes extends ColumnTypes { - import profile.api._ - implicit def `xyz.driver.core.time.Date.columnType`: BaseColumnType[Date] - implicit def `java.time.LocalDate.columnType`: BaseColumnType[LocalDate] - } - - object DateColumnTypes { - trait SqlDate extends DateColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.time.Date.columnType`: BaseColumnType[Date] = - MappedColumnType.base[Date, java.sql.Date](dateToSqlDate, sqlDateToDate) - - override implicit def `java.time.LocalDate.columnType`: BaseColumnType[LocalDate] = - MappedColumnType.base[LocalDate, java.sql.Date](java.sql.Date.valueOf, _.toLocalDate) - } - } - - trait RefinedColumnTypes[T, Predicate] extends ColumnTypes { - import profile.api._ - implicit def `eu.timepit.refined.api.Refined`( - implicit columnType: BaseColumnType[T], - validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate] - } - - object RefinedColumnTypes { - trait RefinedValue[T, Predicate] extends RefinedColumnTypes[T, Predicate] { - import profile.api._ - override implicit def `eu.timepit.refined.api.Refined`( - implicit columnType: BaseColumnType[T], - validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate] = - MappedColumnType.base[T Refined Predicate, T]( - _.value, { dbValue => - refineV[Predicate](dbValue) match { - case Left(refinementError) => - throw new SQLDataException( - s"Value in the database doesn't match the refinement constraints: $refinementError") - case Right(refinedValue) => - refinedValue - } - } - ) - } - } - - trait IdColumnTypes extends ColumnTypes { - import profile.api._ - implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] - } - - object IdColumnTypes { - trait UUID extends IdColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType - .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) - } - trait SerialId extends IdColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) - } - trait NaturalId extends IdColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.Id.columnType`[T] = - MappedColumnType.base[Id[T], String](_.value, Id[T]) - } - } - - trait TimestampColumnTypes extends ColumnTypes { - import profile.api._ - implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] - implicit def `java.time.Instant.columnType`: BaseColumnType[Instant] - } - - object TimestampColumnTypes { - trait SqlTimestamp extends TimestampColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = - MappedColumnType.base[Time, java.sql.Timestamp]( - time => new java.sql.Timestamp(time.millis), - timestamp => Time(timestamp.getTime)) - - override implicit def `java.time.Instant.columnType`: BaseColumnType[Instant] = - MappedColumnType.base[Instant, java.sql.Timestamp](java.sql.Timestamp.from, _.toInstant) - } - - trait PrimitiveTimestamp extends TimestampColumnTypes { - import profile.api._ - - override implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = - MappedColumnType.base[Time, Long](_.millis, Time.apply) - - override implicit def `java.time.Instant.columnType`: BaseColumnType[Instant] = - MappedColumnType.base[Instant, Long](_.toEpochMilli, Instant.ofEpochMilli) - } - } - - trait KeyMappers extends ColumnTypes { - import profile.api._ - - def uuidKeyMapper[T] = - MappedColumnType - .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) - def serialKeyMapper[T] = MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) - def naturalKeyMapper[T] = MappedColumnType.base[Id[T], String](_.value, Id[T]) - } - - trait DatabaseObject extends ColumnTypes { - def createTables(): Future[Unit] - def disconnect(): Unit - } - - abstract class DatabaseObjectAdapter extends DatabaseObject { - def createTables(): Future[Unit] = Future.successful(()) - def disconnect(): Unit = {} - } -} diff --git a/src/main/scala/xyz/driver/core/database/package.scala b/src/main/scala/xyz/driver/core/database/package.scala deleted file mode 100644 index aee14c6..0000000 --- a/src/main/scala/xyz/driver/core/database/package.scala +++ /dev/null @@ -1,61 +0,0 @@ -package xyz.driver.core - -import java.sql.{Date => SqlDate} -import java.util.Calendar - -import date.{Date, Month} -import slick.dbio._ -import slick.jdbc.JdbcProfile -import slick.relational.RelationalProfile - -package object database { - - type Schema = { - def create: DBIOAction[Unit, NoStream, Effect.Schema] - def drop: DBIOAction[Unit, NoStream, Effect.Schema] - } - - @deprecated( - "sbt-slick-codegen 0.11.0+ no longer needs to generate these methods. Please use the new `CodegenTables` trait when upgrading.", - "driver-core 1.8.12") - type GeneratedTables = { - // structure of Slick data model traits generated by sbt-slick-codegen - val profile: JdbcProfile - def schema: profile.SchemaDescription - - def createNamespaceSchema: StreamingDBIO[Vector[Unit], Unit] - def dropNamespaceSchema: StreamingDBIO[Vector[Unit], Unit] - } - - /** A structural type for schema traits generated by sbt-slick-codegen. - * This will compile with codegen versions before 0.11.0, but note - * that methods in [[GeneratedTables]] are no longer generated. - */ - type CodegenTables[Profile <: RelationalProfile] = { - val profile: Profile - def schema: profile.SchemaDescription - } - - private[database] def sqlDateToDate(sqlDate: SqlDate): Date = { - // NOTE: SQL date does not have a time component, so this date - // should only be interpreted in the running JVMs timezone. - val cal = Calendar.getInstance() - cal.setTime(sqlDate) - Date(cal.get(Calendar.YEAR), Month(cal.get(Calendar.MONTH)), cal.get(Calendar.DAY_OF_MONTH)) - } - - private[database] def dateToSqlDate(date: Date): SqlDate = { - val cal = Calendar.getInstance() - cal.set(date.year, date.month, date.day, 0, 0, 0) - new SqlDate(cal.getTime.getTime) - } - - @deprecated("Dal is deprecated. Please use Repository trait instead!", "1.8.26") - type Dal = Repository - - @deprecated("SlickDal is deprecated. Please use SlickRepository class instead!", "1.8.26") - type SlickDal = SlickRepository - - @deprecated("FutureDal is deprecated. Please use FutureRepository class instead!", "1.8.26") - type FutureDal = FutureRepository -} diff --git a/src/test/scala/xyz/driver/core/database/DatabaseTest.scala b/src/test/scala/xyz/driver/core/database/DatabaseTest.scala deleted file mode 100644 index 8d2a4ac..0000000 --- a/src/test/scala/xyz/driver/core/database/DatabaseTest.scala +++ /dev/null @@ -1,42 +0,0 @@ -package xyz.driver.core.database - -import org.scalatest.{FlatSpec, Matchers} -import org.scalatest.prop.Checkers -import xyz.driver.core.rest.errors.DatabaseException - -class DatabaseTest extends FlatSpec with Matchers with Checkers { - import xyz.driver.core.generators._ - "Date SQL converter" should "correctly convert back and forth to SQL dates" in { - for (date <- 1 to 100 map (_ => nextDate())) { - sqlDateToDate(dateToSqlDate(date)) should be(date) - } - } - - "Converter helper methods" should "work correctly" in { - object TestConverter extends Converters - - val validLength = nextInt(10) - val valid = nextToken(validLength) - val validOp = Some(valid) - val invalid = nextToken(validLength + nextInt(10, 1)) - val invalidOp = Some(invalid) - def mapper(s: String): Option[String] = if (s.length == validLength) Some(s) else None - - TestConverter.fromStringOrThrow(valid, mapper, valid) should be(valid) - - TestConverter.expectValid(mapper, valid) should be(valid) - - TestConverter.expectExistsAndValid(mapper, validOp) should be(valid) - - TestConverter.expectValidOrEmpty(mapper, validOp) should be(Some(valid)) - TestConverter.expectValidOrEmpty(mapper, None) should be(None) - - an[DatabaseException] should be thrownBy TestConverter.fromStringOrThrow(invalid, mapper, invalid) - - an[DatabaseException] should be thrownBy TestConverter.expectValid(mapper, invalid) - - an[DatabaseException] should be thrownBy TestConverter.expectExistsAndValid(mapper, invalidOp) - - an[DatabaseException] should be thrownBy TestConverter.expectValidOrEmpty(mapper, invalidOp) - } -} -- cgit v1.2.3