diff --git a/unicorn-core/src/main/scala/org/virtuslab/unicorn/Unicorn.scala b/unicorn-core/src/main/scala/org/virtuslab/unicorn/Unicorn.scala index ad15965..d06b768 100644 --- a/unicorn-core/src/main/scala/org/virtuslab/unicorn/Unicorn.scala +++ b/unicorn-core/src/main/scala/org/virtuslab/unicorn/Unicorn.scala @@ -1,5 +1,6 @@ package org.virtuslab.unicorn +import org.virtuslab.unicorn.dsl.UnicornDSL import org.virtuslab.unicorn.repositories.Repositories import slick.jdbc.JdbcProfile @@ -16,6 +17,7 @@ trait HasJdbcProfile { */ trait Unicorn[Underlying] extends Tables[Underlying] - with Repositories[Underlying] { + with Repositories[Underlying] + with UnicornDSL[Underlying] { self: HasJdbcProfile => } diff --git a/unicorn-core/src/main/scala/org/virtuslab/unicorn/dsl/UnicornDSL.scala b/unicorn-core/src/main/scala/org/virtuslab/unicorn/dsl/UnicornDSL.scala new file mode 100644 index 0000000..50b5335 --- /dev/null +++ b/unicorn-core/src/main/scala/org/virtuslab/unicorn/dsl/UnicornDSL.scala @@ -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] + } +} diff --git a/unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/UsersRepositoryDSLTest.scala b/unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/UsersRepositoryDSLTest.scala new file mode 100644 index 0000000..ce162d4 --- /dev/null +++ b/unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/UsersRepositoryDSLTest.scala @@ -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 +} \ No newline at end of file diff --git a/unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/UsersRepositoryTest.scala b/unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/UsersRepositoryTest.scala index c7c98b4..e55ed4e 100644 --- a/unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/UsersRepositoryTest.scala +++ b/unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/UsersRepositoryTest.scala @@ -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] @@ -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") diff --git a/unicorn-play/src/test/scala/org/virtuslab/unicorn/UnicornPlayTests.scala b/unicorn-play/src/test/scala/org/virtuslab/unicorn/UnicornPlayTests.scala index a6090c3..f292aba 100644 --- a/unicorn-play/src/test/scala/org/virtuslab/unicorn/UnicornPlayTests.scala +++ b/unicorn-play/src/test/scala/org/virtuslab/unicorn/UnicornPlayTests.scala @@ -1,6 +1,8 @@ package org.virtuslab.unicorn -import org.virtuslab.unicorn.repositories.{ UsersRepositoryTest, AbstractUserTable } +import org.virtuslab.unicorn.repositories.DSLUserTable +import org.virtuslab.unicorn.repositories.UserRepositoryDSLTest +import org.virtuslab.unicorn.repositories.{ AbstractUserTable, UsersRepositoryTest } class UnicornPlayTests extends BasePlayTest @@ -8,3 +10,10 @@ class UnicornPlayTests with AbstractUserTable { override val identifiers: Identifiers[Long] = LongUnicornPlayIdentifiers } + +class UnicornPlayDSLTests + extends BasePlayTest + with UserRepositoryDSLTest + with DSLUserTable { + override val identifiers: Identifiers[Long] = LongUnicornPlayIdentifiers +} \ No newline at end of file