├── .gitattributes ├── .github └── workflows │ ├── release.yml │ └── scala.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── MIGRATION.md ├── README.md ├── build.sbt ├── project ├── Dependencies.scala ├── Settings.scala ├── build.properties └── plugins.sbt ├── scalastyle-config.xml ├── scalastyle-test-config.xml ├── unicorn-core └── src │ ├── main │ └── scala │ │ └── org │ │ └── virtuslab │ │ └── unicorn │ │ ├── Identifiers.scala │ │ ├── Tables.scala │ │ ├── TypeMappers.scala │ │ ├── Unicorn.scala │ │ ├── UnicornCore.scala │ │ └── repositories │ │ ├── IdRepositories.scala │ │ ├── JunctionRepositories.scala │ │ └── Repositories.scala │ └── test │ ├── resources │ └── logback.xml │ └── scala │ └── org │ └── virtuslab │ └── unicorn │ ├── BaseTest.scala │ ├── TestUnicorn.scala │ └── repositories │ ├── AlternativeIDTest.scala │ ├── DictionaryRepositoryTest.scala │ ├── JunctionRepositoryTest.scala │ ├── TypeMapperTest.scala │ └── UsersRepositoryTest.scala └── unicorn-play └── src ├── main └── scala │ └── org │ └── virtuslab │ └── unicorn │ ├── PlayIdentifiers.scala │ └── UnicornPlay.scala └── test └── scala └── org └── virtuslab └── unicorn ├── BasePlayTest.scala ├── PlayCompanionTest.scala ├── StringPlayUnicorn.scala └── UnicornPlayTests.scala /.gitattributes: -------------------------------------------------------------------------------- 1 | *.MF eol=lf 2 | *.sbt eol=lf 3 | *.sbt eol=lf 4 | *.scala eol=lf 5 | *.xml eol=lf 6 | *.yml eol=lf 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [master, main] 5 | tags: ["*"] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v2.3.4 11 | with: 12 | fetch-depth: 0 13 | - uses: olafurpg/setup-scala@v10 14 | - run: sbt ci-release 15 | env: 16 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 17 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 18 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 19 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 20 | -------------------------------------------------------------------------------- /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'adopt' 21 | - name: Run tests 22 | run: sbt +test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .bsp 4 | 5 | # sbt specific 6 | dist/* 7 | target/ 8 | lib_managed/ 9 | src_managed/ 10 | project/boot/ 11 | project/plugins/project/ 12 | 13 | # Scala-IDE specific 14 | .scala_dependencies 15 | 16 | # IDEA specific 17 | .idea/* 18 | .idea_modules/* -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | All kind of tips & tricks for people interested in contributing to unicorn. 5 | 6 | CI job 7 | ------ 8 | 9 | A Travis job is set up for `unicorn` [here](https://travis-ci.org/VirtusLab/unicorn). Each branch and PR is cross-tested against `Scala 2.10.4`, `Scala 2.11.5`, `OpenJDK 7` and `OpenJDK 8`. 10 | 11 | Code coverage 12 | ------------- 13 | 14 | Unicorn uses [scoverage](https://github.com/scoverage/scalac-scoverage-plugin) plugin for code coverage. To run it, use: 15 | 16 | ``` 17 | sbt clean coverage test 18 | ``` 19 | 20 | (`clean` *is important*) 21 | 22 | Results are placed in `unicorn\unicorn-core\target\scala-2.11\scoverage-report` and `unicorn\unicorn-play\target\scala-2.11\scoverage-report`. 23 | 24 | Minimum coverage is set for both projects, *100%* for `unicorn-play` and *98%* for `unicorn-core` (there are some DB screw-ups that are hard to test there), so all code you add to project *have to be 100% test-covered*, otherwise Travis build will fail. 25 | 26 | Releasing 27 | --------- 28 | 29 | Before release you must have access to Sonatype and have PGP keys for signing artifacts. 30 | 31 | To automate release process, `unicorn` uses [sbt-release](https://github.com/sbt/sbt-release) plugin. To release a new version, just use `sbt release` and follow instructions. For more information, see plugin docs. 32 | 33 | **Warn** - You should *not* update version file (`version.sbt`) yourself, `sbt-release` does it for you. 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Virtus Lab Sp. z o.o. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | Migration to 1.1.x 2 | ============================= 3 | Version 1.1.x removes `Play.current` dependency (deprecated in play 2.5). Database configuration is now resolved using DI. 4 | To fully use DI some major changes in class composition are required. 5 | Entity definition can be in separate file, but all classes that depends on Database Driver have to be mixed together. 6 | 7 | Changes in Entity ID class definition: 8 | Old way: 9 | ``` 10 | import LongUnicornPlay._ 11 | case class UserId(id: Long) extends BaseId 12 | ``` 13 | New way (explicit ID type): 14 | ``` 15 | import LongUnicornPlayIdentifiers._ 16 | case class UserId(id: Long) extends BaseId[Long] 17 | ``` 18 | 19 | Table definition: 20 | Old way (global import): 21 | ``` 22 | import LongPlayUnicorn._ 23 | import LongPlayUnicorn.driver.api._ 24 | 25 | class UserTable(tag: SlickTag) extends IdTable[UserId, User](tag, "test") { 26 | def name = column[String]("name") 27 | override def * : ProvenShape[User] = (id.?, name) <> (User.tupled, User.unapply) 28 | } 29 | 30 | class UserRepository extends BaseIdRepository[UserId, User, UserTable](TableQuery[UserTable]) 31 | 32 | ``` 33 | New way (Mixed Unicorn object): 34 | ``` 35 | trait UserRepositoryComponents { 36 | self: UnicornWrapper[Long] => 37 | 38 | import unicorn._ 39 | import unicorn.driver.api._ 40 | 41 | class UserTable(tag: SlickTag) extends IdTable[UserId, User](tag, "test") { 42 | def name = column[String]("name") 43 | override def * : ProvenShape[User] = (id.?, name) <> (User.tupled, User.unapply) 44 | } 45 | 46 | object UserBaseRepository extends BaseIdRepository[UserId, User, UserTable](TableQuery[UserTable]) 47 | } 48 | ``` 49 | 50 | To use Guice DI, you can define UserRepository like this: 51 | ``` 52 | @Singleton() 53 | class UserRepository @Inject() (val unicorn: LongUnicornPlayJDBC) 54 | extends UserRepositoryComponents with UnicornWrapper[Long] { 55 | import unicorn.driver.api._ 56 | def save(user: User): DBIO[UserId] = UserBaseRepository.save(user) 57 | } 58 | ``` 59 | and then inject `UserRepository` wherever you need it. 60 | 61 | Migration to 1.0.x 62 | ============================= 63 | 64 | Version 1.0.x brings Slick 3 new `DBIOAction` based API to Unicorn. We dropped `implicit session` parameter from method signatures but we had wrap return types in `DBIOAction`. We also added `implicit executionContext` in a few places. 65 | 66 | E.g. old `BaseIdRepository` methods: 67 | ``` 68 | def findById(id: Id)(implicit session: Session): Option[Entity] 69 | def findExistingById(id: Id)(implicit session: Session): Entity 70 | ``` 71 | now become 72 | ``` 73 | def findById(id: Id): DBIO[Option[Entity]] 74 | def findExistingById(id: Id)(implicit ec: ExecutionContext): DBIO[Entity] 75 | ``` 76 | To get to know more about `DBIOAction`s check out [Slick 3 documentation](http://slick.typesafe.com/docs/). 77 | 78 | 79 | Migration form 0.5.x to 0.6.x 80 | ============================= 81 | 82 | Version 0.6.x brings possibility for using different type then `Long` as underlying `Id` type. 83 | The most interesting are `UUID` and `String`. This change allow us to start working on typesafe composite keys. 84 | 85 | For backward compatibility with `0.5.x` we introduced `LongUnicornCore` and `LongUnicornPlay`. 86 | Before attempting to perform this migration you should known how to migrate your tables definitions to `slick-2.1.x`. 87 | All needed information you could find in awesome [migration guide](http://slick.typesafe.com/doc/2.1.0/upgrade.html#upgrade-from-2-0-to-2-1) 88 | 89 | 90 | Core migration 91 | -------------- 92 | 93 | Changes is only on backing your version on unicorn cake. So code like: 94 | ``` 95 | object Unicorn extends UnicornCore with HasJdbcDriver { 96 | val driver = H2Driver 97 | } 98 | ``` 99 | now becomes: 100 | ``` 101 | object Unicorn extends LongUnicornCore with HasJdbcDriver { 102 | val driver = H2Driver 103 | } 104 | ``` 105 | and this is all your changes. 106 | 107 | Play migration 108 | -------------- 109 | 110 | When you use `unicorn-play` migration is still quite easy, 111 | but it will touch all file where unicorn and slick was used. 112 | 113 | Imports like those: 114 | ``` 115 | import org.virtuslab.unicorn.UnicornPlay._ 116 | import org.virtuslab.unicorn.UnicornPlay.driver.simple._ 117 | ``` 118 | now becomes: 119 | ``` 120 | import org.virtuslab.unicorn.LongUnicornPlay._ 121 | import org.virtuslab.unicorn.LongUnicornPlay.driver.simple._ 122 | ``` 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scala Slick type-safe ids 2 | ========================= 3 | 4 | [![Join the chat at https://gitter.im/VirtusLab/unicorn](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/VirtusLab/unicorn?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | [![Coverage Status](https://coveralls.io/repos/github/VirtusLab/unicorn/badge.svg?branch=coveralls)](https://coveralls.io/github/VirtusLab/unicorn?branch=coveralls) 6 | 7 | Slick (the Scala Language-Integrated Connection Kit) is a framework for type-safe, composable data access in Scala. This library adds tools to use type-safe IDs for your classes so you can no longer join on bad id field or mess up order of fields in mappings. It also provides a way to create data access layer with methods (like querying all, querying by id, saving or deleting) for all classes with such IDs in just 4 lines of code. 8 | 9 | Idea for type-safe ids was derived from Slick creator's [presentation on ScalaDays 2013](http://www.parleys.com/play/51c2e20de4b0d38b54f46243/chapter63/about). 10 | 11 | This library is used in [Advanced play-slick Typesafe Activator template](https://github.com/VirtusLab/activator-play-advanced-slick). 12 | 13 | Unicorn is Open Source under [Apache 2.0 license](LICENSE). 14 | 15 | Contributors 16 | ------------ 17 | * [Jerzy Müller](https://github.com/Kwestor) 18 | * [Krzysztof Romanowski](https://github.com/romanowski) 19 | * [Łukasz Dubiel](https://github.com/bambuchaAdm) 20 | * [Matt Gilbert](https://github.com/mgilbertnz) 21 | * [Paweł Batko](https://github.com/pbatko) 22 | * [Krzysztof Borowski](https://github.com/liosedhel) 23 | * [Paweł Dolega](https://github.com/pdolega) 24 | 25 | Feel free to use it, test it and to contribute! For some helpful tips'n'tricks, see [contribution guide](CONTRIBUTING.md). 26 | 27 | Getting unicorn 28 | --------------- 29 | 30 | For core latest version (Scala 2.12.x and Slick 3.3.x) use: 31 | 32 | ```scala 33 | libraryDependencies += "org.virtuslab" %% "unicorn-core" % "1.3.3" 34 | ``` 35 | 36 | For play version (Scala 2.12.x, Slick 3.3.x, Play 2.7.x): 37 | 38 | ```scala 39 | libraryDependencies += "org.virtuslab" %% "unicorn-play" % "1.3.3" 40 | ``` 41 | 42 | Or see [our Maven repository](https://mvnrepository.com/artifact/org.virtuslab/). 43 | 44 | For Slick 3.3.x and play 2.7 see version [`1.3.3`](https://github.com/VirtusLab/unicorn/tree/v1.2.x-slick-3.2.x) 45 | 46 | For Slick 3.2.x and play 2.6 see version [`1.3.2`](https://github.com/VirtusLab/unicorn/tree/v1.2.x-slick-3.2.x) 47 | 48 | For Slick 3.2.x and play 2.5 see version [`1.2.x`](https://github.com/VirtusLab/unicorn/tree/v1.2.x-slick-3.2.x) 49 | 50 | For Slick 3.1.x and play 2.5 see version [`1.1.x`](https://github.com/VirtusLab/unicorn/tree/v1.1.x-slick-3.1.x) 51 | 52 | For Slick 3.1.x and play 2.4 see version [`1.0.x`](https://github.com/VirtusLab/unicorn/tree/v1.0.x-slick-3.1.x) 53 | 54 | For Slick 3.0.x see version [`0.7.x`](https://github.com/VirtusLab/unicorn/tree/v0.7.x-slick-3.0.x) 55 | 56 | For Slick 2.1.x see version [`0.6.x`](https://github.com/VirtusLab/unicorn/tree/v0.6.x-slick-2.1.x) 57 | 58 | For Slick 2.0.x see version [`0.5.x`](https://github.com/VirtusLab/unicorn/tree/v0.5.x-slick-2.0.x). 59 | 60 | For Slick 1.x see version [`0.4.x`](https://github.com/VirtusLab/unicorn/tree/v0.4.x-slick-1.0.x). 61 | 62 | Migration from older versions 63 | ============================= 64 | 65 | See our [migration guide](MIGRATION.md). 66 | 67 | Play Examples 68 | ============= 69 | 70 | From version 0.5.0 forward dependency on Play! framework and `play-slick` library is no longer necessary. 71 | 72 | If you are using Play! anyway, examples below show how to make use of `unicorn` then. 73 | 74 | Defining entities 75 | ----------------- 76 | 77 | ```scala 78 | package model 79 | 80 | import org.virtuslab.unicorn.{BaseId, WithId} 81 | import org.virtuslab.unicorn.LongUnicornPlayIdentifiers._ 82 | 83 | /** Id class for type-safe joins and queries. */ 84 | case class UserId(id: Long) extends AnyVal with BaseId[Long] 85 | 86 | /** Companion object for id class, extends IdCompanion 87 | * and brings all required implicits to scope when needed. 88 | */ 89 | object UserId extends IdCompanion[UserId] 90 | 91 | /** User entity. 92 | * 93 | * @param id user id 94 | * @param email user email address 95 | * @param lastName lastName 96 | * @param firstName firstName 97 | */ 98 | case class UserRow(id: Option[UserId], 99 | email: String, 100 | firstName: String, 101 | lastName: String) extends WithId[Long, UserId] 102 | ``` 103 | 104 | Defining composable repositories 105 | -------------------------------- 106 | 107 | ```scala 108 | package repositories 109 | /** 110 | * A place for all objects directly connected with database. 111 | * 112 | * Put your user queries here. 113 | * Having them in separate in this trait keeps `UserRepository` neat and tidy. 114 | */ 115 | trait UserBaseRepositoryComponent { 116 | self: UnicornWrapper[Long] => 117 | import unicorn._ 118 | import unicorn.driver.api._ 119 | 120 | 121 | /** Table definition for users. */ 122 | class Users(tag: Tag) extends IdTable[UserId, UserRow](tag, "USERS") { 123 | 124 | /** By definition id column is inserted as lowercase 'id', 125 | * if you want to change it, here is your setting. 126 | */ 127 | protected override val idColumnName = "ID" 128 | 129 | def email = column[String]("EMAIL") 130 | 131 | def firstName = column[String]("FIRST_NAME") 132 | 133 | def lastName = column[String]("LAST_NAME") 134 | 135 | override def * = (id.?, email, firstName, lastName) <> (UserRow.tupled, UserRow.unapply) 136 | 137 | } 138 | 139 | class UserBaseRepository 140 | extends BaseIdRepository[UserId, UserRow, Users](TableQuery[Users]) 141 | 142 | val userBaseRepository = new UserBaseRepository 143 | 144 | } 145 | @Singleton() 146 | class UserRepository @Inject() (val unicorn: LongUnicornPlayJDBC) 147 | extends UserBaseRepositoryComponent with UnicornWrapper[Long] { 148 | 149 | import unicorn.driver.api._ 150 | 151 | def save(user: UserRow): DBIO[UserId] = { 152 | userBaseRepository.save(user) 153 | } 154 | } 155 | ``` 156 | 157 | Usage 158 | ----- 159 | 160 | ```scala 161 | package repositories 162 | 163 | import model.UserRow 164 | 165 | import scala.concurrent.ExecutionContext.Implicits.global 166 | 167 | class UsersRepositoryTest extends BasePlayTest with UserBaseRepositoryComponent { 168 | 169 | "Users Repository" should "save and query users" in runWithRollback { 170 | 171 | val user = UserRow(None, "test@email.com", "Krzysztof", "Nowak") 172 | val action = for { 173 | _ <- userBaseRepository.create 174 | userId <- userBaseRepository.save(user) 175 | userOpt <- userBaseRepository.findById(userId) 176 | } yield userOpt 177 | 178 | action.map { userOpt => 179 | userOpt.map(_.email) shouldEqual Some(user.email) 180 | userOpt.map(_.firstName) shouldEqual Some(user.firstName) 181 | userOpt.map(_.lastName) shouldEqual Some(user.lastName) 182 | userOpt.flatMap(_.id) should not be (None) 183 | } 184 | } 185 | } 186 | ``` 187 | 188 | Core Examples 189 | ============= 190 | 191 | If you do not want to include Play! but still want to use unicorn, `unicorn-core` will make it available for you. 192 | 193 | Preparing Unicorn to work 194 | ------------------------- 195 | 196 | First you have to bake your own cake to provide `unicorn` with proper driver (in example case H2), 197 | as also build new object for Long ID support in entities: 198 | 199 | ```scala 200 | package infra 201 | 202 | object LongUnicornIdentifiers extends Identifiers[Long] { 203 | override def ordering: Ordering[Long] = implicitly[Ordering[Long]] 204 | 205 | override type IdCompanion[Id <: BaseId[Long]] = CoreCompanion[Id] 206 | } 207 | 208 | object Unicorn 209 | extends LongUnicornCore 210 | with HasJdbcDriver { 211 | 212 | override lazy val driver = H2Driver 213 | } 214 | ``` 215 | 216 | Then you can use that cake to import driver and types provided by `unicorn` as shown in next sections. 217 | 218 | Defining entities 219 | ----------------- 220 | 221 | ```scala 222 | package model 223 | 224 | import infra.LongUnicornIdentifiers._ 225 | import infra.Unicorn.driver.api._ 226 | import slick.lifted.Tag 227 | 228 | /** Id class for type-safe joins and queries. */ 229 | case class UserId(id: Long) extends AnyVal with BaseId[Long] 230 | 231 | /** Companion object for id class, extends IdMapping 232 | * and brings all required implicits to scope when needed. 233 | */ 234 | object UserId extends IdCompanion[UserId] 235 | 236 | /** User entity. */ 237 | case class UserRow(id: Option[UserId], 238 | email: String, 239 | firstName: String, 240 | lastName: String) extends WithId[Long, UserId] 241 | 242 | /** Table definition for users. */ 243 | class Users(tag: Tag) extends IdTable[UserId, UserRow](tag, "USERS") { 244 | 245 | // use this property if you want to change name of `id` column to uppercase 246 | // you need this on H2 for example 247 | override val idColumnName = "ID" 248 | 249 | def email = column[String]("EMAIL") 250 | 251 | def firstName = column[String]("FIRST_NAME") 252 | 253 | def lastName = column[String]("LAST_NAME") 254 | 255 | override def * = (id.?, email, firstName, lastName) <> (UserRow.tupled, UserRow.unapply) 256 | } 257 | ``` 258 | 259 | Defining repositories 260 | --------------------- 261 | 262 | ```scala 263 | package repositories 264 | 265 | import infra.Unicorn._ 266 | import infra.Unicorn.driver.api._ 267 | import model._ 268 | 269 | /** 270 | * Repository for users. 271 | * 272 | * It brings all base repository methods with it from [[BaseIdRepository]], but you can add yours as well. 273 | * 274 | * Use your favourite DI method to instantiate it in your application. 275 | */ 276 | class UsersRepository extends BaseIdRepository[UserId, UserRow, Users](TableQuery[Users]) 277 | ``` 278 | 279 | Usage 280 | ----- 281 | 282 | ```scala 283 | package repositories 284 | 285 | import model.UserRow 286 | import scala.concurrent.ExecutionContext.Implicits.global 287 | 288 | 289 | class UsersRepositoryTest extends BaseTest[Long] { 290 | 291 | val usersRepository: UsersRepository = new UsersRepository 292 | 293 | "Users Service" should "save and query users" in runWithRollback { 294 | val user = UserRow(None, "test@email.com", "Krzysztof", "Nowak") 295 | 296 | val actions = for { 297 | _ <- usersRepository.create 298 | userId <- usersRepository.save(user) 299 | user <- usersRepository.findById(userId) 300 | } yield user 301 | 302 | actions map { userOpt => 303 | userOpt shouldBe defined 304 | 305 | userOpt.value should have( 306 | 'email(user.email), 307 | 'firstName(user.firstName), 308 | 'lastName(user.lastName) 309 | ) 310 | userOpt.value.id shouldBe defined 311 | } 312 | } 313 | } 314 | ``` 315 | 316 | Defining custom underlying type 317 | =============================== 318 | 319 | All reviews examples used `Long` as underlying `Id` type. From version `0.6.0` there is possibility to define own. 320 | 321 | Let's use `String` as our type for `id`. So we should bake unicorn with `String` parametrization. 322 | 323 | Play example 324 | ------------ 325 | ```scala 326 | @Singleton() 327 | class StringUnicornPlay @Inject() (databaseConfigProvider: DatabaseConfigProvider) 328 | extends UnicornPlay[String](databaseConfigProvider.get[JdbcProfile]) 329 | 330 | 331 | object StringUnicornPlayIdentifiers extends PlayIdentifiersImpl[String] { 332 | override val ordering: Ordering[String] = implicitly[Ordering[String]] 333 | override type IdCompanion[Id <: BaseId[String]] = PlayCompanion[Id] 334 | } 335 | ``` 336 | 337 | Core example 338 | ------------ 339 | ```scala 340 | object StringUnicornIdentifiers extends Identifiers[String] { 341 | override def ordering: Ordering[String] = implicitly[Ordering[String]] 342 | 343 | override type IdCompanion[Id <: BaseId[Long]] = CoreCompanion[Id] 344 | } 345 | 346 | object Unicorn 347 | extends UnicornCore[String] 348 | with HasJdbcDriver { 349 | 350 | override lazy val driver = H2Driver 351 | } 352 | ``` 353 | 354 | Usage is same as in `Long` example. Main difference is that you should import classes from self-baked cake. 355 | The only concern is that `id` is auto-increment so we can't use arbitrary type there. 356 | We plan to solve this problem in next versions. 357 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | inThisBuild(List( 2 | organization := "org.virtuslab", 3 | homepage := Some(url("https://github.com/VirtusLab/unicorn")), 4 | licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), 5 | developers := List( 6 | Developer( 7 | "jsroka", 8 | "jsroka", 9 | "jsroka@virtuslab.com", 10 | url("https://virtuslab.com/") 11 | ) 12 | ) 13 | )) 14 | 15 | val `unicorn-core` = project 16 | .settings(Settings.core: _*) 17 | .settings( 18 | libraryDependencies ++= Dependencies.core, 19 | // cannot be higher due to tests not able to reproduce abnormal DB behavior 20 | coverageMinimum := 100, 21 | (scalastyleConfig in Test) := file("scalastyle-test-config.xml") 22 | ) 23 | 24 | val `unicorn-play` = project 25 | .settings(Settings.play: _*) 26 | .settings( 27 | libraryDependencies ++= Dependencies.core, 28 | libraryDependencies ++= Dependencies.play, 29 | coverageMinimum := 100, 30 | (scalastyleConfig in Test) := file("scalastyle-test-config.xml") 31 | ) 32 | .dependsOn(`unicorn-core` % Settings.alsoOnTest) 33 | 34 | val unicorn = project 35 | .in(file(".")) 36 | .aggregate(`unicorn-core`, `unicorn-play`) 37 | .dependsOn(`unicorn-core`, `unicorn-play`) 38 | .settings(Settings.parent: _*) 39 | .enablePlugins(ScalaUnidocPlugin) 40 | .settings( 41 | name := "unicorn" 42 | ) -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | val mainCore: Seq[ModuleID] = Seq( 6 | "com.typesafe.slick" %% "slick" % "3.4.1", 7 | "joda-time" % "joda-time" % "2.12.5", 8 | "org.joda" % "joda-convert" % "2.2.3" 9 | ) 10 | 11 | val testCore = Seq( 12 | "org.scalatest" %% "scalatest" % "3.2.16" % "test", 13 | "com.h2database" % "h2" % "2.2.220" % "test", 14 | "ch.qos.logback" % "logback-classic" % "1.4.8" % "test" 15 | ) 16 | 17 | val core: Seq[ModuleID] = mainCore ++ testCore 18 | 19 | val mainPlay = Seq( 20 | "com.typesafe.play" %% "play-slick" % "5.1.0" 21 | ) 22 | 23 | val testPlay = Seq( 24 | "org.scalatestplus.play" %% "scalatestplus-play" % "5.1.0" % Test, 25 | "com.typesafe.play" %% "play-test" % "2.8.20" % "test" 26 | ) 27 | 28 | val play = mainPlay ++ testPlay 29 | } 30 | -------------------------------------------------------------------------------- /project/Settings.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | import sbtrelease.ReleasePlugin.autoImport._ 4 | 5 | object Settings { 6 | 7 | val scala_2_12 = "2.12.18" 8 | val scala_2_13 = "2.13.11" 9 | 10 | val alsoOnTest = "compile->compile;test->test" 11 | 12 | // settings for ALL modules, including parent 13 | val common = Seq( 14 | organization := "org.virtuslab", 15 | scalaVersion := scala_2_13, 16 | crossScalaVersions := Seq(scala_2_12, scala_2_13), 17 | releaseCrossBuild := true, 18 | 19 | fork in Test := true, 20 | parallelExecution in Test := false, 21 | testOptions in Test += Tests.Argument("-oDF"), 22 | autoAPIMappings := true 23 | ) 24 | 25 | val core = common 26 | 27 | val play = common 28 | 29 | // common settings for play and core modules 30 | val parent = common ++ Seq( 31 | resolvers += Resolver.typesafeRepo("releases"), 32 | resolvers += Resolver.sonatypeRepo("releases"), 33 | resolvers += Resolver.sonatypeRepo("snapshots"), 34 | scalacOptions ++= Seq( 35 | "-feature", 36 | "-deprecation", 37 | "-unchecked", 38 | "-Xlint", 39 | "-Xfatal-warnings" 40 | ), 41 | updateOptions := updateOptions.value.withCachedResolution(true), 42 | scoverage.ScoverageKeys.coverageFailOnMinimum := true 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | /* ************ */ 2 | /* Code quality */ 3 | 4 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.2") 5 | 6 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") 7 | 8 | /* ------------------ */ 9 | /* Deploy and release */ 10 | 11 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.8") 12 | 13 | addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.1") 14 | 15 | addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") 16 | 17 | /* ------------- */ 18 | /* Code coverage */ 19 | 20 | resolvers += Classpaths.sbtPluginReleases 21 | 22 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0") 23 | 24 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.4") 25 | 26 | 27 | -------------------------------------------------------------------------------- /scalastyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | Scalastyle standard configuration 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /scalastyle-test-config.xml: -------------------------------------------------------------------------------- 1 | 2 | Scalastyle standard configuration 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /unicorn-core/src/main/scala/org/virtuslab/unicorn/Identifiers.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import slick.lifted.MappedTo 4 | 5 | /** 6 | * Base trait for implementing ids. 7 | * It is existential trait so it can have only defs. 8 | */ 9 | trait BaseId[U] extends Any with MappedTo[U] { 10 | def id: Underlying 11 | override def value: Underlying = id 12 | } 13 | 14 | /** 15 | * Base class for all entities that contains an id. 16 | * @tparam Id type of Id 17 | */ 18 | trait WithId[Underlying, Id <: BaseId[Underlying]] { 19 | 20 | /** @return id of entity (optional, entities does not have ids before save) */ 21 | def id: Option[Id] 22 | } 23 | 24 | trait Identifiers[Underlying] { 25 | 26 | def ordering: Ordering[Underlying] 27 | 28 | import scala.language.higherKinds 29 | 30 | type IdCompanion[Id <: BaseId[Underlying]] 31 | 32 | /** 33 | * Base class for companion objects for id classes. 34 | * Adding this will allow you not to import mapping from your table class every time you need it. 35 | * 36 | * @tparam Id type of Id 37 | */ 38 | abstract class CoreCompanion[Id <: BaseId[Underlying]] { 39 | 40 | /** Ordering for ids */ 41 | implicit val basicOrdering = Ordering.by[Id, Id#Underlying](_.value)(ordering) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /unicorn-core/src/main/scala/org/virtuslab/unicorn/Tables.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import slick.lifted.Index 4 | import slick.lifted.ProvenShape 5 | 6 | protected[unicorn] trait Tables[Underlying] extends TypeMappers { 7 | self: HasJdbcProfile => 8 | 9 | import profile.api._ 10 | 11 | /** 12 | * Base class for all tables that contains an id. 13 | * 14 | * @param schemaName name of schema (optional) 15 | * @param tableName name of the table 16 | * @param mapping mapping for id of this table 17 | * @tparam Id type of id 18 | * @tparam Entity type of entities in table 19 | */ 20 | abstract class IdTable[Id <: BaseId[Underlying], Entity <: WithId[Underlying, Id]](tag: Tag, schemaName: Option[String], tableName: String)(implicit val mapping: BaseColumnType[Id]) 21 | extends BaseTable[Entity](tag, schemaName, tableName) { 22 | 23 | /** 24 | * Auxiliary constructor without schema name. 25 | * @param tableName name of table 26 | */ 27 | def this(tag: Tag, tableName: String)(implicit mapping: BaseColumnType[Id]) = this(tag, None, tableName) 28 | 29 | /** 30 | * Name of an `id` column - override it if you want to change it. 31 | * 32 | * For example in H2DB where you need an uppercase "ID": 33 | * 34 | * {{{ 35 | * override val idColumnName = "ID" 36 | * }}} 37 | */ 38 | protected val idColumnName: String = "id" 39 | 40 | /** @return id column representation of this table */ 41 | def id: Rep[Id] = column[Id](idColumnName, O.PrimaryKey, O.AutoInc) 42 | } 43 | 44 | /** 45 | * Base trait for all tables. If you want to add some helpers methods for tables, here is the place. 46 | * 47 | * @param schemaName name of schema (optional) 48 | * @param tableName name of the table 49 | * @tparam Entity type of entities in table 50 | */ 51 | abstract class BaseTable[Entity](tag: Tag, schemaName: Option[String], tableName: String) 52 | extends Table[Entity](tag, schemaName, tableName) 53 | with CustomTypeMappers { 54 | 55 | /** 56 | * Auxiliary constructor without schema name. 57 | * @param tableName name of table 58 | */ 59 | def this(tag: Tag, tableName: String) = this(tag, None, tableName) 60 | } 61 | 62 | /** 63 | * Base table for simple linking between two values 64 | * 65 | * @param schemaName name of schema (optional) 66 | * @param tableName name of the table 67 | * @tparam First type of one entity 68 | * @tparam Second type of other entity 69 | */ 70 | abstract class JunctionTable[First: BaseColumnType, Second: BaseColumnType](tag: Tag, schemaName: Option[String], tableName: String) 71 | extends Table[(First, Second)](tag, schemaName, tableName) { 72 | 73 | /** 74 | * Auxiliary constructor without schema name. 75 | * @param tableName name of table 76 | */ 77 | def this(tag: Tag, tableName: String) = this(tag, None, tableName) 78 | 79 | /** 80 | * instead of def * = colA ~ colB write def columns = colA -> colB 81 | * @return 82 | */ 83 | def columns: (Rep[First], Rep[Second]) 84 | 85 | final def * : ProvenShape[(First, Second)] = (columns._1, columns._2) 86 | 87 | final def uniqueValues: Index = index(s"${tableName}_uniq_idx", *, unique = true) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /unicorn-core/src/main/scala/org/virtuslab/unicorn/TypeMappers.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import java.sql.{ Date, Timestamp } 4 | 5 | import org.joda.time.{ DateTime, Duration, LocalDate } 6 | 7 | trait TypeMappers { 8 | self: HasJdbcProfile => 9 | 10 | import profile.api._ 11 | 12 | /** 13 | * Custom Type mappers for Slick. 14 | */ 15 | trait CustomTypeMappers { 16 | 17 | /** Type mapper for `org.joda.time.DateTime` */ 18 | implicit val dateTimeMapper: BaseColumnType[DateTime] = MappedColumnType.base[DateTime, Timestamp]( 19 | dt => new Timestamp(dt.getMillis), 20 | ts => new DateTime(ts.getTime)) 21 | 22 | /** Type mapper for `org.joda.time.LocalDate` */ 23 | implicit val localDateMapper: BaseColumnType[LocalDate] = MappedColumnType.base[LocalDate, Date]( 24 | dt => new Date(dt.toDate.getTime), 25 | d => new LocalDate(d.getTime)) 26 | 27 | /** Type mapper for `org.joda.time.Duration` */ 28 | implicit val durationTypeMapper: BaseColumnType[Duration] = MappedColumnType.base[Duration, Long]( 29 | d => d.getMillis, 30 | l => Duration.millis(l)) 31 | } 32 | 33 | /** Object for [[org.virtuslab.unicorn.TypeMappers.CustomTypeMappers]] if you prefer import rather than extend. */ 34 | object CustomTypeMappers extends CustomTypeMappers 35 | 36 | } 37 | -------------------------------------------------------------------------------- /unicorn-core/src/main/scala/org/virtuslab/unicorn/Unicorn.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import org.virtuslab.unicorn.repositories.Repositories 4 | import slick.jdbc.JdbcProfile 5 | 6 | trait HasJdbcProfile { 7 | 8 | val profile: JdbcProfile 9 | 10 | } 11 | 12 | /** 13 | * Base cake for Unicorn. Extended by versions for `unicorn-core` and `unicorn-play`. 14 | */ 15 | trait Unicorn[Underlying] 16 | extends Tables[Underlying] 17 | with Repositories[Underlying] { 18 | self: HasJdbcProfile => 19 | } 20 | -------------------------------------------------------------------------------- /unicorn-core/src/main/scala/org/virtuslab/unicorn/UnicornCore.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | /** 4 | * Cake for unicorn-core. 5 | */ 6 | trait UnicornCoreLike[Underlying] extends Unicorn[Underlying] { 7 | self: HasJdbcProfile => 8 | } 9 | 10 | abstract class UnicornCore[Underlying](implicit val ordering: Ordering[Underlying]) 11 | extends UnicornCoreLike[Underlying] { 12 | self: HasJdbcProfile => 13 | } 14 | 15 | trait LongUnicornCore extends UnicornCore[Long] { 16 | self: HasJdbcProfile => 17 | } 18 | -------------------------------------------------------------------------------- /unicorn-core/src/main/scala/org/virtuslab/unicorn/repositories/IdRepositories.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn.repositories 2 | 3 | import java.sql.SQLException 4 | 5 | import org.virtuslab.unicorn._ 6 | 7 | import scala.concurrent.ExecutionContext 8 | 9 | protected[unicorn] trait IdRepositories[Underlying] { 10 | self: HasJdbcProfile with Tables[Underlying] with Repositories[Underlying] => 11 | 12 | import profile.api.{ Table => _, _ } 13 | /** 14 | * Base class for all queries with an [[org.virtuslab.unicorn.BaseId]]. 15 | * 16 | * @tparam Id type of id 17 | * @tparam Entity type of elements that are queried 18 | * @tparam Table type of table 19 | */ 20 | protected trait BaseIdQueries[Id <: BaseId[Underlying], Entity <: WithId[Underlying, Id], Table <: IdTable[Id, Entity]] { 21 | 22 | /** @return query to operate on */ 23 | protected def query: TableQuery[Table] 24 | 25 | /** @return type mapper for I, required for querying */ 26 | protected implicit def mapping: BaseColumnType[Id] 27 | 28 | val byIdQuery = Compiled(byIdFunc _) 29 | 30 | /** Query all ids. */ 31 | protected lazy val allIdsQuery = query.map(_.id) 32 | 33 | /** Query element by id, method version. */ 34 | protected def byIdFunc(id: Rep[Id]) = query.filter(_.id === id) 35 | 36 | /** Query by multiple ids. */ 37 | protected def byIdsQuery(ids: Seq[Id]) = query.filter(_.id inSet ids) 38 | } 39 | 40 | /** 41 | * Base trait for repositories where we use [[org.virtuslab.unicorn.BaseId]]s. 42 | * 43 | * @tparam Id type of id 44 | * @tparam Entity type of entity 45 | * @tparam Table type of table 46 | */ 47 | // format: OFF 48 | class BaseIdRepository[Id <: BaseId[Underlying], Entity <: WithId[Underlying, Id], Table <: IdTable[Id, Entity]](protected val query: TableQuery[Table]) 49 | (implicit val mapping: BaseColumnType[Id]) 50 | extends CommonRepositoryMethods[Entity, Table](query) 51 | with BaseIdQueries[Id, Entity, Table] { 52 | // format: ON 53 | 54 | protected def queryReturningId = query returning query.map(_.id) 55 | 56 | final val tableName = query.baseTableRow.tableName 57 | 58 | /** 59 | * Finds one element by id. 60 | * 61 | * @param id id of element 62 | * @return Option(element) 63 | */ 64 | def findById(id: Id): DBIO[Option[Entity]] = byIdQuery(id).result.headOption 65 | 66 | /** 67 | * Clones element by id. 68 | * 69 | * @param id id of element to clone 70 | * @return Option(id) of new element 71 | */ 72 | def copyAndSave(id: Id)(implicit ec: ExecutionContext): DBIO[Id] = 73 | for { 74 | elem <- findById(id) 75 | result <- elem match { 76 | case None => DBIO.failed(new NoSuchElementException(s"Element with $id doesn't exist")) 77 | case Some(e) => queryReturningId += e 78 | } 79 | } yield result 80 | 81 | /** 82 | * Finds one element by id. 83 | * 84 | * @param id id of element 85 | * @return element 86 | */ 87 | def findExistingById(id: Id)(implicit ec: ExecutionContext): DBIO[Entity] = 88 | findById(id).map { 89 | case Some(elem) => elem 90 | case None => throw new NoSuchFieldException(s"Element with id: $id in table: $tableName does not exist") 91 | } 92 | 93 | /** 94 | * Finds elements by given ids. 95 | * 96 | * @param ids ids of element 97 | * @return Seq(element) 98 | */ 99 | def findByIds(ids: Seq[Id]): DBIO[Seq[Entity]] = byIdsQuery(ids).result 100 | 101 | /** 102 | * Deletes one element by id. 103 | * 104 | * @param id id of element 105 | * @return number of deleted elements (0 or 1) 106 | */ 107 | def deleteById(id: Id): DBIO[Int] = byIdQuery(id).delete 108 | 109 | /** 110 | * @return Sequence of ids 111 | */ 112 | def allIds(): DBIO[Seq[Id]] = allIdsQuery.result 113 | 114 | /** 115 | * Saves one element. 116 | * 117 | * @param elem element to save 118 | * @return Option(elementId) 119 | */ 120 | def save(elem: Entity)(implicit ec: ExecutionContext): DBIO[Id] = { 121 | elem.id match { 122 | case Some(id) => 123 | val updateAction = byIdFunc(id).update(elem) 124 | updateAction.map { rowsUpdated => 125 | afterSave(elem) 126 | if (rowsUpdated == 1) id 127 | else throw new SQLException(s"Error during save in table: $tableName, " + 128 | s"for id: $id - $rowsUpdated rows updated, expected: 1. Entity: $elem") 129 | } 130 | case None => 131 | val result = queryReturningId += elem 132 | result.map { rowsInserted => 133 | afterSave(elem) 134 | rowsInserted 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Hook executed after element is saved - if you want to do some stuff then, override it. 141 | * 142 | * @param elem element to save 143 | */ 144 | protected def afterSave(elem: Entity): Unit = {} 145 | 146 | /** 147 | * Saves multiple elements. 148 | * 149 | * @param elems elements to save 150 | * @return Sequence of ids 151 | */ 152 | def saveAll(elems: Seq[Entity])(implicit ec: ExecutionContext): DBIO[Seq[Id]] = { 153 | // conversion is required to force lazy collections 154 | val actions = elems.toIndexedSeq map save 155 | 156 | DBIO.sequence(actions) 157 | } 158 | 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /unicorn-core/src/main/scala/org/virtuslab/unicorn/repositories/JunctionRepositories.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn.repositories 2 | 3 | import org.virtuslab.unicorn.{ HasJdbcProfile, Tables } 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | protected[unicorn] trait JunctionRepositories[Underlying] { 8 | self: HasJdbcProfile with Tables[Underlying] with Repositories[Underlying] => 9 | 10 | import profile.api.{ Table => _, _ } 11 | 12 | /** 13 | * Repository with basic methods for junction tables. 14 | * @tparam First type of one entity 15 | * @tparam Second type of other entity 16 | */ 17 | class JunctionRepository[First: BaseColumnType, Second: BaseColumnType, Table <: JunctionTable[First, Second]](val query: TableQuery[Table]) 18 | extends CommonRepositoryMethods[(First, Second), Table](query) { 19 | 20 | protected def findOneQueryFun(first: Rep[First], second: Rep[Second]) = 21 | query.filter(row => row.columns._1 === first && row.columns._2 === second) 22 | 23 | protected val findOneQueryCompiled = Compiled(findOneQueryFun _) 24 | 25 | protected def existsQueryFun(first: Rep[First], second: Rep[Second]) = findOneQueryFun(first, second).exists 26 | 27 | protected val existsQuery = Compiled(existsQueryFun _) 28 | 29 | protected def findByFirstFun(first: Rep[First]) = query.filter(_.columns._1 === first) 30 | 31 | protected val findByFirstQueryCompiled = Compiled(findByFirstFun _) 32 | 33 | protected def findSecondByFirstFun(first: Rep[First]) = findByFirstFun(first).map(_.columns._2) 34 | 35 | protected val findSecondByFirstQuery = Compiled(findSecondByFirstFun _) 36 | 37 | protected def findBySecondFun(second: Rep[Second]) = query.filter(_.columns._2 === second) 38 | 39 | protected val findBySecondQuery = Compiled(findBySecondFun _) 40 | 41 | protected def findFirstBySecondFun(second: Rep[Second]) = findBySecondFun(second).map(_.columns._1) 42 | 43 | protected val findFirstBySecondQuery = Compiled(findFirstBySecondFun _) 44 | 45 | /** 46 | * Deletes one element. 47 | * 48 | * @param first element of junction 49 | * @param second element of junction 50 | * @return number of deleted elements (0 or 1) 51 | */ 52 | def delete(first: First, second: Second): DBIO[Int] = 53 | findOneQueryCompiled((first, second)).delete 54 | 55 | /** 56 | * Checks if element exists in database. 57 | * 58 | * @param first element of junction 59 | * @param second element of junction 60 | * @return true if element exists in database 61 | */ 62 | def exists(first: First, second: Second): DBIO[Boolean] = 63 | existsQuery((first, second)).result 64 | 65 | /** 66 | * Saves one element if it's not present in db already. 67 | * 68 | * @param a one element 69 | * @param b other element 70 | */ 71 | def save(a: First, b: Second)(implicit ec: ExecutionContext): DBIO[Unit] = { 72 | exists(a, b).flatMap { 73 | case true => DBIO.successful(()) 74 | case false => 75 | val insert = query += ((a, b)) 76 | val result = insert.map(_ => ()) 77 | result 78 | } 79 | } 80 | 81 | /** 82 | * @param a element to query by 83 | * @return all b values for given a 84 | */ 85 | def forA(a: First): DBIO[Seq[Second]] = findSecondByFirstQuery(a).result 86 | 87 | /** 88 | * @param b element to query by 89 | * @return all a values for given b 90 | */ 91 | def forB(b: Second): DBIO[Seq[First]] = findFirstBySecondQuery(b).result 92 | 93 | /** 94 | * Delete all rows with given a value. 95 | * @param a element to query by 96 | */ 97 | def deleteForA(a: First): DBIO[Int] = findByFirstQueryCompiled(a).delete 98 | 99 | /** 100 | * Delete all rows with given b value. 101 | * @param b element to query by 102 | */ 103 | def deleteForB(b: Second): DBIO[Int] = findBySecondQuery(b).delete 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /unicorn-core/src/main/scala/org/virtuslab/unicorn/repositories/Repositories.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn.repositories 2 | 3 | import org.virtuslab.unicorn.{ HasJdbcProfile, Tables } 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | protected[unicorn] trait Repositories[Underlying] 8 | extends JunctionRepositories[Underlying] 9 | with IdRepositories[Underlying] { 10 | self: HasJdbcProfile with Tables[Underlying] => 11 | 12 | import profile.api._ 13 | 14 | /** 15 | * Implementation detail - common methods for all repositories. 16 | */ 17 | private[repositories] abstract class CommonRepositoryMethods[Entity, T <: Table[Entity]](query: TableQuery[T]) { 18 | 19 | /** 20 | * @return all elements of type A 21 | */ 22 | def findAll(): DBIO[Seq[Entity]] = query.result 23 | 24 | /** 25 | * Deletes all elements in table. 26 | * @return number of deleted elements 27 | */ 28 | def deleteAll(): DBIO[Int] = query.delete 29 | 30 | /** 31 | * Creates table definition in database. 32 | * 33 | */ 34 | def create(): DBIO[Unit] = query.schema.create 35 | 36 | /** 37 | * Drops table definition from database. 38 | * 39 | */ 40 | def drop(): DBIO[Unit] = query.schema.drop 41 | } 42 | 43 | /** 44 | * Base for services for entities that have no type-safe id created - for example join tables. 45 | * 46 | * @tparam Entity type of entity 47 | * @tparam T type of table 48 | * @param query base table query 49 | */ 50 | abstract class BaseRepository[Entity, T <: Table[Entity]](val query: TableQuery[T]) 51 | extends CommonRepositoryMethods[Entity, T](query) { 52 | 53 | /** 54 | * Saves one element. Warning - if element already exist, it's not updated. 55 | * 56 | * @param elem element to save 57 | * @return elem itself 58 | */ 59 | def save(elem: Entity)(implicit ec: ExecutionContext): DBIO[Entity] = exists(elem).flatMap { 60 | case true => DBIO.successful(elem) 61 | case false => 62 | val insert = query += elem 63 | val result = insert.map(_ => elem) 64 | result 65 | } 66 | 67 | /** 68 | * Checks if element exists in database. It have to be implemented by user, 69 | * because this is service for entities without an id and generic method 70 | * could not be created. 71 | * 72 | * @param elem element to check for 73 | * @return true if element exists in database 74 | */ 75 | protected def exists(elem: Entity): DBIO[Boolean] 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /unicorn-core/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /unicorn-core/src/test/scala/org/virtuslab/unicorn/BaseTest.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import org.scalatest._ 4 | import org.scalatest.concurrent.ScalaFutures 5 | import org.scalatest.flatspec.AnyFlatSpecLike 6 | import org.scalatest.matchers.should.Matchers 7 | import org.scalatest.time.{ Millis, Seconds, Span } 8 | import org.virtuslab.unicorn.TestUnicorn.profile.api._ 9 | import slick.dbio.DBIOAction 10 | 11 | import scala.concurrent.Await 12 | import scala.concurrent.duration._ 13 | 14 | trait LongTestUnicorn { 15 | lazy val unicorn = TestUnicorn 16 | } 17 | 18 | trait BaseTest[Underlying] extends AnyFlatSpecLike with Matchers with BeforeAndAfterEach with ScalaFutures { 19 | 20 | val unicorn: Unicorn[Underlying] with HasJdbcProfile 21 | 22 | val dbURL = "jdbc:h2:mem:unicorn" 23 | 24 | val dbDriver = "org.h2.Driver" 25 | 26 | lazy val DB = unicorn.profile.backend.Database.forURL(dbURL, driver = dbDriver) 27 | 28 | implicit val defaultPatience = 29 | PatienceConfig(timeout = Span(5, Seconds), interval = Span(500, Millis)) 30 | 31 | case class IntentionalRollbackException() extends Exception("Transaction intentionally aborted") 32 | 33 | def runWithRollback[R, S <: slick.dbio.NoStream, E <: slick.dbio.Effect](action: DBIOAction[R, S, E]): Unit = { 34 | try { 35 | val block = action andThen DBIO.failed(IntentionalRollbackException()) 36 | val future = DB.run(block.transactionally) 37 | Await.result(future, 5.seconds) 38 | } catch { 39 | case _: IntentionalRollbackException => // Success 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /unicorn-core/src/test/scala/org/virtuslab/unicorn/TestUnicorn.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import slick.jdbc.H2Profile 4 | 5 | object LongUnicornIdentifiers extends Identifiers[Long] { 6 | override def ordering: Ordering[Long] = implicitly[Ordering[Long]] 7 | 8 | override type IdCompanion[Id <: BaseId[Long]] = CoreCompanion[Id] 9 | } 10 | 11 | object TestUnicorn 12 | extends LongUnicornCore 13 | with HasJdbcProfile { 14 | 15 | override lazy val profile = H2Profile 16 | } 17 | -------------------------------------------------------------------------------- /unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/AlternativeIDTest.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn.repositories 2 | 3 | import org.scalatest.flatspec.AnyFlatSpecLike 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | import java.util.UUID 7 | import org.virtuslab.unicorn.TestUnicorn.profile.api._ 8 | import org.virtuslab.unicorn._ 9 | import slick.jdbc.H2Profile 10 | 11 | import scala.concurrent.ExecutionContext.Implicits.global 12 | 13 | object UUIDUnicorn extends UnicornCore[UUID] with HasJdbcProfile { 14 | override val profile = H2Profile 15 | } 16 | 17 | object UUIDUnicornIdentifiers extends Identifiers[UUID] { 18 | override def ordering: Ordering[UUID] = implicitly[Ordering[UUID]] 19 | } 20 | 21 | trait UUIDTestUnicorn { 22 | val unicorn: Unicorn[UUID] with HasJdbcProfile = UUIDUnicorn 23 | } 24 | 25 | /** 26 | * This class is simplified clone of Users from UsersRepositoryTest. 27 | * It uses UUID ids instead of Long 28 | */ 29 | trait UUIDTable extends UUIDTestUnicorn { 30 | 31 | import unicorn._ 32 | 33 | case class UniqueUserId(id: UUID) extends BaseId[UUID] 34 | 35 | case class PersonRow(id: Option[UniqueUserId], name: String) extends WithId[UUID, UniqueUserId] 36 | 37 | class UniquePersons(tag: Tag) extends IdTable[UniqueUserId, PersonRow](tag, "U_USERS") { 38 | def name = column[String]("NAME") 39 | 40 | override def * = (id.?, name) <> (PersonRow.tupled, PersonRow.unapply) 41 | } 42 | 43 | //provides custom ddl query to generate UUID primary keys 44 | object UniquePersons { 45 | val CreateSql = sqlu"""CREATE TABLE IF NOT EXISTS "U_USERS" ("id" UUID default RANDOM_UUID() PRIMARY KEY , "NAME" VARCHAR(255) NOT NULL);""" 46 | val CreateDdl = CreateSql.map(_ => ()) 47 | val DropSql = sqlu"DROP TABLE U_USERS;" 48 | } 49 | 50 | val personsQuery = TableQuery[UniquePersons] 51 | 52 | object PersonsRepository extends BaseIdRepository[UniqueUserId, PersonRow, UniquePersons](personsQuery) 53 | 54 | } 55 | 56 | trait PersonUUIDTest extends AnyFlatSpecLike { 57 | self: AnyFlatSpecLike with Matchers with BaseTest[UUID] with UUIDTable => 58 | 59 | "Persons Repository" should "work fine with UUID id" in runWithRollback { 60 | val person = PersonRow(None, "Alexander") 61 | 62 | val actions = for { 63 | _ <- UniquePersons.CreateDdl 64 | personId <- PersonsRepository save person 65 | foundPerson <- PersonsRepository findById personId 66 | } yield foundPerson 67 | 68 | actions map { foundPerson => 69 | foundPerson.flatMap(_.id) shouldNot be(None) 70 | foundPerson.map(_.name) shouldEqual Some(person.name) 71 | } 72 | } 73 | } 74 | 75 | class AlternativeIDTest extends BaseTest[UUID] with UUIDTable with PersonUUIDTest 76 | -------------------------------------------------------------------------------- /unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/DictionaryRepositoryTest.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn.repositories 2 | 3 | import org.virtuslab.unicorn.TestUnicorn._ 4 | import org.virtuslab.unicorn.TestUnicorn.profile.api._ 5 | import org.virtuslab.unicorn.{ BaseTest, LongTestUnicorn } 6 | import slick.dbio.Effect.Read 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | class DictionaryRepositoryTest extends BaseTest[Long] with LongTestUnicorn { 11 | 12 | type DictionaryEntry = (String, String) 13 | 14 | class Dictionary(tag: Tag) extends BaseTable[DictionaryEntry](tag, "DICTIONARY") { 15 | 16 | def key = column[String]("key") 17 | 18 | def value = column[String]("value") 19 | 20 | def dictionaryIndex = index("dictionary_idx", (key, value), unique = true) 21 | 22 | def * = (key, value) 23 | } 24 | 25 | val dictQuery: TableQuery[Dictionary] = TableQuery[Dictionary] 26 | 27 | object DictionaryRepository extends BaseRepository[DictionaryEntry, Dictionary](dictQuery) { 28 | 29 | protected def findQuery(entry: DictionaryEntry) = for { 30 | dictionaryEntry <- query if dictionaryEntry.key === entry._1 && dictionaryEntry.value === entry._2 31 | } yield dictionaryEntry.value 32 | 33 | override protected def exists(entry: DictionaryEntry): DBIOAction[Boolean, NoStream, Read] = 34 | findQuery(entry).result.headOption.map(_.nonEmpty) 35 | } 36 | 37 | "Dictionary repository" should "save and query users" in runWithRollback { 38 | val entry = ("key", "value") 39 | 40 | val actions = for { 41 | _ <- dictQuery.schema.create 42 | _ <- DictionaryRepository save entry 43 | find1 <- DictionaryRepository.findAll() 44 | 45 | // when saving second time 46 | _ <- DictionaryRepository save entry 47 | 48 | // then no new entry should be added 49 | find2 <- DictionaryRepository.findAll() 50 | 51 | _ <- DictionaryRepository.deleteAll() 52 | find3 <- DictionaryRepository.findAll() 53 | } yield (find1, find2, find3) 54 | 55 | actions map { 56 | case (find1, find2, find3) => 57 | find1 shouldEqual Seq(entry) 58 | find2 shouldEqual Seq(entry) 59 | find3 shouldBe empty 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/JunctionRepositoryTest.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn.repositories 2 | 3 | import org.virtuslab.unicorn.LongUnicornIdentifiers.IdCompanion 4 | import org.virtuslab.unicorn.TestUnicorn.profile.api._ 5 | import org.virtuslab.unicorn.{ BaseId, BaseTest, LongTestUnicorn } 6 | 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | 9 | class JunctionRepositoryTest extends BaseTest[Long] with LongTestUnicorn { 10 | 11 | import unicorn._ 12 | 13 | behavior of classOf[JunctionRepository[_, _, _]].getSimpleName 14 | 15 | case class OrderId(id: Long) extends BaseId[Long] 16 | 17 | object OrderId extends IdCompanion[OrderId] 18 | 19 | case class CustomerId(id: Long) extends BaseId[Long] 20 | 21 | object CustomerId extends IdCompanion[CustomerId] 22 | 23 | class OrderCustomer(tag: Tag) extends JunctionTable[OrderId, CustomerId](tag, "order_customer") { 24 | def orderId = column[OrderId]("ORDER_ID") 25 | 26 | def customerId = column[CustomerId]("CUSTOMER_ID") 27 | 28 | def columns = orderId -> customerId 29 | } 30 | 31 | object OrderCustomer { 32 | val tableQuery = TableQuery[OrderCustomer] 33 | } 34 | 35 | object OrderCustomerRepository 36 | extends JunctionRepository[OrderId, CustomerId, OrderCustomer](OrderCustomer.tableQuery) 37 | 38 | it should "save pairs" in runWithRollback { 39 | val actions = for { 40 | _ <- OrderCustomerRepository.create 41 | _ <- OrderCustomerRepository.save(OrderId(100), CustomerId(200)) 42 | all <- OrderCustomerRepository.findAll() 43 | } yield all 44 | 45 | actions map { result => 46 | result should have size 1 47 | } 48 | } 49 | 50 | it should "save pair only once" in runWithRollback { 51 | val actions = for { 52 | _ <- OrderCustomerRepository.create 53 | _ <- OrderCustomerRepository.save(OrderId(100), CustomerId(200)) 54 | _ <- OrderCustomerRepository.save(OrderId(100), CustomerId(200)) 55 | all <- OrderCustomerRepository.findAll 56 | } yield all 57 | 58 | actions map { result => 59 | result should have size 1 60 | } 61 | } 62 | 63 | it should "find all pairs" in runWithRollback { 64 | val actions = for { 65 | _ <- OrderCustomerRepository.create 66 | _ <- OrderCustomerRepository.save(OrderId(100), CustomerId(200)) 67 | _ <- OrderCustomerRepository.save(OrderId(101), CustomerId(200)) 68 | all <- OrderCustomerRepository.findAll 69 | } yield all 70 | 71 | actions map { result => 72 | result should have size 2 73 | } 74 | } 75 | 76 | it should "find by first" in runWithRollback { 77 | val orderId = OrderId(100) 78 | val actions = for { 79 | _ <- OrderCustomerRepository.create 80 | _ <- OrderCustomerRepository.save(orderId, CustomerId(200)) 81 | _ <- OrderCustomerRepository.save(orderId, CustomerId(201)) 82 | _ <- OrderCustomerRepository.save(OrderId(101), CustomerId(201)) 83 | order <- OrderCustomerRepository.forA(orderId) 84 | } yield order 85 | 86 | actions map { result => 87 | result should have size 2 88 | } 89 | } 90 | 91 | it should "find by second" in runWithRollback { 92 | val customerId = CustomerId(200) 93 | val actions = for { 94 | _ <- OrderCustomerRepository.create 95 | _ <- OrderCustomerRepository.save(OrderId(100), customerId) 96 | _ <- OrderCustomerRepository.save(OrderId(101), customerId) 97 | _ <- OrderCustomerRepository.save(OrderId(101), CustomerId(100)) 98 | order <- OrderCustomerRepository.forB(customerId) 99 | } yield order 100 | 101 | actions map { result => 102 | result should have size 2 103 | } 104 | } 105 | 106 | it should "delete by first" in runWithRollback { 107 | val orderId = OrderId(100) 108 | val actions = for { 109 | _ <- OrderCustomerRepository.create 110 | _ <- OrderCustomerRepository.save(orderId, CustomerId(200)) 111 | _ <- OrderCustomerRepository.save(orderId, CustomerId(201)) 112 | _ <- OrderCustomerRepository.delete(orderId, CustomerId(200)) 113 | orders <- OrderCustomerRepository.findAll() 114 | } yield orders 115 | 116 | actions map { result => 117 | result should have size 1 118 | } 119 | } 120 | 121 | it should "delete all items with given first" in runWithRollback { 122 | val orderId = OrderId(100) 123 | val actions = for { 124 | _ <- OrderCustomerRepository.create 125 | _ <- OrderCustomerRepository.save(orderId, CustomerId(200)) 126 | _ <- OrderCustomerRepository.save(orderId, CustomerId(201)) 127 | _ <- OrderCustomerRepository.deleteForA(orderId) 128 | orders <- OrderCustomerRepository.findAll 129 | } yield orders 130 | 131 | actions map { result => 132 | result shouldBe empty 133 | } 134 | } 135 | 136 | it should "delete all items with given second" in runWithRollback { 137 | val customerId = CustomerId(200) 138 | val actions = for { 139 | _ <- OrderCustomerRepository.create 140 | _ <- OrderCustomerRepository.save(OrderId(100), customerId) 141 | _ <- OrderCustomerRepository.save(OrderId(101), customerId) 142 | _ <- OrderCustomerRepository.deleteForB(customerId) 143 | all <- OrderCustomerRepository.findAll 144 | } yield all 145 | 146 | actions map { result => 147 | result shouldBe empty 148 | } 149 | } 150 | 151 | it should "check that one pair exists" in runWithRollback { 152 | val customerId = CustomerId(200) 153 | 154 | val actions = for { 155 | _ <- OrderCustomerRepository.create 156 | _ <- OrderCustomerRepository.save(OrderId(100), customerId) 157 | _ <- OrderCustomerRepository.save(OrderId(101), customerId) 158 | firstExist <- OrderCustomerRepository.exists(OrderId(200), customerId) 159 | secondExist <- OrderCustomerRepository.exists(OrderId(101), customerId) 160 | } yield (firstExist, secondExist) 161 | 162 | actions map { 163 | case (firstExist, secondExist) => 164 | firstExist shouldBe false 165 | secondExist shouldBe true 166 | } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/TypeMapperTest.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn.repositories 2 | 3 | import org.joda.time.{ DateTime, Duration, LocalDate } 4 | import org.scalatest.concurrent.PatienceConfiguration.Timeout 5 | import org.scalatest.concurrent.ScalaFutures 6 | import org.scalatest.time.{ Seconds, Span } 7 | import org.virtuslab.unicorn.TestUnicorn.profile.api._ 8 | import org.virtuslab.unicorn.{ BaseTest, LongTestUnicorn } 9 | 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | 12 | class TypeMapperTest extends BaseTest[Long] with LongTestUnicorn { 13 | 14 | import unicorn._ 15 | 16 | behavior of classOf[CustomTypeMappers].getSimpleName 17 | 18 | case class JodaRow( 19 | dateTime: DateTime, 20 | duration: Duration, 21 | localDate: LocalDate) 22 | 23 | class Joda(tag: Tag) extends BaseTable[JodaRow](tag, "JODA") { 24 | 25 | def dateTime = column[DateTime]("EMAIL") 26 | 27 | def duration = column[Duration]("FIRST_NAME") 28 | 29 | def localDate = column[LocalDate]("LAST_NAME") 30 | 31 | override def * = (dateTime, duration, localDate) <> (JodaRow.tupled, JodaRow.unapply) 32 | } 33 | 34 | val jodaQuery: TableQuery[Joda] = TableQuery[Joda] 35 | 36 | it should "provide mappings for joda.time types" in runWithRollback { 37 | val joda = JodaRow(DateTime.now(), Duration.millis(120), LocalDate.now()) 38 | val actions = for { 39 | _ <- jodaQuery.schema.create 40 | _ <- jodaQuery += (joda) 41 | first <- jodaQuery.result.headOption 42 | } yield first 43 | 44 | actions map { first => 45 | first shouldEqual Some(joda) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /unicorn-core/src/test/scala/org/virtuslab/unicorn/repositories/UsersRepositoryTest.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn.repositories 2 | 3 | import org.scalatest.OptionValues 4 | import org.scalatest.flatspec.AnyFlatSpecLike 5 | import org.scalatest.matchers.should.Matchers 6 | import org.virtuslab.unicorn.{ BaseTest, _ } 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | trait AbstractUserTable { 11 | 12 | val unicorn: Unicorn[Long] with HasJdbcProfile 13 | val identifiers: Identifiers[Long] 14 | 15 | import unicorn._ 16 | import unicorn.profile.api._ 17 | import identifiers._ 18 | 19 | case class UserId(id: Long) extends BaseId[Long] 20 | 21 | object UserId extends CoreCompanion[UserId] 22 | 23 | case class UserRow( 24 | id: Option[UserId], 25 | email: String, 26 | firstName: String, 27 | lastName: String) extends WithId[Long, UserId] 28 | 29 | class Users(tag: Tag) extends IdTable[UserId, UserRow](tag, "USERS") { 30 | 31 | def email = column[String]("EMAIL") 32 | 33 | def firstName = column[String]("FIRST_NAME") 34 | 35 | def lastName = column[String]("LAST_NAME") 36 | 37 | override def * = (id.?, email, firstName, lastName) <> (UserRow.tupled, UserRow.unapply) 38 | } 39 | 40 | val usersQuery: TableQuery[Users] = TableQuery[Users] 41 | 42 | object UsersRepository extends BaseIdRepository[UserId, UserRow, Users](usersQuery) 43 | 44 | } 45 | 46 | trait UsersRepositoryTest extends OptionValues { 47 | self: AnyFlatSpecLike with Matchers with BaseTest[Long] with AbstractUserTable => 48 | 49 | "Users Service" should "save and query users" in runWithRollback { 50 | val user = UserRow(None, "test@email.com", "Krzysztof", "Nowak") 51 | 52 | val actions = for { 53 | _ <- UsersRepository.create() 54 | userId <- UsersRepository.save(user) 55 | user <- UsersRepository.findById(userId) 56 | } yield user 57 | 58 | actions map { userOpt => 59 | userOpt shouldBe defined 60 | 61 | userOpt.value should have( 62 | 'email(user.email), 63 | 'firstName(user.firstName), 64 | 'lastName(user.lastName)) 65 | userOpt.value.id shouldBe defined 66 | } 67 | } 68 | 69 | it should "save and query multiple users" in runWithRollback { 70 | val users = (Stream from 1 take 10) map (n => UserRow(None, "test@email.com", "Krzysztof" + n, "Nowak")) 71 | 72 | // setup 73 | val actions = for { 74 | _ <- UsersRepository.create() 75 | _ <- UsersRepository saveAll users 76 | all <- UsersRepository.findAll() 77 | } yield all 78 | 79 | actions map { newUsers => 80 | newUsers.size shouldEqual 10 81 | newUsers.headOption map (_.firstName) shouldEqual Some("Krzysztof1") 82 | newUsers.lastOption map (_.firstName) shouldEqual Some("Krzysztof10") 83 | } 84 | } 85 | 86 | it should "query existing user" in runWithRollback { 87 | val blankUser = UserRow(None, "test@email.com", "Krzysztof", "Nowak") 88 | 89 | val actions = for { 90 | _ <- UsersRepository.create() 91 | userId <- UsersRepository save blankUser 92 | user <- UsersRepository.findExistingById(userId) 93 | } yield user 94 | 95 | actions map { user2 => 96 | user2 should have( 97 | 'email(blankUser.email), 98 | 'firstName(blankUser.firstName), 99 | 'lastName(blankUser.lastName)) 100 | user2.id shouldBe defined 101 | } 102 | } 103 | 104 | it should "update existing user" in runWithRollback { 105 | val blankUser = UserRow(None, "test@email.com", "Krzysztof", "Nowak") 106 | 107 | val actions = for { 108 | _ <- UsersRepository.create() 109 | userId <- UsersRepository save blankUser 110 | user <- UsersRepository.findExistingById(userId) 111 | _ <- UsersRepository save user.copy(firstName = "Jerzy", lastName = "Muller") 112 | updatedUser <- UsersRepository.findExistingById(userId) 113 | } yield (userId, updatedUser) 114 | 115 | actions map { 116 | case (userId, updatedUser) => 117 | updatedUser should have( 118 | 'email("test@email.com"), 119 | 'firstName("Jerzy"), 120 | 'lastName("Muller"), 121 | 'id(Some(userId))) 122 | } 123 | } 124 | 125 | it should "query all ids" in runWithRollback { 126 | val users = Seq( 127 | UserRow(None, "test1@email.com", "Krzysztof", "Nowak"), 128 | UserRow(None, "test2@email.com", "Janek", "Nowak"), 129 | UserRow(None, "test3@email.com", "Marcin", "Nowak")) 130 | 131 | val actions = for { 132 | _ <- UsersRepository.create() 133 | ids <- UsersRepository saveAll users 134 | allIds <- UsersRepository.allIds() 135 | } yield (ids, allIds) 136 | 137 | actions map { 138 | case (ids, allIds) => 139 | allIds shouldEqual ids 140 | } 141 | } 142 | 143 | it should "sort users by id" in runWithRollback { 144 | val users = Seq( 145 | UserRow(None, "test1@email.com", "Krzysztof", "Nowak"), 146 | UserRow(None, "test2@email.com", "Janek", "Nowak"), 147 | UserRow(None, "test3@email.com", "Marcin", "Nowak")) 148 | 149 | val actions = for { 150 | _ <- UsersRepository.create() 151 | ids <- UsersRepository saveAll users 152 | users <- UsersRepository.findAll() 153 | } yield (ids, users) 154 | 155 | actions map { 156 | case (ids, users) => 157 | val usersWithIds = (users zip ids).map { case (user, id) => user.copy(id = Some(id)) } 158 | users.sortBy(_.id) shouldEqual usersWithIds 159 | } 160 | } 161 | 162 | it should "query multiple users by ids" in runWithRollback { 163 | val users = Seq( 164 | UserRow(None, "test1@email.com", "Krzysztof", "Nowak"), 165 | UserRow(None, "test2@email.com", "Janek", "Nowak"), 166 | UserRow(None, "test3@email.com", "Marcin", "Nowak")) 167 | 168 | val actions = for { 169 | _ <- UsersRepository.create() 170 | ids <- UsersRepository saveAll users 171 | allUsers <- UsersRepository.findAll() 172 | selectedUsers: Seq[UserRow] = { 173 | val usersWithIds = (users zip ids).map { case (user, id) => user.copy(id = Some(id)) } 174 | Seq(usersWithIds.head, usersWithIds.last) 175 | } 176 | foundSelectedUsers <- UsersRepository.findByIds(selectedUsers.flatMap(_.id)) 177 | } yield (allUsers, foundSelectedUsers, selectedUsers) 178 | 179 | actions map { 180 | case (allUsers, foundSelectedUsers, selectedUsers) => 181 | allUsers.size shouldEqual 3 182 | foundSelectedUsers shouldEqual selectedUsers 183 | } 184 | } 185 | 186 | it should "copy user by id" in runWithRollback { 187 | 188 | val user = UserRow(None, "test1@email.com", "Krzysztof", "Nowak") 189 | 190 | val actions = for { 191 | _ <- UsersRepository.create() 192 | id <- UsersRepository.save(user) 193 | idOfCopy <- UsersRepository.copyAndSave(id) 194 | copiedUser <- UsersRepository.findById(idOfCopy) 195 | } yield copiedUser.value 196 | 197 | actions map { copiedUser => 198 | copiedUser.id shouldNot be(user.id) 199 | 200 | copiedUser should have( 201 | 'email(user.email), 202 | 'firstName(user.firstName), 203 | 'lastName(user.lastName)) 204 | } 205 | } 206 | 207 | it should "delete user by id" in runWithRollback { 208 | val users = Seq( 209 | UserRow(None, "test1@email.com", "Krzysztof", "Nowak"), 210 | UserRow(None, "test2@email.com", "Janek", "Nowak"), 211 | UserRow(None, "test3@email.com", "Marcin", "Nowak")) 212 | 213 | val actions = for { 214 | _ <- UsersRepository.create() 215 | ids <- UsersRepository saveAll users 216 | initialUsers <- UsersRepository.findAll() 217 | _ <- UsersRepository.deleteById(ids(1)) 218 | resultingUsers <- UsersRepository.findAll() 219 | } yield (ids, initialUsers, resultingUsers) 220 | 221 | actions map { 222 | case (ids, initialUsers, resultingUsers) => 223 | initialUsers should have size users.size 224 | val usersWithIds = (users zip ids).map { case (user, id) => user.copy(id = Some(id)) } 225 | val remainingUsers = Seq(usersWithIds.head, usersWithIds.last) 226 | resultingUsers shouldEqual remainingUsers 227 | } 228 | 229 | } 230 | 231 | it should "delete all users" in runWithRollback { 232 | val users = Seq( 233 | UserRow(None, "test1@email.com", "Krzysztof", "Nowak"), 234 | UserRow(None, "test2@email.com", "Janek", "Nowak"), 235 | UserRow(None, "test3@email.com", "Marcin", "Nowak")) 236 | 237 | val actions = for { 238 | _ <- UsersRepository.create() 239 | ids <- UsersRepository saveAll users 240 | initialUsers <- UsersRepository.findAll() 241 | _ <- UsersRepository.deleteAll() 242 | resultingUsers <- UsersRepository.findAll() 243 | } yield (initialUsers, resultingUsers) 244 | 245 | actions map { 246 | case (initialUsers, resultingUsers) => 247 | initialUsers should have size users.size 248 | resultingUsers shouldBe empty 249 | } 250 | } 251 | 252 | it should "create and drop table" in runWithRollback { 253 | val actions = for { 254 | _ <- UsersRepository.create() 255 | _ <- UsersRepository.drop() 256 | } yield () 257 | 258 | actions 259 | } 260 | } 261 | 262 | class CoreUserRepositoryTest extends BaseTest[Long] with UsersRepositoryTest with AbstractUserTable { 263 | override lazy val unicorn = TestUnicorn 264 | override val identifiers: Identifiers[Long] = LongUnicornIdentifiers 265 | } -------------------------------------------------------------------------------- /unicorn-play/src/main/scala/org/virtuslab/unicorn/PlayIdentifiers.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import play.api.data.FormError 4 | import play.api.data.format.Formatter 5 | import play.api.mvc.{ PathBindable, QueryStringBindable } 6 | 7 | import play.api.libs.json._ 8 | 9 | protected[unicorn] trait PlayIdentifiers[Underlying] { 10 | self: PlayIdentifiersImpl[Underlying] with Identifiers[Underlying] => 11 | 12 | abstract class PlayCompanion[Id <: BaseId[Underlying]] 13 | extends CoreCompanion[Id] 14 | with Applicable[Id] 15 | with PlayImplicits[Id] 16 | 17 | /** Marker trait */ 18 | protected[unicorn] trait Applicable[Id <: BaseId[Underlying]] extends Any { 19 | 20 | /** 21 | * Factory method for I instance creation. 22 | * @param id long from which I instance is created 23 | * @return I instance 24 | */ 25 | def apply(id: Id#Underlying): Id 26 | } 27 | 28 | /** 29 | * Implicits required by Play. 30 | * 31 | * @tparam Id type of Id 32 | */ 33 | protected[unicorn] trait PlayImplicits[Id <: BaseId[Underlying]] { 34 | self: Applicable[Id] => 35 | 36 | /** Type mapper for route files. */ 37 | implicit final val pathBinder: PathBindable[Id] = underlyingPathBinder.transform(apply, _.value) 38 | 39 | /** Implicit for mapping id to routes params for play */ 40 | implicit final val queryStringBinder: QueryStringBindable[Id] = underlyingQueryStringBinder.transform(apply, _.value) 41 | 42 | /** Form formatter for Id */ 43 | implicit final val idMappingFormatter: Formatter[Id] = new Formatter[Id] { 44 | 45 | override val format = Some(("format.numeric", Nil)) 46 | 47 | override def bind(key: String, data: Map[String, String]): Either[Seq[FormError], Id] = { 48 | 49 | def handleErrors(errors: Seq[FormError]): Seq[FormError] = errors match { 50 | case _ if data.get(key).forall(_.isEmpty) => errors.map(_.copy(messages = Seq("id.empty"))) 51 | case _ => errors.map(_.copy(messages = Seq("id.invalid"))) 52 | } 53 | 54 | underlyingFormatter.bind(key, data) 55 | .right.map(apply) 56 | .left.map(handleErrors) 57 | } 58 | 59 | override def unbind(key: String, id: Id): Map[String, String] = underlyingFormatter.unbind(key, id.value) 60 | } 61 | 62 | /** Json format for Id */ 63 | implicit final val idJsonFormat: Format[Id] = new Format[Id] { 64 | def reads(p1: JsValue): JsResult[Id] = underlyingFormat.reads(p1).map(apply) 65 | def writes(p1: Id): JsValue = underlyingFormat.writes(p1.value) 66 | } 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /unicorn-play/src/main/scala/org/virtuslab/unicorn/UnicornPlay.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import javax.inject.{ Inject, Singleton } 4 | 5 | import play.api.data.format.Formats._ 6 | import play.api.data.format.Formatter 7 | import play.api.db.slick.DatabaseConfigProvider 8 | import play.api.libs.json.Format 9 | import play.api.mvc.{ PathBindable, QueryStringBindable } 10 | import slick.basic.DatabaseConfig 11 | import slick.jdbc.{ JdbcBackend, JdbcProfile } 12 | 13 | trait UnicornWrapper[Underlying] { 14 | protected val unicorn: UnicornPlay[Underlying] 15 | } 16 | 17 | abstract class UnicornPlayLike[Underlying](dbConfig: DatabaseConfig[JdbcProfile]) 18 | extends Unicorn[Underlying] 19 | with HasJdbcProfile { 20 | 21 | val profile: JdbcProfile = dbConfig.profile 22 | 23 | val db: JdbcBackend#DatabaseDef = dbConfig.db 24 | } 25 | 26 | abstract class UnicornPlay[Underlying](dbConfig: DatabaseConfig[JdbcProfile]) 27 | extends UnicornPlayLike[Underlying](dbConfig) 28 | 29 | abstract class PlayIdentifiersImpl[Underlying](implicit 30 | val underlyingFormatter: Formatter[Underlying], 31 | val underlyingFormat: Format[Underlying], 32 | val underlyingQueryStringBinder: QueryStringBindable[Underlying], 33 | val underlyingPathBinder: PathBindable[Underlying], 34 | val ordering: Ordering[Underlying]) extends PlayIdentifiers[Underlying] with Identifiers[Underlying] 35 | 36 | @Singleton() 37 | class LongUnicornPlay @Inject() (dbConfig: DatabaseConfig[JdbcProfile]) 38 | extends UnicornPlay[Long](dbConfig) 39 | 40 | @Singleton() 41 | class LongUnicornPlayJDBC @Inject() (databaseConfigProvider: DatabaseConfigProvider) 42 | extends LongUnicornPlay(databaseConfigProvider.get[JdbcProfile]) 43 | 44 | object LongUnicornPlayIdentifiers extends PlayIdentifiersImpl[Long] { 45 | override val ordering: Ordering[Long] = implicitly[Ordering[Long]] 46 | override type IdCompanion[Id <: BaseId[Long]] = PlayCompanion[Id] 47 | } -------------------------------------------------------------------------------- /unicorn-play/src/test/scala/org/virtuslab/unicorn/BasePlayTest.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import org.scalatest._ 4 | import org.scalatest.flatspec.AnyFlatSpecLike 5 | import org.scalatest.matchers.should.Matchers 6 | import play.api.db.slick.DatabaseConfigProvider 7 | import play.api.{ Application, Configuration, Play } 8 | import play.api.inject.guice.GuiceApplicationBuilder 9 | import org.scalatestplus.play.guice.GuiceFakeApplicationFactory 10 | import slick.jdbc.JdbcProfile 11 | 12 | trait BasePlayTest 13 | extends AnyFlatSpecLike 14 | with OptionValues 15 | with Matchers 16 | with BeforeAndAfterEach 17 | with BaseTest[Long] 18 | with BeforeAndAfterAll 19 | with GuiceFakeApplicationFactory { 20 | 21 | private val testDb = Configuration( 22 | "slick.dbs.default.profile" -> "slick.jdbc.H2Profile$", 23 | "slick.dbs.default.db.driver" -> "org.h2.Driver", 24 | "slick.dbs.default.db.url" -> "jdbc:h2:mem:play", 25 | "slick.dbs.default.db.user" -> "sa", 26 | "slick.dbs.default.db.password" -> "") 27 | 28 | implicit val app: Application = { 29 | val fake = new GuiceApplicationBuilder(configuration = testDb).build 30 | Play.start(fake) 31 | fake 32 | } 33 | 34 | override lazy val unicorn: Unicorn[Long] with HasJdbcProfile = 35 | new LongUnicornPlay(DatabaseConfigProvider.get[JdbcProfile](app)) 36 | 37 | import unicorn.profile.api._ 38 | 39 | override protected def beforeEach(): Unit = { 40 | DB.run(sqlu"""DROP ALL OBJECTS""") 41 | super.beforeEach() 42 | } 43 | 44 | override protected def afterAll(): Unit = { 45 | Play.stop(app) 46 | super.afterEach() 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /unicorn-play/src/test/scala/org/virtuslab/unicorn/PlayCompanionTest.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import LongUnicornPlayIdentifiers._ 4 | import play.api.data.format.Formatter 5 | import play.api.mvc.{ PathBindable, QueryStringBindable } 6 | import play.api.libs.json._ 7 | 8 | class PlayCompanionTest extends BasePlayTest { 9 | 10 | case class UserId(id: Long) extends BaseId[Long] 11 | 12 | object UserId extends IdCompanion[UserId] 13 | 14 | case class User(id: UserId, name: String) 15 | 16 | it should "have implicit query string binder" in { 17 | implicitly[QueryStringBindable[UserId]] shouldNot be(null) 18 | } 19 | 20 | it should "have implicit json format" in { 21 | implicitly[Format[UserId]] shouldNot be(null) 22 | } 23 | 24 | it should "have implicit formatter" in { 25 | implicitly[Formatter[UserId]] shouldNot be(null) 26 | } 27 | 28 | it should "have implicit path bindable" in { 29 | implicitly[PathBindable[UserId]] shouldNot be(null) 30 | } 31 | 32 | it should "have working implicit query string binder" in { 33 | val qsb = implicitly[QueryStringBindable[UserId]] 34 | val userId = UserId(123) 35 | qsb.bind("id", Map("id" -> Seq("123"))).value shouldEqual Right(userId) 36 | qsb.unbind("id", userId) shouldEqual "id=123" 37 | } 38 | 39 | it should "have working implicit json format" in { 40 | import play.api.libs.functional.syntax._ 41 | 42 | val user = User(UserId(123), "Jerzy") 43 | val jsonUser = Json.parse(""" { "id" : 123, "name": "Jerzy" } """) 44 | 45 | // Reads 46 | 47 | implicit val reads: Reads[User] = ( 48 | (JsPath \ "id").read[UserId] and 49 | (JsPath \ "name").read[String])(User.apply _) 50 | 51 | jsonUser.validate[User].get shouldEqual user 52 | 53 | // Writes 54 | 55 | implicit val writes: Writes[User] = ( 56 | (JsPath \ "id").write[UserId] and 57 | (JsPath \ "name").write[String])(unlift(User.unapply)) 58 | 59 | Json.toJson(user) shouldEqual jsonUser 60 | } 61 | 62 | it should "have working implicit formatter" in { 63 | import play.api.data._ 64 | import play.api.data.Forms._ 65 | 66 | val user = User(UserId(123), "Jerzy") 67 | val userMap = Map("id" -> "123", "name" -> "Jerzy") 68 | 69 | val userForm = Form( 70 | mapping( 71 | "id" -> Forms.of[UserId], 72 | "name" -> text)(User.apply)(User.unapply)) 73 | 74 | // Reads 75 | userForm.bind(userMap).get shouldEqual user 76 | 77 | // Writes - just check if it works without errors 78 | userForm.fill(user) 79 | 80 | // Test erroneous input 81 | val emptyBadUserMap = Map("id" -> "", "name" -> "Jerzy") 82 | val numberFormatBadUserMap = Map("id" -> "123a", "name" -> "Jerzy") 83 | 84 | userForm.bind(emptyBadUserMap).errors shouldEqual List(FormError("id", Seq("id.empty"))) 85 | userForm.bind(numberFormatBadUserMap).errors shouldEqual List(FormError("id", Seq("id.invalid"))) 86 | } 87 | 88 | it should "have working implicit path bindable" in { 89 | val pb = implicitly[PathBindable[UserId]] 90 | val userId = UserId(123) 91 | val userIdString = "123" 92 | 93 | pb.bind("id", userIdString) shouldEqual Right(userId) 94 | pb.unbind("id", userId) shouldEqual userIdString 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /unicorn-play/src/test/scala/org/virtuslab/unicorn/StringPlayUnicorn.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import com.google.inject.{ Inject, Singleton } 4 | import slick.basic.DatabaseConfig 5 | import slick.jdbc.JdbcProfile 6 | import slick.lifted.{ ProvenShape, Tag => SlickTag } 7 | import play.api.data.format.Formats._ 8 | 9 | @Singleton() 10 | class StringUnicornPlay @Inject() (dbConfig: DatabaseConfig[JdbcProfile]) extends UnicornPlay[String](dbConfig) 11 | 12 | object StringUnicornPlayIdentifiers extends PlayIdentifiersImpl[String] { 13 | override val ordering: Ordering[String] = implicitly[Ordering[String]] 14 | override type IdCompanion[Id <: BaseId[String]] = PlayCompanion[Id] 15 | } 16 | 17 | import StringUnicornPlayIdentifiers._ 18 | 19 | case class UserId(id: String) extends BaseId[String] 20 | 21 | object UserId extends IdCompanion[UserId] 22 | 23 | case class UserRow(id: Option[UserId], name: String) extends WithId[String, UserId] 24 | 25 | trait UserQuery { 26 | self: UnicornWrapper[String] => 27 | 28 | import unicorn._ 29 | import unicorn.profile.api._ 30 | 31 | class UserTable(tag: SlickTag) extends IdTable[UserId, UserRow](tag, "test") { 32 | def name = column[String]("name") 33 | override def * : ProvenShape[UserRow] = (id.?, name) <> (UserRow.tupled, UserRow.unapply) 34 | } 35 | 36 | class UserRepository extends BaseIdRepository[UserId, UserRow, UserTable](TableQuery[UserTable]) 37 | } 38 | 39 | -------------------------------------------------------------------------------- /unicorn-play/src/test/scala/org/virtuslab/unicorn/UnicornPlayTests.scala: -------------------------------------------------------------------------------- 1 | package org.virtuslab.unicorn 2 | 3 | import org.virtuslab.unicorn.repositories.{ AbstractUserTable, UsersRepositoryTest } 4 | 5 | class UnicornPlayTests 6 | extends BasePlayTest 7 | with UsersRepositoryTest 8 | with AbstractUserTable { 9 | override val identifiers: Identifiers[Long] = LongUnicornPlayIdentifiers 10 | } 11 | --------------------------------------------------------------------------------