├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── jvmopts.travis ├── modules ├── core │ └── src │ │ ├── main │ │ └── scala │ │ │ └── io │ │ │ └── strongtyped │ │ │ └── active │ │ │ └── slick │ │ │ ├── ActiveRecord.scala │ │ │ ├── CrudActions.scala │ │ │ ├── DBIOExtensions.scala │ │ │ ├── EntityActions.scala │ │ │ ├── EntityActionsLike.scala │ │ │ ├── JdbcProfileProvider.scala │ │ │ ├── Lens.scala │ │ │ ├── OptimisticLocking.scala │ │ │ ├── SchemaManagement.scala │ │ │ ├── exceptions │ │ │ ├── ActiveSlickException.scala │ │ │ ├── NoRowsAffectedException.scala │ │ │ ├── RowNotFoundException.scala │ │ │ ├── StaleObjectStateException.scala │ │ │ └── TooManyRowsAffectedException.scala │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── io │ │ └── strongtyped │ │ └── active │ │ └── slick │ │ ├── BeerTest.scala │ │ ├── CrudTest.scala │ │ ├── EntityActionsBeforeInsertUpdateTest.scala │ │ ├── Schema.scala │ │ ├── SupplierTest.scala │ │ └── test │ │ ├── DbSuite.scala │ │ └── H2Suite.scala ├── samples │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── codegen_schema.sql │ │ └── scala │ │ │ └── io │ │ │ └── strongtyped │ │ │ └── active │ │ │ └── slick │ │ │ └── docexamples │ │ │ ├── ActiveSlickWithCodegen.scala │ │ │ ├── MappingWithActiveSlick.scala │ │ │ ├── MappingWithoutActiveSlick.scala │ │ │ └── OptimisticLockingExample.scala │ │ └── test │ │ └── scala │ │ └── io │ │ └── strongtyped │ │ └── active │ │ └── slick │ │ └── docexamples │ │ └── ActiveSlickWithCodegenTest.scala └── shapeless │ └── src │ └── main │ └── scala │ └── io │ └── strongtyped │ └── active │ └── slick │ └── shapeless │ └── ShapelessSlick.scala ├── project ├── BuildSettings.scala ├── Dependencies.scala ├── build.properties ├── plugins.sbt └── sbt-ui.sbt ├── scripts └── test-shapeless.scala ├── sonatype.sbt ├── src └── asciidoctor │ ├── foundation-blue.css │ ├── index.adoc │ └── pygments-monokai.css └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.idea_modules 3 | target 4 | /.classpath 5 | /.project 6 | /.settings 7 | /.cache 8 | .sbtserver* 9 | src/asciidoctor/index.html 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | sudo: false 3 | script: sbt -jvm-opts jvmopts.travis +test 4 | jdk: 5 | - oraclejdk8 6 | notifications: 7 | webhooks: 8 | urls: 9 | - https://webhooks.gitter.im/e/7e03eb850971919f740b 10 | on_success: change 11 | on_failure: always 12 | on_start: false 13 | after_success: 14 | - ! '[[ $TRAVIS_BRANCH == "develop" ]] && { sbt "+publish" ; };' 15 | env: 16 | global: 17 | - secure: VVyIsdj+WEXW6jwW7PxbGmlMmUYpy/qjH6vCvYgHK7Rrz/cX4GuT1FXE9xLnAZua2tWejkfBCSy+DPxAnDsVugAa0cvfHgA+i13/Zbm7LkH2PPvnIdjgl9LlhhxAHFr/OL8dKthPWw1U7wkFhCgFxN/OGytya0TItMOYiBG2cvk= 18 | - secure: QdGIhfPlMVPFR8e9REJwDLKbKwcleOTY5c+foJzLtyW3Y3yXt0jOzqdBUZqEMyTjvYDPcwabL6IMVp6vir3cIYLIxQx9eFofzAnwUfSOzJ7ASxdkPD5LZJXQHzHN+alskbyPF7y28kAuJEFF+FPlkH+3rSr7eBew3NAJqqUlpzI= 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with 4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 5 | 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 8 | language governing permissions and limitations under the License. 9 | 10 | Copyright 2014 Strong[Typed] (@StrongTyped) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is not maintained anymore and therefore it's archived. Feel free to contact the owner (@octonato), if you would like to have unarchive it and have it transfer to you. 2 | 3 | # ActiveSlick 4 | 5 | [![Build Status](https://travis-ci.org/strongtyped/active-slick.svg?branch=develop)](https://travis-ci.org/strongtyped/active-slick) 6 | 7 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/strongtyped/active-slick?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 8 | 9 | 10 | ActiveSlick is a library that offers CRUD operations for Slick 3.x projects. The main goal is to provide some basic operations to manage the lifecycle of persisted objects (new/persisted/deleted/stale) and enable the implementation of the Active Record Pattern on Slick mapped case classes. 11 | 12 | ### Documentation (WIP) 13 | http://www.strongtyped.io/active-slick/ 14 | 15 | ### Main features 16 | - Basic CRUD and auxiliary methods - add/update/save, delete, findById, count and fetchAll (backed by Reactive Streams). 17 | - Generic Id type. 18 | - Optimistic locking my means of incremental version. 19 | - Before insert and update hooks. 20 | - **ActiveRecord** trait to enable the Active Record Pattern on mapped case classes via class extensions (pimp-my-library style) 21 | 22 | ### Project artifact 23 | 24 | The artifacts are published to Sonatype Repository. Simply add the following to your build.sbt. 25 | 26 | As of version 0.3.x we don't support Slick 2.0 anymore. The differences between Slick 2.x and Slick 3.x are so huge that it makes impossible to support two versions. 27 | 28 | ```scala 29 | libraryDependencies += "io.strongtyped" %% "active-slick" % "0.3.5" 30 | ``` 31 | 32 | Source code for version 0.3.5 can be found at: 33 | https://github.com/strongtyped/active-slick/tree/v0.3.5 34 | 35 | 36 | The version supporting Slick 2.0 is still available on Sonatype Repo. However, this documentation only covers the current series (i.e.: 0.3.x). 37 | 38 | ```scala 39 | libraryDependencies += "io.strongtyped" %% "active-slick" % "0.2.2" 40 | ``` 41 | 42 | Source code for version 0.2.2 can be found at: 43 | https://github.com/strongtyped/active-slick/tree/v0.2.2 44 | 45 | ### Contribution policy 46 | 47 | Contributions via GitHub pull requests are gladly accepted from their original author. Along with any pull requests, please state that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so. 48 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | import BuildSettings._ 3 | 4 | scalaVersion in ThisBuild := "2.11.11" 5 | 6 | ivyScala := ivyScala.value map { 7 | _.copy(overrideScalaVersion = true) 8 | } 9 | 10 | organization in ThisBuild := "io.strongtyped" 11 | 12 | crossScalaVersions in ThisBuild := Seq("2.11.11", "2.12.2") 13 | 14 | parallelExecution in Test := false 15 | 16 | 17 | lazy val root = Project( 18 | id = "active-slick-root", 19 | base = file("."), 20 | settings = projSettings ++ Seq( 21 | publishArtifact := false 22 | ) 23 | ) aggregate(activeSlick, shapelessIntegration, samples) 24 | 25 | 26 | // Core ========================================== 27 | lazy val activeSlick = Project( 28 | id = "active-slick", 29 | base = file("modules/core"), 30 | settings = projSettings ++ mainDeps ++ testDeps 31 | ) 32 | //================================================ 33 | 34 | 35 | // Shapeless Integration ======================== 36 | lazy val shapelessIntegration = { 37 | 38 | // settings to include shapeless dependencies 39 | val shapeless = Seq( 40 | libraryDependencies ++= shapelessDeps 41 | ) 42 | 43 | Project( 44 | id = "active-slick-shapeless", 45 | base = file("modules/shapeless"), 46 | settings = projSettings ++ mainDeps ++ shapeless ++ testDeps 47 | ) dependsOn activeSlick 48 | } 49 | //================================================ 50 | 51 | 52 | // Samples ======================================= 53 | // contains examples used on the docs, not intended to be released 54 | lazy val samples = Project( 55 | id = "active-slick-samples", 56 | base = file("modules/samples"), 57 | settings = projSettings ++ Seq( 58 | publishArtifact := false, 59 | libraryDependencies ++= Seq( 60 | "com.typesafe.slick" %% "slick-codegen" % slickVersion, 61 | "com.h2database" % "h2" % "1.4.187", 62 | "org.slf4j" % "slf4j-nop" % "1.7.12", // To silence build warnings 63 | scalaTest 64 | ), 65 | slick <<= slickCodeGenTask, 66 | sourceGenerators in Compile <+= slickCodeGenTask 67 | ) ++ mainDeps 68 | ).dependsOn(activeSlick % "compile->compile;test->test") 69 | //======+========================================= 70 | 71 | lazy val slick = TaskKey[Seq[File]]("gen-tables") 72 | 73 | lazy val slickCodeGenTask = (sourceManaged, dependencyClasspath in Compile, runner in Compile, streams) map { (srcManaged, cp, r, s) => 74 | val pkg = "io.strongtyped.active.slick.docexamples.codegen" 75 | val url = "jdbc:h2:mem:test;INIT=runscript from 'modules/samples/src/main/resources/codegen_schema.sql'" // connection info for a pre-populated throw-away, in-memory db for this demo, which is freshly initialized on every run 76 | val jdbcDriver = "org.h2.Driver" 77 | val slickDriver = "slick.jdbc.H2Profile" 78 | toError(r.run("slick.codegen.SourceCodeGenerator", cp.files, Array(slickDriver, jdbcDriver, url, srcManaged.getPath, pkg), s.log)) 79 | val outputDir = srcManaged / pkg.replace(".", "/") 80 | Seq(outputDir / "Tables.scala") 81 | } -------------------------------------------------------------------------------- /jvmopts.travis: -------------------------------------------------------------------------------- 1 | -Xmx1024M 2 | -Xss2M 3 | -XX:+DoEscapeAnalysis 4 | -XX:+UseParallelGC 5 | -XX:+UseCompressedOops 6 | -XX:ReservedCodeCacheSize=128M 7 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/ActiveRecord.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import slick.dbio.DBIO 4 | import scala.concurrent.ExecutionContext 5 | 6 | 7 | abstract class ActiveRecord[R <: CrudActions](val repository: R) { 8 | 9 | def model: repository.Model 10 | 11 | def save()(implicit exc: ExecutionContext): DBIO[repository.Model] = 12 | repository.save(model) 13 | 14 | def update()(implicit exc: ExecutionContext): DBIO[repository.Model] = 15 | repository.update(model) 16 | 17 | def delete()(implicit exc: ExecutionContext): DBIO[Int] = repository.delete(model) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/CrudActions.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import slick.jdbc.JdbcProfile 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | /** 8 | * Define basic CRUD actions. 9 | * 10 | * This trait make no assumption about the presence of an Entity and a corresponding ID. 11 | * Therefore it can also be used for persistence of Value Objects. 12 | */ 13 | trait CrudActions { 14 | 15 | val jdbcProfile: JdbcProfile 16 | 17 | import jdbcProfile.api._ 18 | 19 | type Model 20 | // tag::adoc[] 21 | /** Returns total table count */ 22 | def count: DBIO[Int] 23 | 24 | /** Insert or Update a Model 25 | * Insert `Model` if not yet persisted, otherwise update it. 26 | * @return DBIO[Model] for a `Model` as persisted in the table. 27 | */ 28 | def save(entity: Model)(implicit exc: ExecutionContext): DBIO[Model] 29 | 30 | /** Update a `Model`. 31 | * @return DBIO[Model] for a `Model` as persisted in the table. 32 | */ 33 | def update(entity: Model)(implicit exc: ExecutionContext): DBIO[Model] 34 | 35 | /** Delete a `Model`. 36 | * @return DBIO[Int] with the number of affected rows 37 | */ 38 | def delete(entity: Model)(implicit exc: ExecutionContext): DBIO[Int] 39 | 40 | /** Fetch all elements from a table. 41 | * @param fetchSize - the number of row to fetch, defaults to 100 42 | * @return StreamingDBIO[Seq[Model], Model] 43 | */ 44 | def fetchAll(fetchSize: Int = 100) 45 | (implicit exc: ExecutionContext): StreamingDBIO[Seq[Model], Model] 46 | 47 | // end::adoc[] 48 | } 49 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/DBIOExtensions.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import io.strongtyped.active.slick.exceptions.{TooManyRowsAffectedException, NoRowsAffectedException} 4 | import slick.dbio.DBIO 5 | 6 | import scala.concurrent.ExecutionContext 7 | 8 | object DBIOExtensions { 9 | 10 | implicit class UpdateActionExtensionMethods(dbAction: DBIO[Int]) { 11 | 12 | def mustAffectOneSingleRow(implicit exc: ExecutionContext): DBIO[Int] = { 13 | dbAction.flatMap { 14 | case 1 => DBIO.successful(1) // expecting one result 15 | case 0 => DBIO.failed(NoRowsAffectedException) 16 | case n if n > 1 => DBIO.failed(new TooManyRowsAffectedException(affectedRowCount = n, expectedRowCount = 1)) 17 | } 18 | } 19 | 20 | def mustAffectAtLeastOneRow(implicit exc: ExecutionContext): DBIO[Int] = { 21 | 22 | dbAction.flatMap { 23 | case n if n >= 1 => DBIO.successful(n) // expecting one or more results 24 | case 0 => DBIO.failed(NoRowsAffectedException) 25 | } 26 | } 27 | } 28 | 29 | implicit class SelectSingleExtensionMethods[R](dbAction: DBIO[Seq[R]]) { 30 | 31 | def mustSelectSingleRecord(implicit exc: ExecutionContext): DBIO[R] = { 32 | dbAction.flatMap { 33 | case s if s.size == 1 => DBIO.successful(s.head) 34 | case s if s.isEmpty => DBIO.failed(NoRowsAffectedException) 35 | case s => DBIO.failed(new TooManyRowsAffectedException(affectedRowCount = s.size, expectedRowCount = 1)) 36 | } 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/EntityActions.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import io.strongtyped.active.slick.DBIOExtensions._ 4 | import io.strongtyped.active.slick.exceptions.{NoRowsAffectedException, RowNotFoundException} 5 | import slick.ast.BaseTypedType 6 | import slick.dbio.{FailureAction, SuccessAction} 7 | import slick.profile.RelationalProfile 8 | 9 | import scala.concurrent.ExecutionContext 10 | import scala.language.{existentials, higherKinds, implicitConversions} 11 | import scala.util.{Failure, Success} 12 | 13 | abstract class EntityActions extends EntityActionsLike { 14 | this: JdbcProfileProvider => 15 | 16 | import jdbcProfile.api._ 17 | 18 | def baseTypedType: BaseTypedType[Id] 19 | 20 | protected implicit lazy val btt: BaseTypedType[Id] = baseTypedType 21 | 22 | type EntityTable <: RelationalProfile#Table[Entity] 23 | 24 | def tableQuery: TableQuery[EntityTable] 25 | 26 | def $id(table: EntityTable): Rep[Id] 27 | 28 | def idLens: Lens[Entity, Option[Id]] 29 | 30 | override def count: DBIO[Int] = tableQuery.size.result 31 | 32 | override def findById(id: Id): DBIO[Entity] = 33 | filterById(id).result.head 34 | 35 | override def findOptionById(id: Id): DBIO[Option[Entity]] = 36 | filterById(id).result.headOption 37 | 38 | override def save(entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = { 39 | idLens.get(entity) match { 40 | // if has an Id, try to update it 41 | case Some(id) => update(entity) 42 | 43 | // if has no Id, try to add it 44 | case None => insert(entity).map { id => 45 | idLens.set(entity, Option(id)) 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * Before insert interceptor method. This method is called just before record insertion. 52 | * The default implementation returns a successful DBIO wrapping the passed entity. 53 | * 54 | * The returned `DBIOAction` is combined with the final insert `DBIOAction` 55 | * and 'marked' to run on the same transaction. 56 | * 57 | * Override this method if you need to add extract validation or modify the entity before insert. 58 | * 59 | * See examples bellow: 60 | * {{{ 61 | * // simple validation example 62 | * override def beforeInsert(foo: Foo)(implicit exc: ExecutionContext): DBIO[Foo] = { 63 | * if (foo.name.trim.isEmpty) { 64 | * DBIO.failed(new RuntimeException("Name can't be empty!!!") 65 | * } else { 66 | * DBIO.successful(foo) 67 | * } 68 | * } 69 | * }}} 70 | * 71 | * {{{ 72 | * // simple audit example 73 | * override def beforeInsert(foo: Foo)(implicit exc: ExecutionContext): DBIO[Foo] = { 74 | * // ensure that created and lastUpdate fields are updated just before insert 75 | * val audited = foo.copy(created = DateTime.now, lastUpdate = DateTime.now) 76 | * DBIO.successful(audited) 77 | * } 78 | * }}} 79 | */ 80 | def beforeInsert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = { 81 | // default implementation does nothing 82 | DBIO.successful(entity) 83 | } 84 | 85 | 86 | /** 87 | * Before update interceptor method. This method is called just before record update. 88 | * The default implementation returns a successful DBIO wrapping the passed entity. 89 | * 90 | * The returned `DBIOAction` is combined with the final update `DBIOAction` 91 | * and 'marked' to run on the same transaction. 92 | * 93 | * Override this method if you need to add extract validation or modify the entity before update. 94 | * 95 | * See examples bellow: 96 | * 97 | * {{{ 98 | * // simple validation example 99 | * override def beforeUpdate(id: Int, foo: Foo)(implicit exc: ExecutionContext): DBIO[Foo] = { 100 | * findById(id).flatMap { oldFoo => 101 | * if (oldFoo.name != foo.name) { 102 | * DBIO.failed(new RuntimeException("Can't modify name!!!") 103 | * } else { 104 | * DBIO.successful(foo) 105 | * } 106 | * } 107 | * } 108 | * }}} 109 | * 110 | * {{{ 111 | * // simple audit example 112 | * override def beforeUpdate(id: Int, foo: Foo)(implicit exc: ExecutionContext): DBIO[Foo] = { 113 | * // ensure that lastUpdate fields are updated just before update 114 | * val audited = foo.copy(lastUpdate = DateTime.now) 115 | * DBIO.successful(audited) 116 | * } 117 | * }}} 118 | */ 119 | def beforeUpdate(id: Id, entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = { 120 | // default implementation does nothing 121 | DBIO.successful(entity) 122 | } 123 | 124 | override def insert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Id] = { 125 | val action = beforeInsert(entity).flatMap { preparedModel => 126 | tableQuery.returning(tableQuery.map($id)) += preparedModel 127 | } 128 | // beforeInsert and '+=' must run on same tx 129 | action.transactionally 130 | } 131 | 132 | override def fetchAll(fetchSize: Int = 100)(implicit exc: ExecutionContext): StreamingDBIO[Seq[Entity], Entity] = { 133 | tableQuery 134 | .result 135 | .transactionally 136 | .withStatementParameters(fetchSize = fetchSize) 137 | } 138 | 139 | override def update(entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = { 140 | val action = 141 | for { 142 | id <- tryExtractId(entity) 143 | preparedModel <- beforeUpdate(id, entity) 144 | updatedModel <- update(id, preparedModel) 145 | } yield updatedModel 146 | 147 | // beforeUpdate and update must run on same tx 148 | action.transactionally 149 | } 150 | 151 | 152 | protected def update(id: Id, entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = { 153 | 154 | val triedUpdate = filterById(id).update(entity).mustAffectOneSingleRow.asTry 155 | 156 | triedUpdate.flatMap { 157 | case Success(_) => DBIO.successful(entity) 158 | case Failure(NoRowsAffectedException) => DBIO.failed(new RowNotFoundException(entity)) 159 | case Failure(ex) => DBIO.failed(ex) 160 | } 161 | 162 | } 163 | 164 | override def delete(entity: Entity)(implicit exc: ExecutionContext): DBIO[Int] = { 165 | tryExtractId(entity).flatMap { id => 166 | deleteById(id) 167 | } 168 | } 169 | 170 | def deleteById(id: Id)(implicit exc: ExecutionContext): DBIO[Int] = { 171 | filterById(id).delete.mustAffectOneSingleRow 172 | } 173 | 174 | 175 | private def tryExtractId(entity: Entity): DBIO[Id] = { 176 | idLens.get(entity) match { 177 | case Some(id) => SuccessAction(id) 178 | case None => FailureAction(new RowNotFoundException(entity)) 179 | } 180 | } 181 | 182 | def filterById(id: Id) = tableQuery.filter($id(_) === id) 183 | 184 | 185 | } -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/EntityActionsLike.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import slick.ast.BaseTypedType 4 | import slick.jdbc.JdbcProfile 5 | 6 | import scala.concurrent.ExecutionContext 7 | 8 | /** 9 | * Define basic life cycle actions for a Entity that involve. 10 | * 11 | */ 12 | trait EntityActionsLike extends CrudActions { 13 | 14 | val jdbcProfile: JdbcProfile 15 | 16 | import jdbcProfile.api._ 17 | 18 | /** The type of the Entity */ 19 | type Entity 20 | /** The `Entity`'s Id type */ 21 | type Id 22 | /** CrudActions.Model is the `Entity` in this context */ 23 | type Model = Entity 24 | 25 | // tag::adoc[] 26 | /** Insert a new `Entity` 27 | * @return DBIO[Id] for the generated `Id` 28 | */ 29 | def insert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Id] 30 | 31 | /** Delete a `Entity` by `Id` 32 | * @return DBIO[Int] with the number of affected rows 33 | */ 34 | def deleteById(id: Id)(implicit exc: ExecutionContext): DBIO[Int] 35 | 36 | /** Finds `Entity` referenced by `Id`. 37 | * May fail if no `Entity` is found for passed `Id` 38 | * @return DBIO[Entity] for the `Entity` 39 | */ 40 | def findById(id: Id): DBIO[Entity] 41 | 42 | /** Finds `Entity` referenced by `Id` optionally. 43 | * @return DBIO[Option[Entity]] for the `Entity` 44 | */ 45 | def findOptionById(id: Id): DBIO[Option[Entity]] 46 | 47 | // end::adoc[] 48 | } 49 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/JdbcProfileProvider.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import slick.jdbc.{DerbyProfile, SQLiteProfile, MySQLProfile, HsqldbProfile, PostgresProfile, H2Profile, JdbcProfile} 4 | 5 | 6 | trait JdbcProfileProvider { 7 | type JP <: JdbcProfile 8 | val jdbcProfile: JP 9 | } 10 | 11 | object JdbcProfileProvider { 12 | 13 | trait H2ProfileProvider extends JdbcProfileProvider { 14 | override type JP = H2Profile 15 | override val jdbcProfile: H2Profile = H2Profile 16 | } 17 | 18 | trait PostgresProfileProvider extends JdbcProfileProvider { 19 | override type JP = PostgresProfile 20 | override val jdbcProfile: PostgresProfile = PostgresProfile 21 | } 22 | 23 | 24 | trait DerbyProfileProvider extends JdbcProfileProvider { 25 | override type JP = DerbyProfile 26 | override val jdbcProfile: DerbyProfile = DerbyProfile 27 | } 28 | 29 | trait HsqlProfileProvider extends JdbcProfileProvider { 30 | override type JP = HsqldbProfile 31 | override val jdbcProfile: HsqldbProfile = HsqldbProfile 32 | } 33 | 34 | trait MySQLProfileProvider extends JdbcProfileProvider { 35 | override type JP = MySQLProfile 36 | override val jdbcProfile: MySQLProfile = MySQLProfile 37 | } 38 | 39 | trait SQLLiteProfileProvider extends JdbcProfileProvider { 40 | override type JP = SQLiteProfile 41 | override val jdbcProfile: SQLiteProfile = SQLiteProfile 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/Lens.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | /** A dead simple lens to provide a getter and a setter for 4 | * lifecycle model/table fields like 'id' and 'version'. 5 | * 6 | * This implementation is by no means target as a general Lens library. It's sole purpose is 7 | * to provide an abstraction for setting and getting first level fields on an arbitrary model. 8 | * 9 | * @param get function mapping O -> V 10 | * where O is the Object we want to 'look' inside and V the value of the field we want to extract 11 | * 12 | * @param set function to assign a the a new value to field affect by this Lens. 13 | */ 14 | case class Lens[O, V](get: O => V, set: (O, V) => O) 15 | 16 | object Lens { 17 | 18 | /** 19 | * Convenience method to allow creation of a Lens using curried syntax. 20 | * {{{ 21 | * case class Coffee(id:Int, name:String) 22 | * 23 | * val idLens = lens { coffee: Coffee => coffee.id } 24 | * { (coffee, id) => coffee.copy(id = id) } 25 | * }}} 26 | */ 27 | def lens[O, V](get: O => V)(set: (O, V) => O) = Lens(get, set) 28 | } -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/OptimisticLocking.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import io.strongtyped.active.slick.exceptions.{NoRowsAffectedException, StaleObjectStateException} 4 | import io.strongtyped.active.slick.DBIOExtensions._ 5 | import scala.concurrent.ExecutionContext 6 | import scala.util.{Failure, Success} 7 | 8 | trait OptimisticLocking { 9 | self: EntityActions => 10 | 11 | 12 | import self.jdbcProfile.api._ 13 | 14 | def $version(table: EntityTable): Rep[Long] 15 | 16 | def versionLens: Lens[self.Entity, Long] 17 | 18 | override protected def update(id: self.Id, versionable: self.Entity)(implicit exc: ExecutionContext): DBIO[self.Entity] = { 19 | 20 | // extract current version 21 | val currentVersion = versionLens.get(versionable) 22 | 23 | // build a query selecting entity with current version 24 | val queryByIdAndVersion = self.filterById(id).filter($version(_) === currentVersion) 25 | 26 | // model with incremented version 27 | val modelWithNewVersion = versionLens.set(versionable, currentVersion + 1) 28 | 29 | val tryUpdate = queryByIdAndVersion.update(modelWithNewVersion).mustAffectOneSingleRow.asTry 30 | 31 | // in case of failure, we want a more meaningful exception ie: StaleObjectStateException 32 | tryUpdate.flatMap { 33 | case Success(_) => DBIO.successful(modelWithNewVersion) 34 | case Failure(NoRowsAffectedException) => DBIO.failed(new StaleObjectStateException(versionable)) 35 | case Failure(e) => DBIO.failed(e) 36 | } 37 | } 38 | 39 | override def save(versionable: self.Entity)(implicit exc: ExecutionContext): DBIO[self.Entity] = { 40 | self.idLens.get(versionable) match { 41 | // if has an Id, try to update it 42 | case Some(id) => update(id, versionable) 43 | 44 | // if has no Id, try to add it 45 | case None => 46 | // initialize versioning 47 | val modelWithVersion = versionLens.set(versionable, 1) 48 | self.insert(modelWithVersion).map { id => idLens.set(modelWithVersion, Option(id)) } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/SchemaManagement.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | trait SchemaManagement { 4 | self: EntityActions => 5 | 6 | import jdbcProfile.api._ 7 | 8 | def createSchema: DBIO[Unit] = tableQuery.schema.create 9 | 10 | def dropSchema: DBIO[Unit] = tableQuery.schema.drop 11 | } 12 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/ActiveSlickException.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.exceptions 2 | 3 | class ActiveSlickException(msg: String) extends RuntimeException(msg) 4 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/NoRowsAffectedException.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.exceptions 2 | 3 | object NoRowsAffectedException extends ActiveSlickException("No rows affected") 4 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/RowNotFoundException.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.exceptions 2 | 3 | class RowNotFoundException[T](notFoundRecord: T) extends ActiveSlickException(s"Row not found: $notFoundRecord") -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/StaleObjectStateException.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.exceptions 2 | 3 | case class StaleObjectStateException[T](staleObject: T) 4 | extends ActiveSlickException(s"Optimistic locking error - object in stale state: $staleObject") -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/exceptions/TooManyRowsAffectedException.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.exceptions 2 | 3 | class TooManyRowsAffectedException(affectedRowCount: Int, expectedRowCount: Int) 4 | extends ActiveSlickException(s"Expected $expectedRowCount row(s) affected, got $affectedRowCount instead") 5 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/io/strongtyped/active/slick/package.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active 2 | 3 | package object slick { 4 | 5 | type H2ProfileProvider = JdbcProfileProvider.H2ProfileProvider 6 | type PostgresProfileProvider = JdbcProfileProvider.PostgresProfileProvider 7 | type DerbyProfileProvider = JdbcProfileProvider.DerbyProfileProvider 8 | type HsqlProfileProvider = JdbcProfileProvider.HsqlProfileProvider 9 | type MySQLProfileProvider = JdbcProfileProvider.MySQLProfileProvider 10 | type SQLLiteProfileProvider = JdbcProfileProvider.SQLLiteProfileProvider 11 | } 12 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/io/strongtyped/active/slick/BeerTest.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import io.strongtyped.active.slick.exceptions.RowNotFoundException 4 | import io.strongtyped.active.slick.test.H2Suite 5 | import org.scalatest._ 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scala.language.postfixOps 8 | 9 | class BeerTest extends FlatSpec with H2Suite with Schema { 10 | 11 | behavior of "A Beer" 12 | 13 | it should "be persistable" in { 14 | val (supplier, beer) = 15 | rollback { 16 | for { 17 | supplier <- Supplier("Acme, Inc.").save() 18 | beer <- Beer("Abc", supplier.id.get, 3.2).save() 19 | beerSupplier <- beer.supplier() 20 | } yield { 21 | beerSupplier.value shouldBe supplier 22 | supplier.id shouldBe defined 23 | (supplier, beer) 24 | } 25 | } 26 | } 27 | 28 | it should "not be persisted with an id chosen by a user" in { 29 | val (supplier, triedBeer) = 30 | rollback { 31 | for { 32 | supplier <- Supplier("Acme, Inc.").save() 33 | beer <- Beer("Abc", supplier.id.get, 3.2, Some(10)).save().asTry 34 | } yield (supplier, beer) 35 | } 36 | 37 | supplier.id shouldBe defined 38 | triedBeer.failure.exception shouldBe a[RowNotFoundException[_]] 39 | } 40 | 41 | 42 | override def createSchemaAction: jdbcProfile.api.DBIO[Unit] = { 43 | jdbcProfile.api.DBIO.seq(Suppliers.createSchema, Beers.createSchema) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/io/strongtyped/active/slick/CrudTest.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import io.strongtyped.active.slick.test.H2Suite 4 | import org.scalatest.FlatSpec 5 | import slick.ast.BaseTypedType 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scala.language.postfixOps 8 | import io.strongtyped.active.slick.Lens._ 9 | 10 | class CrudTest extends FlatSpec with H2Suite with JdbcProfileProvider { 11 | 12 | behavior of "An EntityDao (CRUD)" 13 | 14 | it should "support all CRUD operations" in { 15 | rollback { 16 | for { 17 | // collect initial count 18 | initialCount <- Foos.count 19 | 20 | // save new entry 21 | savedEntry <- Foo("Foo").save() 22 | 23 | // count again, must be initialCount + 1 24 | count <- Foos.count 25 | 26 | // update entry 27 | updatedEntry <- savedEntry.copy(name = "Bar").save() 28 | 29 | // find it back from DB 30 | found <- Foos.findById(savedEntry.id.get) 31 | 32 | // delete it 33 | _ <- found.delete() 34 | 35 | // count total one more time 36 | finalCount <- Foos.count 37 | } yield { 38 | 39 | // check that we can add new entry 40 | count shouldBe (initialCount + 1) 41 | 42 | // check entity properties 43 | savedEntry.id shouldBe 'defined 44 | savedEntry.name shouldBe "Foo" 45 | 46 | // found entry must be a 'Bar' 47 | found.name shouldBe "Bar" 48 | 49 | // after delete finalCount must equal initialCount 50 | finalCount shouldBe initialCount 51 | 52 | savedEntry 53 | } 54 | } 55 | } 56 | 57 | override def createSchemaAction = { 58 | Foos.createSchema 59 | } 60 | 61 | 62 | case class Foo(name: String, id: Option[Int] = None) 63 | 64 | class FooDao extends EntityActions with H2ProfileProvider { 65 | 66 | import jdbcProfile.api._ 67 | 68 | val baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]] 69 | 70 | type Entity = Foo 71 | type Id = Int 72 | 73 | class FooTable(tag: Tag) extends Table[Foo](tag, "FOO_CRUD_TEST") { 74 | 75 | def name = column[String]("NAME") 76 | 77 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc) 78 | 79 | def * = (name, id.?) <>(Foo.tupled, Foo.unapply) 80 | } 81 | 82 | type EntityTable = FooTable 83 | val tableQuery = TableQuery[EntityTable] 84 | 85 | def $id(table: FooTable) = table.id 86 | 87 | val idLens = lens { foo: Foo => foo.id } { (entry, id) => entry.copy(id = id) } 88 | 89 | def createSchema = { 90 | import jdbcProfile.api._ 91 | tableQuery.schema.create 92 | } 93 | } 94 | 95 | val Foos = new FooDao 96 | 97 | 98 | implicit class EntryExtensions(val model: Foo) extends ActiveRecord(Foos) 99 | 100 | } -------------------------------------------------------------------------------- /modules/core/src/test/scala/io/strongtyped/active/slick/EntityActionsBeforeInsertUpdateTest.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import io.strongtyped.active.slick.test.H2Suite 4 | import org.scalatest.FlatSpec 5 | import slick.ast.BaseTypedType 6 | import io.strongtyped.active.slick.Lens._ 7 | import scala.concurrent.ExecutionContext 8 | 9 | class EntityActionsBeforeInsertUpdateTest 10 | extends FlatSpec with H2Suite with JdbcProfileProvider { 11 | 12 | behavior of "An EntityDao with validation " 13 | 14 | it should "return an error if beforeInsert is not successful" in { 15 | 16 | import scala.concurrent.ExecutionContext.Implicits.global 17 | 18 | val result = 19 | rollback { 20 | for { 21 | result <- Foo(" ").save().asTry 22 | } yield result 23 | } 24 | 25 | result.failure.exception shouldBe a[NameShouldNotBeEmptyException] 26 | } 27 | 28 | it should "return an error if beforeUpdate is not successful" in { 29 | 30 | import scala.concurrent.ExecutionContext.Implicits.global 31 | 32 | val result = 33 | rollback { 34 | 35 | val finalAction = 36 | for { 37 | savedEntry <- Foo("abc").save() 38 | // update name should fail according to beforeUpdate method definition 39 | updatedEntry <- savedEntry.copy(name = "Bar").save() 40 | } yield updatedEntry 41 | 42 | finalAction.asTry 43 | } 44 | 45 | result.failure.exception shouldBe a[NameCanNotBeModifiedException] 46 | } 47 | 48 | 49 | override def createSchemaAction = { 50 | Foos.createSchema 51 | } 52 | 53 | 54 | case class Foo(name: String, id: Option[Int] = None) 55 | 56 | class NameShouldNotBeEmptyException extends RuntimeException("Name should not be empty") 57 | 58 | class NameCanNotBeModifiedException extends RuntimeException("Name can not be modified") 59 | 60 | class FooDao extends EntityActions with H2ProfileProvider { 61 | 62 | import jdbcProfile.api._ 63 | 64 | val baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]] 65 | 66 | type EntityTable = FooTable 67 | type Entity = Foo 68 | type Id = Int 69 | 70 | class FooTable(tag: Tag) extends Table[Foo](tag, "FOO_VALIDATION_TEST") { 71 | 72 | def name = column[String]("NAME") 73 | 74 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc) 75 | 76 | def * = (name, id.?) <>(Foo.tupled, Foo.unapply) 77 | 78 | } 79 | 80 | val tableQuery = TableQuery[FooTable] 81 | 82 | def $id(table: FooTable) = table.id 83 | 84 | val idLens = lens { foo: Foo => foo.id } { (entry, id) => entry.copy(id = id) } 85 | 86 | //@formatter:off 87 | // tag::adoc[] 88 | override def beforeInsert(model: Foo) 89 | (implicit exc: ExecutionContext): DBIO[Foo] = { 90 | if (model.name.trim.isEmpty) { 91 | DBIO.failed(new NameShouldNotBeEmptyException) 92 | } else { 93 | DBIO.successful(model) 94 | } 95 | } 96 | 97 | override def beforeUpdate(id: Int, model: Foo) 98 | (implicit exc: ExecutionContext): DBIO[Foo] = { 99 | findById(id).flatMap { oldModel => 100 | if (oldModel.name != model.name) { 101 | DBIO.failed(new NameCanNotBeModifiedException) 102 | } else { 103 | DBIO.successful(model) 104 | } 105 | } 106 | } 107 | // end::adoc[] 108 | //@formatter:on 109 | 110 | def createSchema = { 111 | import jdbcProfile.api._ 112 | tableQuery.schema.create 113 | } 114 | } 115 | 116 | val Foos = new FooDao 117 | 118 | 119 | implicit class EntryExtensions(val model: Foo) extends ActiveRecord(Foos) 120 | 121 | } 122 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/io/strongtyped/active/slick/Schema.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import io.strongtyped.active.slick.Lens._ 4 | 5 | import slick.ast.BaseTypedType 6 | 7 | import scala.language.existentials 8 | 9 | trait Schema extends JdbcProfileProvider { 10 | 11 | case class Supplier(name: String, version: Long = 0, id: Option[Int] = None) 12 | 13 | case class Beer(name: String, 14 | supID: Int, 15 | price: Double, 16 | id: Option[Int] = None) 17 | 18 | class SupplierDao extends EntityActions 19 | with OptimisticLocking 20 | with SchemaManagement 21 | with H2ProfileProvider { 22 | 23 | import jdbcProfile.api._ 24 | 25 | val baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]] 26 | 27 | type Entity = Supplier 28 | type Id = Int 29 | type EntityTable = SuppliersTable 30 | 31 | class SuppliersTable(tag: Tag) extends Table[Supplier](tag, "SUPPLIERS") { 32 | 33 | def version = column[Long]("VERSION") 34 | 35 | def name = column[String]("SUPPLIER_NAME") 36 | 37 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc) 38 | 39 | def * = (name, version, id.?) <> (Supplier.tupled, Supplier.unapply) 40 | 41 | } 42 | 43 | val tableQuery = TableQuery[EntityTable] 44 | 45 | def $id(table: EntityTable) = table.id 46 | 47 | val idLens = lens { supp: Supplier => supp.id } { (supp, id) => supp.copy(id = id) } 48 | 49 | def $version(table: EntityTable) = table.version 50 | 51 | val versionLens = lens { supp: Supplier => supp.version } { (supp, version) => supp.copy(version = version) } 52 | 53 | } 54 | 55 | val Suppliers = new SupplierDao 56 | 57 | implicit class SupplierRecord(val model: Supplier) extends ActiveRecord(Suppliers) 58 | 59 | class BeersDao extends EntityActions with SchemaManagement with H2ProfileProvider { 60 | 61 | import jdbcProfile.api._ 62 | 63 | val baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]] 64 | 65 | type Entity = Beer 66 | type Id = Int 67 | type EntityTable = BeersTable 68 | 69 | // Beer Table, DAO and Record extension 70 | class BeersTable(tag: Tag) extends Table[Beer](tag, "BEERS") { 71 | 72 | def name = column[String]("BEER_NAME") 73 | 74 | def supID = column[Int]("SUP_ID") 75 | 76 | def price = column[Double]("PRICE") 77 | 78 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc) 79 | 80 | def * = (name, supID, price, id.?) <> (Beer.tupled, Beer.unapply) 81 | 82 | def supplier = foreignKey("SUP_FK", supID, Suppliers.tableQuery)(_.id) 83 | } 84 | 85 | val tableQuery = TableQuery[EntityTable] 86 | 87 | def $id(table: EntityTable) = table.id 88 | 89 | val idLens = lens { beer: Beer => beer.id } { (beer, id) => beer.copy(id = id) } 90 | 91 | } 92 | 93 | val Beers = new BeersDao 94 | 95 | implicit class BeerRecord(val model: Beer) extends ActiveRecord(Beers) { 96 | 97 | def supplier() = Suppliers.findOptionById(model.supID) 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /modules/core/src/test/scala/io/strongtyped/active/slick/SupplierTest.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick 2 | 3 | import java.sql.SQLException 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import io.strongtyped.active.slick.exceptions.StaleObjectStateException 6 | import io.strongtyped.active.slick.test.H2Suite 7 | import org.scalatest._ 8 | import slick.dbio.DBIO 9 | 10 | class SupplierTest extends FlatSpec with H2Suite with Schema { 11 | 12 | behavior of "A Supplier" 13 | 14 | it should "be persistable" in { 15 | val initialCount = query(Suppliers.count) 16 | 17 | val supplier = Supplier("Acme, Inc.") 18 | supplier.id should not be defined 19 | 20 | val savedSupplier = 21 | commit { 22 | supplier.save() 23 | } 24 | savedSupplier.id shouldBe defined 25 | 26 | val countAfterSave = query(Suppliers.count) 27 | countAfterSave shouldBe (initialCount + 1) 28 | 29 | commit(savedSupplier.delete()) 30 | 31 | val countAfterDelete = query(Suppliers.count) 32 | countAfterDelete shouldBe initialCount 33 | 34 | } 35 | 36 | it should "be versionable" in { 37 | 38 | val supplier = Supplier("abc") 39 | // no version yet 40 | supplier.version shouldBe 0 41 | 42 | val persistedSupp = commit(supplier.save()) 43 | persistedSupp.version should not be 0 44 | 45 | // modify two versions and try to persist them 46 | val suppWithNewVersion = commit(persistedSupp.copy(name = "abc1").save()) 47 | 48 | intercept[StaleObjectStateException[Supplier]] { 49 | // supplier was persisted in the mean time, so version must be different by now 50 | commit(persistedSupp.copy(name = "abc2").save()) 51 | } 52 | 53 | // supplier with new version can be persisted again 54 | commit(suppWithNewVersion.copy(name = "abc").save()) 55 | } 56 | 57 | it should "return an error when deleting a supplier with beers linked to it" in { 58 | 59 | val deleteResult = 60 | rollback { 61 | for { 62 | supplier <- Supplier("Acme, Inc.").save() 63 | beer <- Beer("Abc", supplier.id.get, 3.2).save() 64 | deleteResult <- supplier.delete().asTry 65 | } yield deleteResult 66 | } 67 | 68 | deleteResult.failure.exception shouldBe a[SQLException] 69 | } 70 | 71 | override def createSchemaAction: jdbcProfile.api.DBIO[Unit] = { 72 | jdbcProfile.api.DBIO.seq(Suppliers.createSchema, Beers.createSchema) 73 | } 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/io/strongtyped/active/slick/test/DbSuite.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.test 2 | 3 | import io.strongtyped.active.slick.JdbcProfileProvider 4 | import org.scalatest._ 5 | import slick.basic.DatabasePublisher 6 | 7 | import scala.concurrent.duration.{FiniteDuration, _} 8 | import scala.concurrent.{Await, ExecutionContext} 9 | import scala.language.postfixOps 10 | import scala.util.{Failure, Success} 11 | 12 | trait DbSuite extends BeforeAndAfterAll with Matchers with OptionValues with TryValues { 13 | 14 | self: Suite with JdbcProfileProvider => 15 | 16 | import jdbcProfile.api._ 17 | 18 | def setupDb: jdbcProfile.backend.DatabaseDef 19 | 20 | private lazy val database: jdbcProfile.backend.DatabaseDef = setupDb 21 | 22 | override protected def afterAll(): Unit = { 23 | database.close() 24 | } 25 | 26 | def query[T](dbAction: DBIO[T])(implicit ex: ExecutionContext, timeout: FiniteDuration = 5 seconds): T = 27 | runAction(dbAction) 28 | 29 | def stream[T](dbAction: StreamingDBIO[T, T])(implicit ex: ExecutionContext, timeout: FiniteDuration = 5 seconds): DatabasePublisher[T] = { 30 | database.stream(dbAction.transactionally) 31 | } 32 | 33 | def commit[T](dbAction: DBIO[T])(implicit ex: ExecutionContext, timeout: FiniteDuration = 5 seconds): T = 34 | runAction(dbAction.transactionally) 35 | 36 | def rollback[T](dbAction: DBIO[T])(implicit ex: ExecutionContext, timeout: FiniteDuration = 5 seconds): T = { 37 | 38 | case class RollbackException(expected: T) extends RuntimeException("rollback exception") 39 | 40 | val markedForRollback = dbAction.flatMap { result => 41 | DBIO 42 | .failed(RollbackException(result)) 43 | .map(_ => result) // map back to T 44 | }.transactionally.asTry 45 | 46 | val finalAction = 47 | markedForRollback.map { 48 | case Success(result) => result 49 | case Failure(RollbackException(result)) => result 50 | case Failure(other) => throw other 51 | } 52 | 53 | runAction(finalAction) 54 | } 55 | 56 | private def runAction[T](dbAction: DBIO[T])(implicit ex: ExecutionContext, timeout: FiniteDuration): T = { 57 | val result = database.run(dbAction) 58 | Await.result(result, timeout) 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /modules/core/src/test/scala/io/strongtyped/active/slick/test/H2Suite.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.test 2 | 3 | import io.strongtyped.active.slick.H2ProfileProvider 4 | import org.scalatest.Suite 5 | 6 | import scala.concurrent.Await 7 | import scala.concurrent.duration._ 8 | import scala.language.postfixOps 9 | 10 | trait H2Suite extends DbSuite with H2ProfileProvider { 11 | self: Suite => 12 | 13 | import jdbcProfile.api._ 14 | 15 | def createSchemaAction: DBIO[Unit] = DBIO.successful(()) 16 | 17 | def timeout = 5 seconds 18 | 19 | override def setupDb: jdbcProfile.backend.DatabaseDef = { 20 | // each test suite gets its own isolated DB 21 | val dbUrl = s"jdbc:h2:mem:${this.getClass.getSimpleName};DB_CLOSE_DELAY=-1" 22 | val db = Database.forURL(dbUrl, driver = "org.h2.Driver") 23 | val result = db.run(createSchemaAction.transactionally) 24 | Await.result(result, timeout) 25 | db 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/samples/src/main/resources/codegen_schema.sql: -------------------------------------------------------------------------------- 1 | create table if not exists "Computers" (id bigint auto_increment not null, name varchar not null); 2 | -------------------------------------------------------------------------------- /modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/ActiveSlickWithCodegen.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.docexamples 2 | 3 | import io.strongtyped.active.slick.{ActiveRecord, JdbcProfileProvider, Lens, EntityActions} 4 | import io.strongtyped.active.slick.docexamples.codegen.Tables 5 | import slick.ast.BaseTypedType 6 | 7 | /** 8 | * Shows how to configure active-slick with a schema that was generated via slick-codegen. In this case, 9 | * A schema is generated on build as the object io.strongtyped.active.slick.docexamples.codegen.Tables. 10 | * See codegen_schema.sql for the schema that feeds into the codegen. 11 | */ 12 | object ActiveSlickWithCodegen { 13 | 14 | abstract class ComputersRepo(tables: Tables) extends EntityActions with JdbcProfileProvider { 15 | // 16 | // Implement JdbcProfileProvider with JDBCProfile from generated Tables.scala 17 | // 18 | override type JP = Tables.profile.type 19 | // Sucks that this is necessary. Did we have to define this type in JdbcProfileProvider? Why not just use JdbcProfile? 20 | override val jdbcProfile: JP = Tables.profile 21 | 22 | // 23 | // Implement EntityActions 24 | // 25 | 26 | import jdbcProfile.api._ 27 | 28 | type Entity = Tables.ComputersRow 29 | type Id = Long 30 | type EntityTable = Tables.Computers 31 | 32 | val baseTypedType = implicitly[BaseTypedType[Id]] 33 | val tableQuery = Tables.Computers 34 | val idLens: Lens[Tables.ComputersRow, Option[Long]] = { 35 | // For the getter, use 0L as a sentinel value because generated ID is usually non-optional 36 | Lens.lens { row: Tables.ComputersRow => if (row.id == 0L) None else Some(row.id) } { (row, maybeId) => maybeId map { id => row.copy(id = id) } getOrElse row } 37 | } 38 | 39 | override def $id(table: EntityTable): Rep[Long] = { 40 | table.id 41 | } 42 | 43 | implicit class EntryExtensions(val model: Tables.ComputersRow) extends ActiveRecord(ComputersRepo) 44 | 45 | } 46 | 47 | object ComputersRepo extends ComputersRepo(Tables) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/MappingWithActiveSlick.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.docexamples 2 | 3 | //@formatter:off 4 | // tag::adoc[] 5 | import io.strongtyped.active.slick._ 6 | import slick.ast.BaseTypedType 7 | import io.strongtyped.active.slick.Lens._ 8 | import scala.language.postfixOps 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | 11 | object MappingWithActiveSlick { 12 | 13 | case class Coffee(name: String, id: Option[Int] = None) 14 | 15 | 16 | 17 | object CoffeeRepo extends EntityActions with H2ProfileProvider { 18 | 19 | import jdbcProfile.api._ // #<1> 20 | val baseTypedType = implicitly[BaseTypedType[Id]] // #<2> 21 | 22 | type Entity = Coffee // #<3> 23 | type Id = Int // #<4> 24 | type EntityTable = CoffeeTable // # <5> 25 | 26 | val tableQuery = TableQuery[CoffeeTable] // # <6> 27 | 28 | def $id(table: CoffeeTable): Rep[Id] = table.id // # <7> 29 | 30 | val idLens = lens { coffee: Coffee => coffee.id } // # <8> 31 | { (coffee, id) => coffee.copy(id = id) } 32 | 33 | class CoffeeTable(tag: Tag) extends Table[Coffee](tag, "COFFEE") { // #<9> 34 | def name = column[String]("NAME") 35 | def id = column[Id]("ID", O.PrimaryKey, O.AutoInc) 36 | def * = (name, id.?) <>(Coffee.tupled, Coffee.unapply) 37 | } 38 | 39 | def findByName(name:String): DBIO[Seq[Coffee]] = { 40 | tableQuery.filter(_.name === name).result 41 | } 42 | 43 | } 44 | 45 | implicit class EntryExtensions(val model: Coffee) extends ActiveRecord(CoffeeRepo) 46 | 47 | val saveAction = Coffee("Colombia").save() 48 | } 49 | // end::adoc[] 50 | //@formatter:on 51 | -------------------------------------------------------------------------------- /modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/MappingWithoutActiveSlick.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.docexamples 2 | 3 | import scala.language.postfixOps 4 | 5 | //@formatter:off 6 | object MappingWithoutActiveSlick { 7 | 8 | // tag::adoc[] 9 | import slick.jdbc.H2Profile.api._ 10 | 11 | case class Coffee(name: String, id: Option[Int] = None) // #<1> 12 | 13 | class CoffeeTable(tag: Tag) extends Table[Coffee](tag, "COFFEE") { 14 | def name = column[String]("NAME") 15 | def id = column[Int]("ID", O.PrimaryKey, O.AutoInc) // #<2> 16 | def * = (name, id.?) <>(Coffee.tupled, Coffee.unapply) 17 | } 18 | 19 | val Coffees = TableQuery[CoffeeTable] 20 | 21 | val coffee = Coffee("Colombia") 22 | val insertAction = Coffees.returning(Coffees.map(_.id)) += coffee // #<3> 23 | // end::adoc[] 24 | } 25 | //@formatter:on -------------------------------------------------------------------------------- /modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/OptimisticLockingExample.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.docexamples 2 | 3 | 4 | import io.strongtyped.active.slick._ 5 | import slick.ast.BaseTypedType 6 | import io.strongtyped.active.slick.Lens._ 7 | import scala.language.postfixOps 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | object OptimisticLockingExample { 11 | 12 | //@formatter:off 13 | // tag::adoc[] 14 | case class Coffee(name: String, version: Long = 0, id: Option[Int] = None) 15 | 16 | object CoffeeRepo extends EntityActions with OptimisticLocking with H2ProfileProvider { 17 | 18 | import jdbcProfile.api._ 19 | 20 | def $version(table: CoffeeTable): Rep[Long] = table.version // #<1> 21 | def versionLens = lens { coffee:Coffee => coffee.version } // #<2> 22 | { (coffee, vers) => coffee.copy(version = vers) } 23 | 24 | 25 | class CoffeeTable(tag: Tag) extends Table[Coffee](tag, "COFFEE") { 26 | def name = column[String]("NAME") 27 | def id = column[Id]("ID", O.PrimaryKey, O.AutoInc) 28 | def version = column[Long]("VERSION") 29 | def * = (name, version, id.?) <>(Coffee.tupled, Coffee.unapply) 30 | } 31 | // end::adoc[] 32 | //@formatter:on 33 | 34 | val baseTypedType = implicitly[BaseTypedType[Id]] 35 | 36 | type Entity = Coffee 37 | type Id = Int 38 | type EntityTable = CoffeeTable 39 | 40 | val tableQuery = TableQuery[CoffeeTable] 41 | 42 | def $id(table: CoffeeTable): Rep[Id] = table.id 43 | 44 | val idLens = lens { coffee: Coffee => coffee.id } { (coffee, id) => coffee.copy(id = id) } 45 | 46 | 47 | def findByName(name: String): DBIO[Seq[Coffee]] = { 48 | tableQuery.filter(_.name === name).result 49 | } 50 | } 51 | 52 | implicit class EntryExtensions(val model: Coffee) extends ActiveRecord(CoffeeRepo) { 53 | 54 | } 55 | 56 | val saveAction = Coffee("Colombia").save() 57 | } 58 | -------------------------------------------------------------------------------- /modules/samples/src/test/scala/io/strongtyped/active/slick/docexamples/ActiveSlickWithCodegenTest.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.docexamples 2 | 3 | import io.strongtyped.active.slick.docexamples.ActiveSlickWithCodegen.ComputersRepo 4 | import io.strongtyped.active.slick.docexamples.ActiveSlickWithCodegen.ComputersRepo.EntryExtensions 5 | import io.strongtyped.active.slick.test.H2Suite 6 | import org.scalatest._ 7 | import ComputersRepo.EntryExtensions 8 | import io.strongtyped.active.slick.docexamples.codegen.Tables.ComputersRow 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | import scala.language.postfixOps 11 | 12 | class ActiveSlickWithCodegenTest extends FlatSpec with H2Suite with OptionValues { 13 | 14 | "ActiveSlickWithCodegen" should "provide crud and active record semantics for generated tables" in { 15 | 16 | // At the beginning the Computers DB should be completely empty 17 | query(ComputersRepo.findOptionById(1)) shouldBe None 18 | query(ComputersRepo.count) shouldBe 0 19 | 20 | // Add a Computer without default ID 0. Make sure it gets an ID and all data are stored 21 | val newComputer = ComputersRow(id = 0L, name = "Macbook Pro 15") 22 | val savedComputer = commit(newComputer.save()) 23 | savedComputer.id should be(1L) 24 | savedComputer.name should be("Macbook Pro 15") 25 | query(ComputersRepo.count) shouldBe 1 26 | 27 | // Query the Computer we saved, update it, and delete it. 28 | query(ComputersRepo.findById(1L)) shouldBe savedComputer 29 | 30 | val updatedComputer = commit(savedComputer.copy(name = "MBP 15").save()) 31 | updatedComputer.id should be(savedComputer.id) 32 | updatedComputer.name should be("MBP 15") 33 | 34 | commit(updatedComputer.delete()) shouldBe 1 35 | query(ComputersRepo.findOptionById(updatedComputer.id)) shouldBe None 36 | 37 | } 38 | 39 | override def setupDb: jdbcProfile.backend.DatabaseDef = { 40 | import jdbcProfile.api._ 41 | val db = Database.forURL( 42 | url = s"jdbc:h2:mem:${this.getClass.getSimpleName};AUTOCOMMIT=TRUE;INIT=runscript from 'modules/samples/src/main/resources/codegen_schema.sql'", 43 | driver = "org.h2.Driver" 44 | ) 45 | db.createSession().conn.setAutoCommit(true) 46 | db 47 | } 48 | 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /modules/shapeless/src/main/scala/io/strongtyped/active/slick/shapeless/ShapelessSlick.scala: -------------------------------------------------------------------------------- 1 | package io.strongtyped.active.slick.shapeless 2 | 3 | import io.strongtyped.active.slick.Lens 4 | import shapeless._ 5 | import scala.language.implicitConversions 6 | 7 | object ShapelessSlick { 8 | 9 | /** 10 | * Converts a Shapeless lens to ActiveSlick's Lens 11 | */ 12 | implicit def shapelessToSimple[O, V](lens: shapeless.Lens[O, V]): Lens[O, V] = { 13 | Lens( 14 | obj => lens.get(obj), 15 | (obj, value) => lens.set(obj)(value) 16 | ) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /project/BuildSettings.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import sbt.Keys._ 3 | import com.typesafe.sbt.SbtScalariform 4 | import xerial.sbt.Sonatype.SonatypeKeys._ 5 | import xerial.sbt.Sonatype.sonatypeSettings 6 | 7 | object BuildSettings { 8 | 9 | val projSettings = Seq( 10 | scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-Xlint") 11 | ) ++ 12 | SbtScalariform.defaultScalariformSettings ++ 13 | sonatypePublishSettings 14 | 15 | def sonatypePublishSettings = sonatypeSettings ++ Seq( 16 | pomExtra := https://github.com/strongtyped/active-slick 17 | 18 | 19 | Apache-style 20 | http://www.apache.org/licenses/LICENSE-2.0 21 | repo 22 | 23 | 24 | 25 | https://github.com/strongtyped/active-slick.git 26 | scm:git:git@github.com:strongtyped/active-slick.git 27 | 28 | 29 | 30 | @renatocaval 31 | Renato Cavalcanti 32 | http://www.strongtyped.io/ 33 | 34 | 35 | ) 36 | } -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | 4 | object Dependencies { 5 | val slickVersion = "3.2.0" 6 | val slick = "com.typesafe.slick" %% "slick" % slickVersion 7 | 8 | val shapelessDeps = Seq("com.chuusai" %% "shapeless" % "2.3.2") 9 | /*Def setting ( 10 | CrossVersion partialVersion scalaVersion.value match { 11 | case Some((2, scalaMajor)) if scalaMajor >= 11 => 12 | Seq("com.chuusai" %% "shapeless" % "2.3.1") 13 | case Some((2, 10)) => 14 | Seq( 15 | "com.chuusai" % "shapeless" % "2.1.0" cross CrossVersion.full, 16 | compilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) 17 | ) 18 | } 19 | )*/ 20 | 21 | 22 | val scalaTest = "org.scalatest" %% "scalatest" % "3.0.1" % "test" 23 | val h2database = "com.h2database" % "h2" % "1.4.181" % "test" 24 | 25 | val mainDeps = Seq( 26 | libraryDependencies ++= Seq(slick) 27 | ) 28 | 29 | val testDeps = Seq( 30 | libraryDependencies ++= Seq(h2database, scalaTest) 31 | ) 32 | } -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0") 2 | 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "0.5.1") 4 | 5 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 6 | -------------------------------------------------------------------------------- /project/sbt-ui.sbt: -------------------------------------------------------------------------------- 1 | // This plugin represents functionality that is to be added to sbt in the future 2 | 3 | addSbtPlugin("org.scala-sbt" % "sbt-core-next" % "0.1.1") -------------------------------------------------------------------------------- /scripts/test-shapeless.scala: -------------------------------------------------------------------------------- 1 | import shapeless._ 2 | import shapeless.Lens 3 | 4 | case class Foo(name:String, id:Int) 5 | 6 | def fooLens(lensId: Lens[Foo, Int])(foo:Foo) = println(lensId.get(foo)) 7 | 8 | val foo = Foo("John", 1) 9 | val idLens = lens[Foo].id 10 | 11 | fooLens(lens[Foo] >> 'id)(foo) 12 | fooLens(idLens)(foo) 13 | 14 | -------------------------------------------------------------------------------- /sonatype.sbt: -------------------------------------------------------------------------------- 1 | sonatypeProfileName := "io.strongtyped" 2 | 3 | credentials in Global ++= (for { 4 | username <- Option(System.getenv().get("SONATYPE_USERNAME")) 5 | password <- Option(System.getenv().get("SONATYPE_PASSWORD")) 6 | } yield Credentials("Sonatype Nexus Repository Manager", "oss.sonatype.org", username, password)).toSeq 7 | 8 | 9 | pomExtra in Global := { 10 | https://github.com/strongtyped/active-slick 11 | 12 | 13 | Apache-style 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | repo 16 | 17 | 18 | 19 | https://github.com/strongtyped/active-slick.git 20 | scm:git:git@github.com:strongtyped/active-slick.git 21 | 22 | 23 | 24 | @renatocaval 25 | Renato Cavalcanti 26 | http://www.strongtyped.io/ 27 | 28 | 29 | } 30 | 31 | publishTo in Global := { 32 | val nexus = "https://oss.sonatype.org/" 33 | if (isSnapshot.value) 34 | Some("snapshots" at nexus + "content/repositories/snapshots") 35 | else 36 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 37 | } -------------------------------------------------------------------------------- /src/asciidoctor/foundation-blue.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.2 | MIT License | git.io/normalize */ 2 | /* ========================================================================== HTML5 display definitions ========================================================================== */ 3 | /** Correct `block` display not defined in IE 8/9. */ 4 | article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } 5 | 6 | /** Correct `inline-block` display not defined in IE 8/9. */ 7 | audio, canvas, video { display: inline-block; } 8 | 9 | /** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ 10 | audio:not([controls]) { display: none; height: 0; } 11 | 12 | /** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ 13 | [hidden], template { display: none; } 14 | 15 | script { display: none !important; } 16 | 17 | /* ========================================================================== Base ========================================================================== */ 18 | /** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ 19 | html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } 20 | 21 | /** Remove default margin. */ 22 | body { margin: 0; } 23 | 24 | /* ========================================================================== Links ========================================================================== */ 25 | /** Remove the gray background color from active links in IE 10. */ 26 | a { background: transparent; } 27 | 28 | /** Address `outline` inconsistency between Chrome and other browsers. */ 29 | a:focus { outline: thin dotted; } 30 | 31 | /** Improve readability when focused and also mouse hovered in all browsers. */ 32 | a:active, a:hover { outline: 0; } 33 | 34 | /* ========================================================================== Typography ========================================================================== */ 35 | /** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ 36 | h1 { font-size: 2em; margin: 0.67em 0; } 37 | 38 | /** Address styling not present in IE 8/9, Safari 5, and Chrome. */ 39 | abbr[title] { border-bottom: 1px dotted; } 40 | 41 | /** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ 42 | b, strong { font-weight: bold; } 43 | 44 | /** Address styling not present in Safari 5 and Chrome. */ 45 | dfn { font-style: italic; } 46 | 47 | /** Address differences between Firefox and other browsers. */ 48 | hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } 49 | 50 | /** Address styling not present in IE 8/9. */ 51 | mark { background: #ff0; color: #000; } 52 | 53 | /** Correct font family set oddly in Safari 5 and Chrome. */ 54 | code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } 55 | 56 | /** Improve readability of pre-formatted text in all browsers. */ 57 | pre { white-space: pre-wrap; } 58 | 59 | /** Set consistent quote types. */ 60 | q { quotes: "\201C" "\201D" "\2018" "\2019"; } 61 | 62 | /** Address inconsistent and variable font size in all browsers. */ 63 | small { font-size: 80%; } 64 | 65 | /** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ 66 | sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } 67 | 68 | sup { top: -0.5em; } 69 | 70 | sub { bottom: -0.25em; } 71 | 72 | /* ========================================================================== Embedded content ========================================================================== */ 73 | /** Remove border when inside `a` element in IE 8/9. */ 74 | img { border: 0; } 75 | 76 | /** Correct overflow displayed oddly in IE 9. */ 77 | svg:not(:root) { overflow: hidden; } 78 | 79 | /* ========================================================================== Figures ========================================================================== */ 80 | /** Address margin not present in IE 8/9 and Safari 5. */ 81 | figure { margin: 0; } 82 | 83 | /* ========================================================================== Forms ========================================================================== */ 84 | /** Define consistent border, margin, and padding. */ 85 | fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } 86 | 87 | /** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ 88 | legend { border: 0; /* 1 */ padding: 0; /* 2 */ } 89 | 90 | /** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ 91 | button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } 92 | 93 | /** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ 94 | button, input { line-height: normal; } 95 | 96 | /** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ 97 | button, select { text-transform: none; } 98 | 99 | /** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ 100 | button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } 101 | 102 | /** Re-set default cursor for disabled elements. */ 103 | button[disabled], html input[disabled] { cursor: default; } 104 | 105 | /** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ 106 | input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } 107 | 108 | /** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ 109 | input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } 110 | 111 | /** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ 112 | input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } 113 | 114 | /** Remove inner padding and border in Firefox 4+. */ 115 | button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } 116 | 117 | /** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ 118 | textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } 119 | 120 | /* ========================================================================== Tables ========================================================================== */ 121 | /** Remove most spacing between table cells. */ 122 | table { border-collapse: collapse; border-spacing: 0; } 123 | 124 | meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; } 125 | 126 | meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; } 127 | 128 | meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; } 129 | 130 | *, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } 131 | 132 | html, body { font-size: 100%; } 133 | 134 | body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; } 135 | 136 | a:hover { cursor: pointer; } 137 | 138 | img, object, embed { max-width: 100%; height: auto; } 139 | 140 | object, embed { height: 100%; } 141 | 142 | img { -ms-interpolation-mode: bicubic; } 143 | 144 | #map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } 145 | 146 | .left { float: left !important; } 147 | 148 | .right { float: right !important; } 149 | 150 | .text-left { text-align: left !important; } 151 | 152 | .text-right { text-align: right !important; } 153 | 154 | .text-center { text-align: center !important; } 155 | 156 | .text-justify { text-align: justify !important; } 157 | 158 | .hide { display: none; } 159 | 160 | .antialiased, body { -webkit-font-smoothing: antialiased; } 161 | 162 | img { display: inline-block; vertical-align: middle; } 163 | 164 | textarea { height: auto; min-height: 50px; } 165 | 166 | select { width: 100%; } 167 | 168 | p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; } 169 | 170 | .subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #980050; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } 171 | 172 | /* Typography resets */ 173 | div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; } 174 | 175 | /* Default Link Styles */ 176 | a { color: #647da1; text-decoration: none; line-height: inherit; } 177 | a:hover, a:focus { color: #640035; } 178 | a img { border: none; } 179 | 180 | /* Default paragraph styles */ 181 | p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; } 182 | p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; } 183 | 184 | /* Default header styles */ 185 | h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; color: #647da1; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; } 186 | h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: gray; line-height: 0; } 187 | 188 | h1 { font-size: 2.125em; } 189 | 190 | h2 { font-size: 1.6875em; } 191 | 192 | h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; } 193 | 194 | h4 { font-size: 1.125em; } 195 | 196 | h5 { font-size: 1.125em; } 197 | 198 | h6 { font-size: 1em; } 199 | 200 | hr { border: dotted #cccccc; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; } 201 | 202 | /* Helpful Typography Defaults */ 203 | em, i { font-style: italic; line-height: inherit; } 204 | 205 | strong, b { font-weight: bold; line-height: inherit; } 206 | 207 | small { font-size: 60%; line-height: inherit; } 208 | 209 | code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #320348; } 210 | 211 | /* Lists */ 212 | ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; } 213 | 214 | ul, ol { margin-left: 1.5em; } 215 | ul.no-bullet, ol.no-bullet { margin-left: 1.5em; } 216 | 217 | /* Unordered Lists */ 218 | ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ } 219 | ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; } 220 | ul.square { list-style-type: square; } 221 | ul.circle { list-style-type: circle; } 222 | ul.disc { list-style-type: disc; } 223 | ul.no-bullet { list-style: none; } 224 | 225 | /* Ordered Lists */ 226 | ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; } 227 | 228 | /* Definition Lists */ 229 | dl dt { margin-bottom: 0.3125em; font-weight: bold; } 230 | dl dd { margin-bottom: 1.25em; } 231 | 232 | /* Abbreviations */ 233 | abbr, acronym { text-transform: uppercase; font-size: 90%; color: #555555; border-bottom: 1px dotted #dddddd; cursor: help; } 234 | 235 | abbr { text-transform: none; } 236 | 237 | /* Blockquotes */ 238 | blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #efefef; } 239 | blockquote cite { display: block; font-size: 0.8125em; color: #5e5e5e; } 240 | blockquote cite:before { content: "\2014 \0020"; } 241 | blockquote cite a, blockquote cite a:visited { color: #5e5e5e; } 242 | 243 | blockquote, blockquote p { line-height: 1.6; color: #777777; } 244 | 245 | /* Microformats */ 246 | .vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; } 247 | .vcard li { margin: 0; display: block; } 248 | .vcard .fn { font-weight: bold; font-size: 0.9375em; } 249 | 250 | .vevent .summary { font-weight: bold; } 251 | .vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; } 252 | 253 | @media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } 254 | h1 { font-size: 2.75em; } 255 | h2 { font-size: 2.3125em; } 256 | h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } 257 | h4 { font-size: 1.4375em; } } 258 | /* Tables */ 259 | table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } 260 | table thead, table tfoot { background: whitesmoke; font-weight: normal; } 261 | table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #333333; text-align: left; } 262 | table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #555555; } 263 | table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } 264 | table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; } 265 | 266 | h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } 267 | 268 | .clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } 269 | .clearfix:after, .float-group:after { clear: both; } 270 | 271 | *:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; } 272 | 273 | pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; } 274 | 275 | .keyseq { color: #888888; } 276 | 277 | kbd { display: inline-block; color: #555555; font-size: 0.75em; line-height: 1.4; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: -0.15em 0.15em 0 0.15em; padding: 0.2em 0.6em 0.2em 0.5em; vertical-align: middle; white-space: nowrap; } 278 | 279 | .keyseq kbd:first-child { margin-left: 0; } 280 | 281 | .keyseq kbd:last-child { margin-right: 0; } 282 | 283 | .menuseq, .menu { color: #3b3b3b; } 284 | 285 | b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } 286 | 287 | b.button:before { content: "["; padding: 0 3px 0 2px; } 288 | 289 | b.button:after { content: "]"; padding: 0 2px 0 3px; } 290 | 291 | #header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } 292 | #header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } 293 | #header:after, #content:after, #footnotes:after, #footer:after { clear: both; } 294 | 295 | #content { margin-top: 1.25em; } 296 | 297 | #content:before { content: none; } 298 | 299 | #header > h1:first-child { color: #647da1; margin-top: 2.25rem; margin-bottom: 0; } 300 | #header > h1:first-child + #toc { margin-top: 8px; border-top: 1px dotted #cccccc; } 301 | #header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px dotted #cccccc; padding-bottom: 8px; } 302 | #header .details { border-bottom: 1px dotted #cccccc; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #5e5e5e; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; } 303 | #header .details span:first-child { margin-left: -0.125em; } 304 | #header .details span.email a { color: #777777; } 305 | #header .details br { display: none; } 306 | #header .details br + span:before { content: "\00a0\2013\00a0"; } 307 | #header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #777777; } 308 | #header .details br + span#revremark:before { content: "\00a0|\00a0"; } 309 | #header #revnumber { text-transform: capitalize; } 310 | #header #revnumber:after { content: "\00a0"; } 311 | 312 | #content > h1:first-child:not([class]) { color: #647da1; border-bottom: 1px dotted #cccccc; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; } 313 | 314 | #toc { border-bottom: 1px solid #cccccc; padding-bottom: 0.5em; } 315 | #toc > ul { margin-left: 0.125em; } 316 | #toc ul.sectlevel0 > li > a { font-style: italic; } 317 | #toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; } 318 | #toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; } 319 | #toc a { text-decoration: none; } 320 | #toc a:active { text-decoration: underline; } 321 | 322 | #toctitle { color: #980050; font-size: 1.2em; } 323 | 324 | @media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; } 325 | body.toc2 { padding-left: 15em; padding-right: 0; } 326 | #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #cccccc; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } 327 | #toc.toc2 #toctitle { margin-top: 0; font-size: 1.2em; } 328 | #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; } 329 | #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } 330 | #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } 331 | body.toc2.toc-right { padding-left: 0; padding-right: 15em; } 332 | body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #cccccc; left: auto; right: 0; } } 333 | @media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } 334 | #toc.toc2 { width: 20em; } 335 | #toc.toc2 #toctitle { font-size: 1.375em; } 336 | #toc.toc2 > ul { font-size: 0.95em; } 337 | #toc.toc2 ul ul { padding-left: 1.25em; } 338 | body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } 339 | #content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } 340 | #content #toc > :first-child { margin-top: 0; } 341 | #content #toc > :last-child { margin-bottom: 0; } 342 | 343 | #footer { max-width: 100%; background-color: #272727; padding: 1.25em; } 344 | 345 | #footer-text { color: #bcbcbc; line-height: 1.44; } 346 | 347 | .sect1 { padding-bottom: 0.625em; } 348 | 349 | @media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } } 350 | .sect1 + .sect1 { border-top: 1px solid #cccccc; } 351 | 352 | #content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; } 353 | #content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; } 354 | #content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; } 355 | #content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #333333; text-decoration: none; } 356 | #content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #262626; } 357 | 358 | .audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; } 359 | 360 | .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; } 361 | 362 | table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } 363 | 364 | .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: #647da1; } 365 | 366 | table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } 367 | 368 | .admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; } 369 | .admonitionblock > table td.icon { text-align: center; width: 80px; } 370 | .admonitionblock > table td.icon img { max-width: none; } 371 | .admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; } 372 | .admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px dotted #cccccc; color: #5e5e5e; } 373 | .admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } 374 | 375 | .exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; } 376 | .exampleblock > .content > :first-child { margin-top: 0; } 377 | .exampleblock > .content > :last-child { margin-bottom: 0; } 378 | 379 | .sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } 380 | .sidebarblock > :first-child { margin-top: 0; } 381 | .sidebarblock > :last-child { margin-bottom: 0; } 382 | .sidebarblock > .content > .title { color: #980050; margin-top: 0; } 383 | 384 | .exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } 385 | 386 | .literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #efefef; } 387 | .sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; } 388 | 389 | .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.75em 0.75em 0.625em 0.75em; font-size: 0.8125em; } 390 | .literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } 391 | @media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } } 392 | @media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } } 393 | 394 | .literalblock.output pre { color: #efefef; background-color: black; } 395 | 396 | .listingblock pre.highlightjs { padding: 0; } 397 | .listingblock pre.highlightjs > code { padding: 0.75em 0.75em 0.625em 0.75em; -webkit-border-radius: 0; border-radius: 0; } 398 | 399 | .listingblock > .content { position: relative; } 400 | 401 | .listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; } 402 | 403 | .listingblock:hover code[data-lang]:before { display: block; } 404 | 405 | .listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } 406 | 407 | .listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; } 408 | 409 | table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; } 410 | 411 | table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; } 412 | 413 | table.pyhltable td.code { padding-left: .75em; padding-right: 0; } 414 | 415 | pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #cccccc; } 416 | 417 | pre.pygments .lineno { display: inline-block; margin-right: .25em; } 418 | 419 | table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; } 420 | 421 | .quoteblock { margin: 0 1em 1.25em 1.5em; display: table; } 422 | .quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; } 423 | .quoteblock blockquote, .quoteblock blockquote p { color: #777777; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; } 424 | .quoteblock blockquote { margin: 0; padding: 0; border: 0; } 425 | .quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #980050; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } 426 | .quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } 427 | .quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; } 428 | .quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #5e5e5e; } 429 | .quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; } 430 | .quoteblock .quoteblock blockquote:before { display: none; } 431 | 432 | .verseblock { margin: 0 1em 1.25em 1em; } 433 | .verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #777777; font-weight: 300; text-rendering: optimizeLegibility; } 434 | .verseblock pre strong { font-weight: 400; } 435 | .verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; } 436 | 437 | .quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; } 438 | .quoteblock .attribution br, .verseblock .attribution br { display: none; } 439 | .quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.05em; color: #5e5e5e; } 440 | 441 | .quoteblock.abstract { margin: 0 0 1.25em 0; display: block; } 442 | .quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; } 443 | .quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; } 444 | 445 | table.tableblock { max-width: 100%; border-collapse: separate; } 446 | table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } 447 | 448 | table.spread { width: 100%; } 449 | 450 | table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } 451 | 452 | table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } 453 | 454 | table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } 455 | 456 | table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } 457 | 458 | table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } 459 | 460 | table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } 461 | 462 | table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } 463 | 464 | table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } 465 | 466 | table.frame-all { border-width: 1px; } 467 | 468 | table.frame-sides { border-width: 0 1px; } 469 | 470 | table.frame-topbot { border-width: 1px 0; } 471 | 472 | th.halign-left, td.halign-left { text-align: left; } 473 | 474 | th.halign-right, td.halign-right { text-align: right; } 475 | 476 | th.halign-center, td.halign-center { text-align: center; } 477 | 478 | th.valign-top, td.valign-top { vertical-align: top; } 479 | 480 | th.valign-bottom, td.valign-bottom { vertical-align: bottom; } 481 | 482 | th.valign-middle, td.valign-middle { vertical-align: middle; } 483 | 484 | table thead th, table tfoot th { font-weight: normal; } 485 | 486 | tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; } 487 | 488 | tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #333333; font-weight: normal; } 489 | 490 | p.tableblock > code:only-child { background: none; padding: 0; } 491 | 492 | p.tableblock { font-size: 1em; } 493 | 494 | td > div.verse { white-space: pre; } 495 | 496 | ol { margin-left: 1.75em; } 497 | 498 | ul li ol { margin-left: 1.5em; } 499 | 500 | dl dd { margin-left: 1.125em; } 501 | 502 | dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; } 503 | 504 | ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; } 505 | 506 | ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } 507 | 508 | ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } 509 | 510 | ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; } 511 | 512 | ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; } 513 | 514 | ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } 515 | ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } 516 | ul.inline > li > * { display: block; } 517 | 518 | .unstyled dl dt { font-weight: normal; font-style: normal; } 519 | 520 | ol.arabic { list-style-type: decimal; } 521 | 522 | ol.decimal { list-style-type: decimal-leading-zero; } 523 | 524 | ol.loweralpha { list-style-type: lower-alpha; } 525 | 526 | ol.upperalpha { list-style-type: upper-alpha; } 527 | 528 | ol.lowerroman { list-style-type: lower-roman; } 529 | 530 | ol.upperroman { list-style-type: upper-roman; } 531 | 532 | ol.lowergreek { list-style-type: lower-greek; } 533 | 534 | .hdlist > table, .colist > table { border: 0; background: none; } 535 | .hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } 536 | 537 | td.hdlist1 { padding-right: .75em; font-weight: bold; } 538 | 539 | td.hdlist1, td.hdlist2 { vertical-align: top; } 540 | 541 | .literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } 542 | 543 | .colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; } 544 | .colist > table tr > td:last-of-type { padding: 0.25em 0; } 545 | 546 | .thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } 547 | 548 | .imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } 549 | .imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } 550 | .imageblock > .title { margin-bottom: 0; } 551 | .imageblock.thumb, .imageblock.th { border-width: 6px; } 552 | .imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } 553 | 554 | .image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } 555 | .image.left { margin-right: 0.625em; } 556 | .image.right { margin-left: 0.625em; } 557 | 558 | a.image { text-decoration: none; } 559 | 560 | span.footnote, span.footnoteref { vertical-align: super; font-size: 0.875em; } 561 | span.footnote a, span.footnoteref a { text-decoration: none; } 562 | span.footnote a:active, span.footnoteref a:active { text-decoration: underline; } 563 | 564 | #footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } 565 | #footnotes hr { width: 20%; min-width: 6.25em; margin: -.25em 0 .75em 0; border-width: 1px 0 0 0; } 566 | #footnotes .footnote { padding: 0 0.375em; line-height: 1.3; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.2em; margin-bottom: .2em; } 567 | #footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } 568 | #footnotes .footnote:last-of-type { margin-bottom: 0; } 569 | 570 | #content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } 571 | 572 | .gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; } 573 | .gist .file-data > table td.line-data { width: 99%; } 574 | 575 | div.unbreakable { page-break-inside: avoid; } 576 | 577 | .big { font-size: larger; } 578 | 579 | .small { font-size: smaller; } 580 | 581 | .underline { text-decoration: underline; } 582 | 583 | .overline { text-decoration: overline; } 584 | 585 | .line-through { text-decoration: line-through; } 586 | 587 | .aqua { color: #00bfbf; } 588 | 589 | .aqua-background { background-color: #00fafa; } 590 | 591 | .black { color: black; } 592 | 593 | .black-background { background-color: black; } 594 | 595 | .blue { color: #0000bf; } 596 | 597 | .blue-background { background-color: #0000fa; } 598 | 599 | .fuchsia { color: #bf00bf; } 600 | 601 | .fuchsia-background { background-color: #fa00fa; } 602 | 603 | .gray { color: #606060; } 604 | 605 | .gray-background { background-color: #7d7d7d; } 606 | 607 | .green { color: #006000; } 608 | 609 | .green-background { background-color: #007d00; } 610 | 611 | .lime { color: #00bf00; } 612 | 613 | .lime-background { background-color: #00fa00; } 614 | 615 | .maroon { color: #600000; } 616 | 617 | .maroon-background { background-color: #7d0000; } 618 | 619 | .navy { color: #000060; } 620 | 621 | .navy-background { background-color: #00007d; } 622 | 623 | .olive { color: #606000; } 624 | 625 | .olive-background { background-color: #7d7d00; } 626 | 627 | .purple { color: #600060; } 628 | 629 | .purple-background { background-color: #7d007d; } 630 | 631 | .red { color: #bf0000; } 632 | 633 | .red-background { background-color: #fa0000; } 634 | 635 | .silver { color: #909090; } 636 | 637 | .silver-background { background-color: #bcbcbc; } 638 | 639 | .teal { color: #006060; } 640 | 641 | .teal-background { background-color: #007d7d; } 642 | 643 | .white { color: #bfbfbf; } 644 | 645 | .white-background { background-color: #fafafa; } 646 | 647 | .yellow { color: #bfbf00; } 648 | 649 | .yellow-background { background-color: #fafa00; } 650 | 651 | span.icon > .fa { cursor: default; } 652 | 653 | .admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; } 654 | .admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #850046; } 655 | .admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; } 656 | .admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; } 657 | .admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; } 658 | .admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; } 659 | 660 | .conum[data-value] { display: inline-block; color: #fff !important; background-color: #555555; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; } 661 | .conum[data-value] * { color: #fff !important; } 662 | .conum[data-value] + b { display: none; } 663 | .conum[data-value]:after { content: attr(data-value); } 664 | pre .conum[data-value] { position: relative; top: -0.125em; } 665 | 666 | b.conum * { color: inherit !important; } 667 | 668 | .conum:not([data-value]):empty { display: none; } 669 | 670 | #header > h1 { border-bottom-style: solid; } 671 | 672 | #toctitle { color: #333333; } 673 | 674 | .listingblock pre, .literalblock pre { background: -moz-linear-gradient(top, white 0%, #f4f4f4 100%); background: -webkit-linear-gradient(top, white 0%, #f4f4f4 100%); background: linear-gradient(to bottom, #ffffff 0%, #f4f4f4 100%); -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); } 675 | 676 | .sidebarblock { background: none; border: none; -webkit-box-shadow: 0 0 5px rgba(177, 0, 93, 0.5); box-shadow: 0 0 5px rgba(177, 0, 93, 0.5); } 677 | .sidebarblock > .content > .title { color: #181818; } 678 | -------------------------------------------------------------------------------- /src/asciidoctor/index.adoc: -------------------------------------------------------------------------------- 1 | = ActiveSlick 2 | :toc: macro 3 | :linkcss: 4 | :icons: font 5 | :source-highlighter: pygments 6 | :pygments-style: monokai 7 | :pygments-linenums-mode: inline 8 | :author: Renato Cavalcanti - Strong[Typed] 9 | 10 | 11 | ActiveSlick is a library that offers CRUD operations for Slick 3.x projects. The main goal is to provide some basic operations to manage the lifecycle of persisted objects (new/persisted/deleted/stale) and enable the implementation of the Active Record Pattern on Slick mapped case classes. 12 | 13 | toc::[] 14 | 15 | == Main features 16 | - Basic CRUD and auxiliary methods - add/update/save, delete, findById, count and fetchAll (backed by Reactive Streams). 17 | - Generic Id type. 18 | - Optimistic locking my means of incremental version. 19 | - Before insert and update hooks. 20 | - **ActiveRecord** to enable the Active Record Pattern on mapped case classes via class extensions (pimp-my-library style) 21 | 22 | == Project artifact 23 | 24 | The artifacts are published to Sonatype Repository. Simply add the following to your build.sbt. 25 | 26 | As of version 0.3.x we don't support Slick 2.0 anymore. The differences between Slick 2.x and Slick 3.x are so huge that it makes impossible to support two versions. 27 | 28 | [source,scala] 29 | ---- 30 | libraryDependencies += "io.strongtyped" %% "active-slick" % "0.3.5" 31 | ---- 32 | Source code for version 0.3.5 can be found at: 33 | https://github.com/strongtyped/active-slick/tree/v0.3.5 34 | 35 | 36 | The version supporting Slick 2.0 is still available on Sonatype Repo. However, this documentation only covers the current series (i.e.: 0.3.x). 37 | [source,scala] 38 | ---- 39 | libraryDependencies += "io.strongtyped" %% "active-slick" % "0.2.2" 40 | ---- 41 | 42 | Source code for version 0.2.2 can be found at: 43 | https://github.com/strongtyped/active-slick/tree/v0.2.2 44 | 45 | 46 | == Motivation 47 | 48 | Slick is able to map result sets to case classes or Tuples because of its isomorphism. This is done thanks to built-in Scala features. However, there is no direct link between case class fields and database columns. Everything is done based on isomorphic projections. 49 | 50 | As a consequence, managing of Entities IDs must be done by hand, over and over again. One needs to save a model, ask Slick to return the generated ID and add it explicitly to the case class. 51 | 52 | The following code fragment demonstrates how this is typically done in a Slick application. 53 | 54 | 55 | [source,scala] 56 | ---- 57 | include::../../modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/MappingWithoutActiveSlick.scala[tags=adoc] 58 | ---- 59 | 60 | <1> Both **Coffee** and **CoffeeTable** have an **Id** representing the primary key. **Coffee** has a field of type **Option[Int]** 61 | <2> **CoffeeTable** has a method returning a **Rep[Int]**. 62 | <3> However, in order to inserta new Coffee we need some boilerplate to get back the generated ID. 63 | 64 | If we manage to connect both **ID** we can provide some generic functionality to manage Entities. Inserting a new `Entity` is only one of the possible use cases. With a well known ID, we can distinguish if an `Entity` has been already persisted or not, we can easilly implement byId methods (`deleteById`, `findById`) and we can add extensions methods like: `save`, `udpate`, `delete`. 65 | 66 | 67 | == History 68 | The first intention of this project was to implement Slick DAOs or Repositories using only Scala features and the Scala compiler (no code generation). 69 | 70 | In the time of Slick 2.0, this library required the usage of the Cake Pattern to wire together all the parts wihtout pre-defining a driver. Since Slick 3.0 imposes a huge refactoring, we decided to turn ActiveSlick inside out and eliminate the need of the Cake Pattern. 71 | 72 | As a result, all special traits/classes like `TableId`, `TableWithIdQuery`, `EntityTableQuery`, etc, are gone. These classes could only compile if a driver was in scope. However, only the final user of the library can decide which driver to use. To overcome that situation we had to define everything inside components and wire them all together in a cake. 73 | 74 | This approach was way to complex and hard to document. Users had to understand how to compose the traits and which classes to implement and/or mixin in order the get the desired effect. 75 | 76 | From v.0.3.x onwards, one can build a DAO/Repository and enable an ActiveRecord extension using plain old Slick mappings. Some configuration is needed, but it's far more obvious to configure it than to compose and bake a cake. 77 | 78 | == EntityActions 79 | 80 | `EntityActions` is the main class we can use to build a DAO/Repository. It offers default implmentation for the following methods. 81 | 82 | 83 | 84 | [source,scala] 85 | ---- 86 | include::../../modules/core/src/main/scala/io/strongtyped/active/slick/CrudActions.scala[tags=adoc] 87 | 88 | include::../../modules/core/src/main/scala/io/strongtyped/active/slick/EntityActionsLike.scala[tags=adoc] 89 | ---- 90 | 91 | TIP: `EntityActions` implements two basic traits: `CrudActions` and `EntityActionsLike`. All methods involving a `Model` are `Id` agnostic and are defined in `CrudActions`. Methods involving an `Entity` are defined by `EntityActionsLike` 92 | 93 | === Mapping using ActiveSlick 94 | 95 | The following code fragment illustrates how we can use ActiveSlick `EntityActions` to bind the `Entity` `Id` field and the `Table Id column`. 96 | 97 | [source,scala] 98 | ---- 99 | include::../../modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/MappingWithActiveSlick.scala[tags=adoc] 100 | ---- 101 | 102 | The mapping is exactly the same. However, we define it inside **CoffeeRepo** which implements **EntityActions**. 103 | 104 | 105 | === Building blocks 106 | The previous code fragment shows all necessary building blocks we need: 107 | 108 | <1> The first thing to note is the `import jdbcProfile.api._`. As said before Slick requires jdbc driver. If this line is not included many of the necessary Slick traits, classes and extension methods won't be available. 109 | <2> Define a `BaseTypedType` for the `Entity`'s Id by looking up on the implicit scope. This is probably the most intriging one. This is necessary to build queries involving the `Id`. Note that you don't need to make it implicit yourself. This is already done by the internals of `EntityActions` implementation. 110 | <3> Type Member for the `Entity` type. We choose to use **Type Member** instead of **Type Parameters** because it make the code a little bit cleaner when composing with other traits. (see `OptimisticLocking` trait and `SupplierDao` in `Schema.scala`). 111 | <4> Type Member for the Entity's `Id` 112 | <5> Type Member pointing to the Entity's Table. This is required by `EntityActions` as the predefined methods must know the type of the table. 113 | <6> The ubiquitous Slick `TableQuery`. This is required by `EntityActions` in order to implement the basic methods. 114 | <7> A method returning the `Id` column (ie: `Rep[Id]`). This is how we indicate which table column must be used for all operations involving the primary key. 115 | <8> Finally a `Lens` to get and set the `Id` on the `Entity`. 116 | 117 | We have now a well known column for our primary key. The next step is to define the method to extract the id from our model and to add a generated id back into the model. 118 | 119 | == Transactions 120 | As of version 0.3.0 and the introduction of `DBIO` in Slick 3.0, all methods return `DBIO` (not `Futures`). In Slick 3.0 the DB sessions and the transactional sessions are not passed as implicit parameters therefore it is the user that have to manage the sessions and transactions. 121 | 122 | If ActiveSlick were returning Futures instead, then the transactions will have to be managed internally by **ActiveSlick** which is of course not desirable. 123 | 124 | == Optimistic Locking 125 | 126 | Optimistic locking is supported by means for a version column of type `Long`. 127 | 128 | [source,scala] 129 | ---- 130 | include::../../modules/samples/src/main/scala/io/strongtyped/active/slick/docexamples/OptimisticLockingExample.scala[tags=adoc] 131 | ... 132 | } 133 | ---- 134 | The `OptimisticLocking` requires two new methods to implement in order to glue the `version` field to the `version` column. 135 | 136 | <1> A method returning the `version` column (ie: `Rep[Long]`). This is how we indicate which table column must be used store the version information. 137 | <2> A `Lens` to get and set the `version` on the `Entity`. 138 | 139 | 140 | [TIP] 141 | ==== 142 | Note that we can mixin the trait `OptimisticLocking` without specifying the type of the `Entity` nor the `Id` type. This is due to the usage of Type Members. 143 | 144 | If we were using Type Parameters, we'll be forced to repeat the types each time as in: 145 | `EntityActions[Coffee, Int](H2Driver) with OptimisticLocking[Coffee, Int]` 146 | ==== 147 | 148 | == Before Insert and Update hooks 149 | 150 | `EntityActions` provide two hooks to modify Entity data before insert and update. One can overwrite these methods to add extra checks or modify the `Entity` just before insert and updates. 151 | 152 | A possible usage could be to automatically update `lastUpdated` field for audit purposes. 153 | (see `EntityActionsBeforeInsertUpdateTest.scala` for a concrete example) 154 | 155 | In both cases, the returned DBIO is combined will be combined with the corresponding DBIO (from insert or update methods) and executed in the same transaction. 156 | 157 | [source,scala] 158 | ---- 159 | def beforeInsert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = { 160 | // default implementation does nothing 161 | DBIO.successful(entity) 162 | } 163 | 164 | def beforeUpdate(id: Id, entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = { 165 | // default implementation does nothing 166 | DBIO.successful(entity) 167 | } 168 | ---- 169 | 170 | 171 | toc::[] 172 | -------------------------------------------------------------------------------- /src/asciidoctor/pygments-monokai.css: -------------------------------------------------------------------------------- 1 | .listingblock .pygments .hll { background-color: #49483e } 2 | .listingblock .pygments { background: #272822; color: #f8f8f2 } 3 | .listingblock .pygments .tok-c { color: #75715e } /* Comment */ 4 | .listingblock .pygments .tok-err { color: #960050; background-color: #1e0010 } /* Error */ 5 | .listingblock .pygments .tok-k { color: #66d9ef } /* Keyword */ 6 | .listingblock .pygments .tok-l { color: #ae81ff } /* Literal */ 7 | .listingblock .pygments .tok-n { color: #f8f8f2 } /* Name */ 8 | .listingblock .pygments .tok-o { color: #f92672 } /* Operator */ 9 | .listingblock .pygments .tok-p { color: #f8f8f2 } /* Punctuation */ 10 | .listingblock .pygments .tok-cm { color: #75715e } /* Comment.Multiline */ 11 | .listingblock .pygments .tok-cp { color: #75715e } /* Comment.Preproc */ 12 | .listingblock .pygments .tok-c1 { color: #75715e } /* Comment.Single */ 13 | .listingblock .pygments .tok-cs { color: #75715e } /* Comment.Special */ 14 | .listingblock .pygments .tok-gd { color: #f92672 } /* Generic.Deleted */ 15 | .listingblock .pygments .tok-ge { font-style: italic } /* Generic.Emph */ 16 | .listingblock .pygments .tok-gi { color: #a6e22e } /* Generic.Inserted */ 17 | .listingblock .pygments .tok-gs { font-weight: bold } /* Generic.Strong */ 18 | .listingblock .pygments .tok-gu { color: #75715e } /* Generic.Subheading */ 19 | .listingblock .pygments .tok-kc { color: #66d9ef } /* Keyword.Constant */ 20 | .listingblock .pygments .tok-kd { color: #66d9ef } /* Keyword.Declaration */ 21 | .listingblock .pygments .tok-kn { color: #f92672 } /* Keyword.Namespace */ 22 | .listingblock .pygments .tok-kp { color: #66d9ef } /* Keyword.Pseudo */ 23 | .listingblock .pygments .tok-kr { color: #66d9ef } /* Keyword.Reserved */ 24 | .listingblock .pygments .tok-kt { color: #66d9ef } /* Keyword.Type */ 25 | .listingblock .pygments .tok-ld { color: #e6db74 } /* Literal.Date */ 26 | .listingblock .pygments .tok-m { color: #ae81ff } /* Literal.Number */ 27 | .listingblock .pygments .tok-s { color: #e6db74 } /* Literal.String */ 28 | .listingblock .pygments .tok-na { color: #a6e22e } /* Name.Attribute */ 29 | .listingblock .pygments .tok-nb { color: #f8f8f2 } /* Name.Builtin */ 30 | .listingblock .pygments .tok-nc { color: #a6e22e } /* Name.Class */ 31 | .listingblock .pygments .tok-no { color: #66d9ef } /* Name.Constant */ 32 | .listingblock .pygments .tok-nd { color: #a6e22e } /* Name.Decorator */ 33 | .listingblock .pygments .tok-ni { color: #f8f8f2 } /* Name.Entity */ 34 | .listingblock .pygments .tok-ne { color: #a6e22e } /* Name.Exception */ 35 | .listingblock .pygments .tok-nf { color: #a6e22e } /* Name.Function */ 36 | .listingblock .pygments .tok-nl { color: #f8f8f2 } /* Name.Label */ 37 | .listingblock .pygments .tok-nn { color: #f8f8f2 } /* Name.Namespace */ 38 | .listingblock .pygments .tok-nx { color: #a6e22e } /* Name.Other */ 39 | .listingblock .pygments .tok-py { color: #f8f8f2 } /* Name.Property */ 40 | .listingblock .pygments .tok-nt { color: #f92672 } /* Name.Tag */ 41 | .listingblock .pygments .tok-nv { color: #f8f8f2 } /* Name.Variable */ 42 | .listingblock .pygments .tok-ow { color: #f92672 } /* Operator.Word */ 43 | .listingblock .pygments .tok-w { color: #f8f8f2 } /* Text.Whitespace */ 44 | .listingblock .pygments .tok-mb { color: #ae81ff } /* Literal.Number.Bin */ 45 | .listingblock .pygments .tok-mf { color: #ae81ff } /* Literal.Number.Float */ 46 | .listingblock .pygments .tok-mh { color: #ae81ff } /* Literal.Number.Hex */ 47 | .listingblock .pygments .tok-mi { color: #ae81ff } /* Literal.Number.Integer */ 48 | .listingblock .pygments .tok-mo { color: #ae81ff } /* Literal.Number.Oct */ 49 | .listingblock .pygments .tok-sb { color: #e6db74 } /* Literal.String.Backtick */ 50 | .listingblock .pygments .tok-sc { color: #e6db74 } /* Literal.String.Char */ 51 | .listingblock .pygments .tok-sd { color: #e6db74 } /* Literal.String.Doc */ 52 | .listingblock .pygments .tok-s2 { color: #e6db74 } /* Literal.String.Double */ 53 | .listingblock .pygments .tok-se { color: #ae81ff } /* Literal.String.Escape */ 54 | .listingblock .pygments .tok-sh { color: #e6db74 } /* Literal.String.Heredoc */ 55 | .listingblock .pygments .tok-si { color: #e6db74 } /* Literal.String.Interpol */ 56 | .listingblock .pygments .tok-sx { color: #e6db74 } /* Literal.String.Other */ 57 | .listingblock .pygments .tok-sr { color: #e6db74 } /* Literal.String.Regex */ 58 | .listingblock .pygments .tok-s1 { color: #e6db74 } /* Literal.String.Single */ 59 | .listingblock .pygments .tok-ss { color: #e6db74 } /* Literal.String.Symbol */ 60 | .listingblock .pygments .tok-bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ 61 | .listingblock .pygments .tok-vc { color: #f8f8f2 } /* Name.Variable.Class */ 62 | .listingblock .pygments .tok-vg { color: #f8f8f2 } /* Name.Variable.Global */ 63 | .listingblock .pygments .tok-vi { color: #f8f8f2 } /* Name.Variable.Instance */ 64 | .listingblock .pygments .tok-il { color: #ae81ff } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | val snapshotSuffix = "-SNAPSHOT" 2 | 3 | version in ThisBuild := "0.3.6" + snapshotSuffix 4 | 5 | isSnapshot := version.value.endsWith(snapshotSuffix) 6 | --------------------------------------------------------------------------------