Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.virtuslab.unicorn

import org.virtuslab.unicorn.dsl.UnicornDSL
import org.virtuslab.unicorn.repositories.Repositories
import slick.jdbc.JdbcProfile

Expand All @@ -16,6 +17,7 @@ trait HasJdbcProfile {
*/
trait Unicorn[Underlying]
extends Tables[Underlying]
with Repositories[Underlying] {
with Repositories[Underlying]
with UnicornDSL[Underlying] {
self: HasJdbcProfile =>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.virtuslab.unicorn.dsl

import org.virtuslab.unicorn.BaseId
import org.virtuslab.unicorn.HasJdbcProfile
import org.virtuslab.unicorn.Identifiers
import org.virtuslab.unicorn.Unicorn
import org.virtuslab.unicorn.WithId
import slick.lifted.TableQuery

trait UnicornDSL[Underlying] { self: Unicorn[Underlying] with HasJdbcProfile =>
import profile.api.BaseColumnType

/**
* Basic DSL designed to not automate as much as possible in creating new DB entities
* This trait is basic version that wires all types where `EntityDsl` has pre-generated Id and IdCompanions.
*
* See `EntityDsl` docs for more information.
*/
trait EntityDSLBase {
/** Id type that will be use in this Entity */
type Id <: BaseId[Underlying]

/** Type of raw row of this entity. Intended to override by case class */
type Row <: BaseRow

/** Implantation detail. Wires Row with Id */
final type BaseRow = WithId[Underlying, Id]

/** Type that represents Table for this entity. Intended to be override with class. */
type Table <: BaseTable

/** Implantation detail. Wires Row and Id with Table */
final type BaseTable = IdTable[Id, Row]

/** Basic class for Repository in this entity */
class DslRepository(override val query: TableQuery[Table])(override implicit val mapping: BaseColumnType[Id])
extends BaseIdRepository[Id, Row, Table](query)

/** Repository for this entity. */
val Repository: DslRepository

/** Table query for this entity */
def query: TableQuery[Table] = Repository.query
}

/**
* This is basic class to create your Entity. Entity here means database table, row and dedicated basic repository
* together with helper classes such as unique Id type.
* Normally, you need to generate multiple classes/objects, remember to specify correct types etc.
* Generally a lot of boilerplate.
* Using Entity Dsl all you need is:
* 1. Create object that extends from `EntityDsl` with proper name (e.g. User, Invoice)
* 2. Create inside case class `Row` representing raw row of data with one filed called `id` of type `Option[Id]`
* 3. Create inside class Table extending `BaseTable` that represents your table (with Tag and table name).
* Inside Unicorn generate definition for `id` that needs to be added to `*` projection as `id.?`
* 4. Last thing is to implement value `Repository` with new instance of `DslRepository`.
*
* Minimal entity looks like this:
*
* ```
* object User extends EntityDsl(myProfile){
* case class Row(id: Option[Id], name: String)
* class Table(tag: Tag) extends BaseTable(tag, "Table_Name"){
* def name = column[String]("FIRST_NAME")
* override def * = (id.?, name).mapTo[Row]
* }
* override val Repository = new DslRepository(TableQuery[Table])
* }
* ```
* There still some boilerplate (e.g. `new DslRepository(TableQuery[Table])`, `.mapTo[Row]`)
* and we plan to elimitate also those in future.
*
* This approach changes slightly the way how you work with your entity. Instead of using e.g. `UserRow` or
* `UsersRepository` now `User.Row` and `User.Repository` should be used.
* Another benefit of this approach is that now you can abstract some pieces of code around entity such as schema
* creation.
*
* @param identifiers identifiers instance use together with you Unicorn instance.
*/
abstract class EntityDsl(val identifiers: Identifiers[Underlying]) extends EntityDSLBase {

/** Pre-generated Id for this entity */
case class Id(id: Underlying) extends BaseId[Underlying]

/** Pre-generated Id companion for this entity */
object Id extends identifiers.CoreCompanion[Id]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
package org.virtuslab.unicorn.repositories

import org.scalatest.FlatSpecLike
import org.scalatest.Matchers
import org.scalatest.OptionValues
import org.virtuslab.unicorn.WithId
import org.virtuslab.unicorn.BaseTest
import org.virtuslab.unicorn._

import scala.concurrent.ExecutionContext.Implicits.global

trait DSLUserTable {

val unicorn: Unicorn[Long] with HasJdbcProfile
val identifiers: Identifiers[Long]

import unicorn._
import unicorn.profile.api._

private def tableName = getClass.getSimpleName + "_DSL_USERS"

object User extends EntityDsl(identifiers) {
case class Row(id: Option[Id], email: String, firstName: String, lastName: String) extends WithId[Long, Id]

class Table(tag: Tag) extends BaseTable(tag, tableName) {
def email = column[String]("EMAIL")

def firstName = column[String]("FIRST_NAME")

def lastName = column[String]("LAST_NAME")

override def * = (id.?, email, firstName, lastName).mapTo[Row]
}

override val Repository = new DslRepository(TableQuery[Table])
}
}

trait UserRepositoryDSLTest extends OptionValues {
self: FlatSpecLike with Matchers with BaseTest[Long] with DSLUserTable =>

"Users Service" should "save and query users" in runWithRollback {
val user = User.Row(None, "test@email.com", "Krzysztof", "Nowak")

val actions = for {
_ <- User.Repository.create
userId <- User.Repository.save(user)
user <- User.Repository.findById(userId)
} yield user

actions map { userOpt =>
userOpt shouldBe defined

userOpt.value should have(
'email(user.email),
'firstName(user.firstName),
'lastName(user.lastName)
)
userOpt.value.id shouldBe defined
}
}

it should "save and query multiple users" in runWithRollback {
val users = (Stream from 1 take 10) map (n => User.Row(None, "test@email.com", "Krzysztof" + n, "Nowak"))

// setup
val actions = for {
_ <- User.Repository.create
_ <- User.Repository saveAll users
all <- User.Repository.findAll()
} yield all

actions map { newUsers =>
newUsers.size shouldEqual 10
newUsers.headOption map (_.firstName) shouldEqual Some("Krzysztof1")
newUsers.lastOption map (_.firstName) shouldEqual Some("Krzysztof10")
}
}

it should "query existing user" in runWithRollback {
val blankUser = User.Row(None, "test@email.com", "Krzysztof", "Nowak")

val actions = for {
_ <- User.Repository.create
userId <- User.Repository save blankUser
user <- User.Repository.findExistingById(userId)
} yield user

actions map { user2 =>
user2 should have(
'email(blankUser.email),
'firstName(blankUser.firstName),
'lastName(blankUser.lastName)
)
user2.id shouldBe defined
}
}

it should "update existing user" in runWithRollback {
val blankUser = User.Row(None, "test@email.com", "Krzysztof", "Nowak")

val actions = for {
_ <- User.Repository.create
userId <- User.Repository save blankUser
user <- User.Repository.findExistingById(userId)
_ <- User.Repository save user.copy(firstName = "Jerzy", lastName = "Muller")
updatedUser <- User.Repository.findExistingById(userId)
} yield (userId, updatedUser)

actions map {
case (userId, updatedUser) =>
updatedUser should have(
'email("test@email.com"),
'firstName("Jerzy"),
'lastName("Muller"),
'id(Some(userId))
)
}
}

it should "query all ids" in runWithRollback {
val users = Seq(
User.Row(None, "test1@email.com", "Krzysztof", "Nowak"),
User.Row(None, "test2@email.com", "Janek", "Nowak"),
User.Row(None, "test3@email.com", "Marcin", "Nowak")
)

val actions = for {
_ <- User.Repository.create
ids <- User.Repository saveAll users
allIds <- User.Repository.allIds()
} yield (ids, allIds)

actions map {
case (ids, allIds) =>
allIds shouldEqual ids
}
}

it should "sort users by id" in runWithRollback {
val users = Seq(
User.Row(None, "test1@email.com", "Krzysztof", "Nowak"),
User.Row(None, "test2@email.com", "Janek", "Nowak"),
User.Row(None, "test3@email.com", "Marcin", "Nowak")
)

val actions = for {
_ <- User.Repository.create
ids <- User.Repository saveAll users
users <- User.Repository.findAll()
} yield (ids, users)

actions map {
case (ids, users) =>
val usersWithIds = (users zip ids).map { case (user, id) => user.copy(id = Some(id)) }
users.sortBy(_.id) shouldEqual usersWithIds
}
}

it should "query multiple users by ids" in runWithRollback {
val users = Seq(
User.Row(None, "test1@email.com", "Krzysztof", "Nowak"),
User.Row(None, "test2@email.com", "Janek", "Nowak"),
User.Row(None, "test3@email.com", "Marcin", "Nowak")
)

val actions = for {
_ <- User.Repository.create
ids <- User.Repository saveAll users
allUsers <- User.Repository.findAll
selectedUsers: Seq[User.Row] = {
val usersWithIds = (users zip ids).map { case (user, id) => user.copy(id = Some(id)) }
Seq(usersWithIds.head, usersWithIds.last)
}
foundSelectedUsers <- User.Repository.findByIds(selectedUsers.flatMap(_.id))
} yield (allUsers, foundSelectedUsers, selectedUsers)

actions map {
case (allUsers, foundSelectedUsers, selectedUsers) =>
allUsers.size shouldEqual 3
foundSelectedUsers shouldEqual selectedUsers
}
}

it should "copy user by id" in runWithRollback {

val user = User.Row(None, "test1@email.com", "Krzysztof", "Nowak")

val actions = for {
_ <- User.Repository.create
id <- User.Repository.save(user)
idOfCopy <- User.Repository.copyAndSave(id)
copiedUser <- User.Repository.findById(idOfCopy)
} yield copiedUser.value

actions map { copiedUser =>
copiedUser.id shouldNot be(user.id)

copiedUser should have(
'email(user.email),
'firstName(user.firstName),
'lastName(user.lastName)
)
}
}

it should "delete user by id" in runWithRollback {
val users = Seq(
User.Row(None, "test1@email.com", "Krzysztof", "Nowak"),
User.Row(None, "test2@email.com", "Janek", "Nowak"),
User.Row(None, "test3@email.com", "Marcin", "Nowak")
)

val actions = for {
_ <- User.Repository.create
ids <- User.Repository saveAll users
initialUsers <- User.Repository.findAll
_ <- User.Repository.deleteById(ids(1))
resultingUsers <- User.Repository.findAll
} yield (ids, initialUsers, resultingUsers)

actions map {
case (ids, initialUsers, resultingUsers) =>
initialUsers should have size users.size
val usersWithIds = (users zip ids).map { case (user, id) => user.copy(id = Some(id)) }
val remainingUsers = Seq(usersWithIds.head, usersWithIds.last)
resultingUsers shouldEqual remainingUsers
}

}

it should "delete all users" in runWithRollback {
val users = Seq(
User.Row(None, "test1@email.com", "Krzysztof", "Nowak"),
User.Row(None, "test2@email.com", "Janek", "Nowak"),
User.Row(None, "test3@email.com", "Marcin", "Nowak")
)

val actions = for {
_ <- User.Repository.create
ids <- User.Repository saveAll users
initialUsers <- User.Repository.findAll
_ <- User.Repository.deleteAll()
resultingUsers <- User.Repository.findAll
} yield (initialUsers, resultingUsers)

actions map {
case (initialUsers, resultingUsers) =>
initialUsers should have size users.size
resultingUsers shouldBe empty
}
}

it should "create and drop table" in runWithRollback {
val actions = for {
_ <- User.Repository.create
_ <- User.Repository.drop
} yield ()

actions
}
}

class CoreUserDSLRepositoryTest extends BaseTest[Long] with UserRepositoryDSLTest with DSLUserTable {
override lazy val unicorn = TestUnicorn
override val identifiers: Identifiers[Long] = LongUnicornIdentifiers
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ trait AbstractUserTable {
import unicorn.profile.api._
import identifiers._

private def tableName = getClass.getSimpleName + "_USERS"

case class UserId(id: Long) extends BaseId[Long]

object UserId extends CoreCompanion[UserId]
Expand All @@ -25,7 +27,7 @@ trait AbstractUserTable {
lastName: String
) extends WithId[Long, UserId]

class Users(tag: Tag) extends IdTable[UserId, UserRow](tag, "USERS") {
class Users(tag: Tag) extends IdTable[UserId, UserRow](tag, tableName) {

def email = column[String]("EMAIL")

Expand Down
Loading