├── .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 | [](https://gitter.im/VirtusLab/unicorn?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
5 | [](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 |
--------------------------------------------------------------------------------