aboutsummaryrefslogtreecommitdiff
path: root/core-database/src
diff options
context:
space:
mode:
Diffstat (limited to 'core-database/src')
-rw-r--r--core-database/src/main/scala/xyz/driver/core/database/Converters.scala25
-rw-r--r--core-database/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala16
-rw-r--r--core-database/src/main/scala/xyz/driver/core/database/Repository.scala74
-rw-r--r--core-database/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala30
-rw-r--r--core-database/src/main/scala/xyz/driver/core/database/database.scala178
-rw-r--r--core-database/src/main/scala/xyz/driver/core/database/package.scala61
-rw-r--r--core-database/src/test/scala/xyz/driver/core/database/DatabaseTest.scala42
7 files changed, 426 insertions, 0 deletions
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)
+ }
+}