├── .github └── workflows │ └── sbt-devops.yml ├── .gitignore ├── .scalafmt.conf ├── CHANGES.md ├── LICENSE ├── README.md ├── build.sbt ├── cb-test-prepare.sh ├── compat-test └── src │ └── main │ └── scala │ └── com │ └── sandinh │ └── couchbase │ └── Main.scala ├── core └── src │ ├── main │ ├── mima-filters │ │ ├── 7.4.5.backward.excludes │ │ └── 9.0.0.backward.excludes │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── com │ │ ├── couchbase │ │ └── client │ │ │ └── scala │ │ │ ├── json │ │ │ └── ToPlayJs.scala │ │ │ └── kv │ │ │ └── OptionsConvert.scala │ │ └── sandinh │ │ └── couchbase │ │ ├── CBBucket.scala │ │ ├── CBCluster.scala │ │ ├── Implicits.scala │ │ ├── access │ │ ├── CaoKey0.scala │ │ ├── CaoKey1.scala │ │ ├── CaoKey2.scala │ │ ├── CaoTrait.scala │ │ └── JsCao.scala │ │ └── package.scala │ └── test │ └── scala │ └── com │ └── sandinh │ └── couchbase │ ├── BackwardCompatSpec.scala │ ├── CB.scala │ ├── CaoSpec.scala │ ├── CompatStringSpec.scala │ ├── GuiceHelper.scala │ ├── JCBC_642Spec.scala │ ├── JsCodecSpec.scala │ ├── OptimisticLocking.scala │ └── Trophy.scala ├── play └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── com │ │ └── sandinh │ │ └── couchbase │ │ ├── PlayCBCluster.scala │ │ └── PlayModule.scala │ └── test │ └── scala │ └── com │ └── sandinh │ └── couchbase │ └── PlayCBClusterSpec.scala ├── project ├── build.properties └── plugins.sbt └── run-cb-test-container.sh /.github/workflows/sbt-devops.yml: -------------------------------------------------------------------------------- 1 | # @see https://github.com/ohze/sbt-devops/blob/main/files/sbt-devops.yml 2 | name: sbt-devops 3 | on: [push, pull_request] 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | outputs: 8 | commitMsg: ${{ steps.commitMsg.outputs.msg }} 9 | strategy: 10 | matrix: 11 | java: [ '8', '11', '17' ] 12 | couchbase: [ '7.0.2', '5.0.1' ] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - id: commitMsg 16 | run: echo "::set-output name=msg::$(git show -s --format=%s $GITHUB_SHA)" 17 | - uses: actions/setup-java@v2 18 | with: 19 | java-version: ${{ matrix.java }} 20 | distribution: 'temurin' 21 | - uses: coursier/cache-action@v6 22 | - run: sbt devopsQA 23 | - run: ./run-cb-test-container.sh 24 | env: 25 | CB_VERSION: ${{ matrix.couchbase }} 26 | - run: sbt test 27 | # https://www.scala-sbt.org/1.x/docs/GitHub-Actions-with-sbt.html#Caching 28 | - run: | 29 | rm -rf "$HOME/.ivy2/local" || true 30 | find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true 31 | find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true 32 | find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true 33 | find $HOME/.sbt -name "*.lock" -delete || true 34 | shell: bash 35 | 36 | publish: 37 | needs: build 38 | if: | 39 | success() && 40 | github.event_name == 'push' && 41 | (github.ref == 'refs/heads/master' || 42 | github.ref == 'refs/heads/10.x' || 43 | startsWith(github.ref, 'refs/tags/')) 44 | runs-on: ubuntu-latest 45 | outputs: 46 | info: ${{ steps.info.outputs.info }} 47 | steps: 48 | - uses: actions/checkout@v2 49 | with: 50 | fetch-depth: 0 51 | - uses: actions/setup-java@v2 52 | with: 53 | distribution: 'temurin' 54 | java-version: '8' 55 | - run: sbt versionCheck ci-release 56 | env: 57 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 58 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 59 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 60 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 61 | # optional 62 | #CI_CLEAN: '; clean ; sonatypeBundleClean' 63 | #CI_RELEASE: '+publishSigned' 64 | #CI_SONATYPE_RELEASE: 'sonatypeBundleRelease' 65 | #CI_SNAPSHOT_RELEASE: '+publish' 66 | - id: info 67 | run: echo "::set-output name=info::$(cat "$GITHUB_WORKSPACE/target/publish.info")" 68 | 69 | notify: 70 | needs: [build, publish] 71 | if: always() 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: docker://ohze/devops-notify 75 | env: 76 | # You can use `DEVOPS_` | `SLACK_` prefix for `MATTERMOST_*` envs below 77 | # ex SLACK_WEBHOOK_URL instead of MATTERMOST_WEBHOOK_URL 78 | MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }} 79 | # optional. See https://developers.mattermost.com/integrate/incoming-webhooks/#parameters 80 | #MATTERMOST_ICON: icon_url or icon_emoji 81 | #MATTERMOST_CHANNEL: use default of the webhook if not set 82 | #MATTERMOST_USERNAME: use default of the webhook if not set 83 | #MATTERMOST_PRETEXT: Message shown above the CI status attachment, ex to mention some user. Default empty. 84 | _DEVOPS_NEEDS: ${{ toJSON(needs) }} 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache/ 6 | .history/ 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | .classpath 19 | .project 20 | .settings/ 21 | 22 | # IDEA specific 23 | .idea/ 24 | .idea_modules/ 25 | .bsp 26 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.0.4 2 | project.git = yes 3 | trailingCommas = keep 4 | docstrings.wrap = no 5 | runner.dialect = scala213source3 6 | fileOverride { 7 | "glob:**/*.sbt" { 8 | runner.dialect = sbt1 9 | } 10 | "glob:**/project/*.scala" { 11 | runner.dialect = scala212source3 12 | } 13 | } 14 | indent.defnSite = 2 15 | newlines.implicitParamListModifierPrefer = before 16 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | We use [Semantic Versioning](http://semver.org/) 3 | 4 | Backward binary compatibility is ensured by [mima](https://github.com/lightbend/mima) 5 | 6 | See also [mima-filters](core/src/main/mima-filters) 7 | 8 | #### v10.0.0 9 | + Use com.couchbase.client:scala-client:1.2.3 instead of com.couchbase.client:java-client 2.7.22 10 | See [Migrating from SDK2 to SDK3 API](https://docs.couchbase.com/java-sdk/current/project-docs/migrating-sdk-code-to-3.n.html) 11 | And [Couchbase Scala SDK](https://docs.couchbase.com/scala-sdk/current/hello-world/start-using-sdk.html) 12 | + Note: In sdk3, couchbase [use Reactor instead of RxJava](https://docs.couchbase.com/java-sdk/current/project-docs/migrating-sdk-code-to-3.n.html#reactive-and-async-apis). 13 | So couchbase-scala 10.x also depends on Reactor instead of RxJava. 14 | + Don't support couchbase 4.x 15 | + [Don't support bucket-level passwords](https://docs.couchbase.com/java-sdk/current/project-docs/migrating-sdk-code-to-3.n.html#authentication) 16 | You need set config `com.sandinh.couchbase.{user, password}` and give the user corresponding roles 17 | + `DocumentDoesNotExistException` -> `DocumentNotFoundException` 18 | + Deprecated `ScalaBucket`. Now `ScalaBucket` is aliased to `CBBucket` 19 | + In sdk 3, `Document` class is removed and the returned value is now `Result`. 20 | - `CBBucket.get[D <: Document[_]](id: String)(implicit tag: ClassTag[D]): Future[D]` is changed to 21 | `get(id: String, options: GetOptions = GetOptions()): Future[GetResult]` 22 | - Similar to other methods such as `insert, append, prepend,..` 23 | - `getT[T](id: String)(implicit c: Class[_ <: Document[T]]): Future[T]` implicit params change: 24 | Instead of `Class[_ <: Document[T]]`, we now need `JsonDeserializer[T], WeakTypeTag[T], ClassTag[T]`. 25 | Note: Don't need the implicit `WeakTypeTag[T]` if [this change](https://review.couchbase.org/c/couchbase-jvm-clients/+/166690) is merged. 26 | + Deprecated `asJava` of `CBCluster`, `CBBucket`. Pls use `underlying` 27 | + Use `implicit ec: ExecutionContext` param instead of `ExecutionContext.Implicits.global` in: 28 | - `CBBucket.{getJsT, getT}` 29 | + Remove the deprecated `CBCluster.getOrElseT` 30 | + Remove `StrCao, StrCao1, StrCao2, WithCaoKey1, WithCaoKey2, rx.Implicits, document.*, transcoder.*` 31 | 32 | ##### v9.2.0 33 | + Compatible with 9.0.0 except [9.0.0.backward.excludes](core/src/main/mima-filters/9.0.0.backward.excludes) 34 | + Update com.couchbase.client:java-client:2.7.20 -> 2.7.22 35 | + Update scala-collection-compat:2.5.0 -> 2.6.0 36 | + (scala3) Update scala 3.0.2 -> 3.1.0 37 | 38 | ##### v9.0.0 39 | + Break compatible with v8.x but binary compatible with v7.x except [7.4.5.backward.excludes](core/src/main/mima-filters/7.4.5.backward.excludes) 40 | Some source change may need if you use JsCao1 or StrCao1 or WithCaoKey1 41 | But if compile success then binary compatibility are ensured. 42 | + Add support for scala 2.13 & scala3 43 | - `couchbase-scala` is built against scala 2.11.12, 2.12.15, 2.13.6, 3.0.2 44 | + Add support for playframework 2.8 45 | - `couchbase-play` for play 2.6 is renamed to `couchbase-play_2_6` and only support scala 2.11 & 2.12 46 | for sbt: `libraryDependencies += "com.sandinh" %% "couchbase-play_2_6" % "9.0.0"` 47 | - For play 2.8: 48 | `libraryDependencies += "com.sandinh" %% "couchbase-play" % "9.0.0"` 49 | Only support scala 2.12 & 2.13 50 | + Incompatible dependencies change of couchbase-play_2_6 against couchbase-play:7.4.5: 51 | - guava: 22.0 -> 23.6.1-jre 52 | - ssl-config-core 0.2.2 -> 0.3.8 53 | - scala-parser-combinators 1.0.6 -> 1.1.2 54 | 55 | ##### v7.4.5 56 | TODO @thanhpv 57 | 58 | ##### v7.4.4 59 | + Support [Concurrent Document Mutations](https://docs.couchbase.com/java-sdk/2.7/concurrent-mutations-cluster.html) 60 | + Use CAS value 61 | 62 | 63 | ##### 8.1.0 64 | + break change! rename CaoBase.setTWithId -> setWithIdT 65 | + add WithCaoKey1.getOrUpdate convenient method 66 | + change back to normal `version := `, don't use sbt-git versioning 67 | 68 | ##### 8.0.0-1-g0a620a6 69 | + update play 2.8.0 70 | 71 | ##### 8.0.0 72 | + break changes: 73 | - rename CaoBase get/set/update/remove methods by adding `WithId` suffix 74 | + drop scala 2.11 & add 2.13 75 | + update play 2.8.0-RC5 76 | + update couchbase java-client 2.7.11 77 | + update typesafe config 1.4.0 //same as dependency of akka-actor:2.6.0 78 | + use scala-collection-compat 79 | + update sbt 1.3.4 & some sbt plugins 80 | + use sbt-git for versioning 81 | 82 | ##### v7.4.2 83 | + update couchbase java-client 2.6.0, play-json 2.6.9 (latest, corresponding with play 2.6.17) 84 | + cross compile for scala 2.12.6, 2.11.12 85 | + update sbt 1.1.6 & some sbt plugins 86 | 87 | ##### v7.4.1 88 | + update couchbase java-client 2.5.2, play-json 2.6.7 89 | + cross compile for scala 2.12.4, 2.11.11 90 | + add travis test on couchbase 5.0.0 91 | + update sbt 1.0.3 & some sbt plugins 92 | 93 | ##### v7.4.0 94 | + update couchbase java-client 2.5.0, play-json 2.6.3 95 | + couchbase-play now depends on play instead of play-alone 96 | + cross compile for scala 2.12.3, 2.11.11 97 | + travis test on couchbase 4.6.3, 4.5.0 98 | + update sbt 1.0.1, sbt-sonatype 2.0, sbt-pgp 1.1.0 99 | + use sbt-coursier 100 | + use sbt-scalafmt-coursier instead of sbt-scalariform 101 | + move source code to github.com/ohze/couchbase-scala 102 | + Change in PlayCBCluster: 103 | - now inject (first constructor's param) Config instead of Configuration 104 | - `disconnectFuture` now return `Future[lang.Boolean]` instead of `Future[Unit]` 105 | + breaking changes in `CBCluster`: 106 | - remove deprecated field `cluster` 107 | - `openBucket` now return `Future[ScalaBucket]` instead of `ScalaBucket`. 108 | - Add `openBucketSync` - which is the old synchronous `openBucket` method. 109 | @note You should never perform long-running blocking operations inside of an asynchronous stream (e.g. inside of maps or flatMaps). 110 | @see [JVMCBC-79](https://issues.couchbase.com/browse/JVMCBC-79) 111 | - Similar for `disconnect` (now return `Future[lang.Boolean]`) & `disconnectSync` 112 | 113 | ##### v7.3.1 114 | + update couchbase java-client 2.3.1, play-json 2.5.4 115 | + remove config `com.sandinh.couchbase.queryEnabled` 116 | because `com.couchbase.client.core.env.DefaultCoreEnvironment.Builder#queryEnabled` is removed fromcouchbase java-client 2.3.x 117 | 118 | ##### v7.3.0 119 | + update scala 2.11.8, couchbase client 2.2.7, play-alone 2.5.3 120 | + test on travis for * 121 | 122 | ##### v7.2.2 123 | + update couchbase java-client 2.2.4 & simplify scalacOptions 124 | + minor change: use TranscoderUtils.encodeStringAsUtf8 instead of Unpooled.copiedBuffer 125 | + fix CompatStringSpec. The failed tests is caused by caching mechanism of couchbase bucket 126 | 127 | ##### v7.2.1 128 | + update java-client 2.2.2, rxjava 1.0.17, play-json 2.4.6, play-alone 2.4.3 129 | 130 | ##### v7.2.0 131 | + update couchbase java-client 2.2.1, rxjava 1.0.15, play-json 2.4.3, play-alone 2.4.2_1 132 | + note: This version is compatible with couchbase-server 2.x, 3.x, 4.x. 133 | see [JCBC-880](https://issues.couchbase.com/browse/JCBC-880) 134 | 135 | ##### v7.1.3 136 | + add binding: `bind[Config].toInstance(configuration.underlying)` in `com.sandinh.couchbase.PlayModule` 137 | 138 | ##### v7.1.2 139 | + `couchbase-play` can be used with [com.sandinh:play-alone](https://github.com/giabao/play-jdbc-standalone) 140 | or full [com.typesafe.play:play](http://playframework.com/) 141 | + update `com.couchbase.client:java-client:2.1.4` 142 | 143 | ##### v7.1.1 144 | change in CBCluster: 145 | + deprecate cluster. Use asJava instead 146 | + Make `env` public 147 | 148 | ##### v7.1.0 149 | + update play-json 2.4.2 (require java 8), com.couchbase.client:java-client:2.1.3, rxjava:1.0.12 150 | + add `couchbase-play` module for using couchbase-scala in a play application 151 | + support n1ql querry 152 | + add convenience method `JsDocument.as[T: Reads]: T` 153 | + (minor break change) move the implicit Doc Classes: com.sandinh.couchbase.{Implicits => Implicit.DocCls} 154 | + add implicit value class DocNotExistFuture, RichJsonObject, RichJsonArray 155 | 156 | ##### v7.0.1 157 | + update scala 2.11.6, com.couchbase.client:java-client:2.1.1, rxjava:1.0.7 158 | + use specs2-core:3.0 159 | 160 | ##### v7.0.0 161 | + update play-json 2.3.8, rxjava 1.0.6 (transitive dep at v1.0.4 from couchbase java-client 2.1.0) 162 | + update couchbase java-client 2.1.0 with changes: 163 | + default disconnect timeout is increased from 5s to 25s 164 | + ScalaBucket.query(String) is replaced by query(Statement) 165 | + RichAsyncViewResult.flatFoldRows now use scConcatMap instead of scFlatMap to preserve order of underlying observable items. 166 | This fixes the bug in sandinh.com's bank-play project: log rows in bank is out-of-created-order 167 | + RichAsyncViewResult is moved from com.sandinh.rx.Implicits to com.sandinh.couchbase.Implicits 168 | 169 | ##### v6.1.0 170 | + fixes SI-9121 by removing com.sandinh.rx.Implicits.{RichFunction1, RichFunction2} 171 | + remove some `@inline` annotations 172 | + add scalacOptions: -optimise -Ybackend:GenBCode -Ydelambdafy:method .. 173 | 174 | ##### v6.0.0 175 | 1. add [CompatStringTranscoderLegacy](src/main/scala/com/sandinh/couchbase/transcoder/CompatStringTranscoder.scala#L51) which: 176 | + decoding a stored document in format of StringDocument OR JsonStringDocument. 177 | + encoding a String as StringDocument. 178 | 179 | (same as in previous version, [CompatStringTranscoder](src/main/scala/com/sandinh/couchbase/transcoder/CompatStringTranscoder.scala#L39): 180 | + decoding a stored document in format of StringDocument OR JsonStringDocument. 181 | + encoding a String as StringDocument.) 182 | 183 | see [CompatStringSpec](src/test/scala/com/sandinh/couchbase/CompatStringSpec.scala) 184 | 185 | 2. [CBCluster.openBucket](src/main/scala/com/sandinh/couchbase/CBCluster.scala#L24) now has `legacyEncodeString: Boolean` param, default = true. 186 | In previous version, CBCluster.openBucket("some_bucket") return a bucket which encode String as JsonString (using CompatStringTranscoder). 187 | For better compatibility, from v6.0.0 the return bucket will encode String using CompatStringTranscoderLegacy. 188 | (This is in-compatible with v5.x, so we bump to v6.0.0). 189 | 190 | ##### v5.1.1 191 | only update scala 2.11.5, couchbase java-client 2.0.3 192 | 193 | ##### v5.1.0 194 | only use CompatStringDocument instead of StringDocument for StrCao, StrCao1, StrCao2 195 | 196 | ##### v5.0.0 197 | + move `def bucket: ScalaBucket` to constructor's param in CaoBase, JsCao, JsCao1, JsCao2, StrCao, StrCao1, StrCao2 198 | + use `com.couchbase.timeout._` keys to config timeout in [duration format](https://github.com/typesafehub/config/blob/master/HOCON.md#duration-format). 199 | see [reference.conf](src/main/resources/reference.conf) for legend 200 | + note: from this version, config values `com.couchbase._` will not be set to java system properties 201 | (see class `DefaultCouchbaseEnvironment`) 202 | + add some convenient methods to ScalaBucket: getT, getOrElseT, getJsT 203 | + CBCluster now auto add `com.sandinh.couchbase.transcoder._` transcoders when openBucket 204 | + add CompatStringDocument which works exactly like JsonStringDocument permit decoding a stored StringDocument 205 | 206 | ##### v4.2.0 207 | reverse `getOrElse` changes in v4.1.0: 208 | `getOrElse(x)(null): type mismatch found Null(null) required XX` 209 | is because x is an instance of class X(..) extend AnyVal 210 | 211 | ##### v4.1.0 212 | note: Do not use this version. see v4.2.0 213 | This version contain some incompatible change: 214 | + getOrElse method in CaoBase, WithCaoKey1, WithCaoKey2: do not use a separate param list for `default` param 215 | (fix usage problem when getOrElse(x)(null): type mismatch found Null(null) required XX) 216 | + rename RichAsyncViewRow.{document => doc}. 217 | We can't name `document` because scala compiler will not implicitly pick that method. 218 | 219 | ##### v4.0.1 220 | + some minor change (no bug fix, no new feature) 221 | + remove crossScalaVersions 2.10 222 | 223 | ##### v4.0.0 224 | WARNING: when implement a real project at http://sandinh.com, we found that RxScala (& RxJava) is VERY VERY complex 225 | (compare to scala Future). 226 | At first, we have created https://github.com/giabao/scala-future-vs-rxscala to share knowledge to our team. 227 | But after several weeks, we have decided to use Future only! (many dangerousness of Rx have not been mentioned in scala-future-vs-rxscala). 228 | So, we change couchbase-scala to just expose Future as the API. 229 | 230 | ##### v3.0.2 231 | + update rxjava 1.0.3 232 | + add com.sandinh.rx.Implicits.RichObs.subscribeError 233 | 234 | ##### v3.0.1 235 | only update libs: 236 | ``` 237 | "com.couchbase.client" % "java-client" % "2.0.2", 238 | "io.reactivex" %% "rxscala" % "0.23.0", 239 | "io.reactivex" % "rxjava" % "1.0.2", 240 | "com.typesafe.play" %% "play-json" % "2.3.7" 241 | ``` 242 | 243 | ##### v3.0.0 244 | + update rxjava 1.0.1 245 | + typeof CBCluster.openBucket & CaoBase.bucket is changed from ScalaBucket to Observable[ScalaBucket] 246 | + typeof CBCluster.disconnect() is changed from Boolean to Observable[Boolean] 247 | + remove CBCluster.disconnect(FiniteDuration) 248 | + setBulk in WithCaoKey1 & WithCaoKey2 now use concatMap instead of flatMap to preserve ordering of result with the params 249 | + fixes http://www.couchbase.com/issues/browse/JCBC-642 250 | 251 | ##### v2.0.1 252 | narrow dependencies from guice to javax.inject 253 | 254 | ##### v2.0.0 255 | first public version 256 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 SanDinh.com 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | couchbase-scala 2 | =============== 3 | 4 | [![CI](https://github.com/ohze/couchbase-scala/actions/workflows/sbt-devops.yml/badge.svg)](https://github.com/ohze/couchbase-scala/actions/workflows/sbt-devops.yml) 5 | 6 | This is a library for accessing Couchbase in Scala. 7 | 8 | ## Using 9 | couchbase-scala is [published to maven center](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.sandinh%22%20couchbase-scala) 10 | 11 | 1. using [typesafe config](https://github.com/typesafehub/config) file application.conf 12 | to config couchbase connection url, buckets, timeout,.. 13 | see [reference.conf](core/src/main/resources/reference.conf) 14 | ``` 15 | com.couchbase.timeout { 16 | connect=10s 17 | # ... 18 | } 19 | com.sandinh.couchbase { 20 | connectionString = "dev.sandinh.com" 21 | user="?" 22 | password="?" 23 | } 24 | ``` 25 | 26 | 2. load the config, instantiate a CBCluster instance, then open a bucket 27 | ```scala 28 | import com.typesafe.config.ConfigFactory 29 | import com.sandinh.couchbase.CBCluster 30 | val cluster = new CBCluster(ConfigFactory.load()) 31 | val accBucket = cluster.bucket("acc") 32 | ``` 33 | 34 | Or, you can use DI (example google guice): 35 | ```scala 36 | class CBModule extends AbstractModule { 37 | override def configure(): Unit = { 38 | bind(classOf[Config]).toInstance(ConfigFactory.load()) 39 | } 40 | } 41 | 42 | class MyClient @Inject() (cluster: CBCluster) { 43 | val accBucket = cluster.bucket("acc") 44 | } 45 | ``` 46 | 47 | 3. access couchbase using CBBucket's api 48 | ```scala 49 | val r: Future[GetResult] = accBucket.get("some_key") 50 | val s = accBucket.getT[String]("some_key") 51 | 52 | //see other methods (upsert, insert, replace, remove, touch, counter, append, unlock, getFromReplica, getAndLock,..) 53 | //from CBBucket class 54 | ``` 55 | 56 | 4. you can use play-json to retrieve a JsValue directly 57 | ```scala 58 | import play.api.libs.json.{Json, Format} 59 | import com.sandinh.couchbase.Implicits.jsonSerializer // Used in upsert 60 | 61 | case class Acc(name: String, gender: Option[Boolean]) 62 | object Acc { 63 | implicit val fmt: OFormat[Acc] = Json.format[Acc] 64 | } 65 | accBucket.upsert("some_key", Acc("name", None)) 66 | val name = accBucket.getJsT[Acc]("some_key").map(_.name) 67 | ``` 68 | 69 | ## Changelog 70 | see [CHANGES.md](CHANGES.md) 71 | 72 | ## Dev guide 73 | 74 | + prepare couchbase for testing 75 | ```shell script 76 | docker run -d --name cb -p 8091-8094:8091-8094 -p 11210:11210 couchbase:5.0.1 77 | docker cp travis-cb-prepare.sh cb:/tmp 78 | docker exec -i cb /tmp/travis-cb-prepare.sh 79 | ``` 80 | or, if you have prepared before => only run `docker start cb` 81 | 82 | ```sbtshell 83 | test 84 | ``` 85 | 86 | ## publish guide 87 | We use [sd-devops](/ohze/sd-devops) so: 88 | + Every push (or merge a PR) to `master` branch will be publish to sonatype snapshots 89 | (only if [QA, test, compatible check](.github/workflows/sd-devops.yml) pass) 90 | + If push tag match glob `v[0-9]*`, ex `v9.0.0` or even `v9bla.bla` 91 | then [publish job](.github/workflows/sd-devops.yml) will publish a release version to sonatype release repo 92 | (which will be sync to maven central) 93 | + **!!!NOTE!!!** You MUST tag version with **v** prefix or else it will not be published! 94 | + You should never manually publish from your local machine unless `sbt publishLocal` 95 | + MUST update [CHANGES.md]! 96 | 97 | ## Licence 98 | This software is licensed under the Apache 2 license: 99 | http://www.apache.org/licenses/LICENSE-2.0 100 | 101 | Copyright 2014-2021 Sân Đình (https://sandinh.com) 102 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | val emptyDoc = Compile / packageDoc / mappings := Seq( 2 | (ThisBuild / baseDirectory).value / "README.md" -> "README.md" 3 | ) 4 | 5 | def resourcePrepare(extra: Def.Initialize[Task[String]]) = 6 | resourceGenerators += Def.task { 7 | val f = managedResourceDirectories.value.head / "application.conf" 8 | val host = java.net.InetAddress.getLocalHost.getHostAddress 9 | IO.write( 10 | f, 11 | s"""com.sandinh.couchbase { 12 | | connectionString="couchbase://$host" 13 | | user="cb" 14 | | password="cb_password" 15 | |} 16 | |${extra.value} 17 | |""".stripMargin 18 | ) 19 | Seq(f) 20 | } 21 | 22 | // util project to test couchbase-scala backward compatibility 23 | lazy val `compat-test` = project 24 | .settings( 25 | skipPublish, 26 | scalaVersion := scala213, 27 | resolvers += Resolver.sonatypeRepo("public"), 28 | libraryDependencies ++= Seq( 29 | "com.sandinh" %% "couchbase-scala" % "9.2.0", 30 | ), 31 | inConfig(Compile)(resourcePrepare(Def.task(""))), 32 | ) 33 | 34 | lazy val `couchbase-scala` = projectMatrix 35 | .in(file("core")) 36 | .configAxis(config13, Seq(scala212, scala213)) 37 | .configAxis(config14, Seq(scala212, scala213)) 38 | .settings( 39 | libraryDependencies ++= Seq( 40 | "com.couchbase.client" %% "scala-client" % "1.4.10", 41 | "javax.inject" % "javax.inject" % "1", 42 | "com.typesafe.play" %% "play-json" % "2.10.0-RC5", 43 | "com.typesafe" % "config" % configAxis.value.version, 44 | "com.google.inject" % "guice" % "5.0.1" % Test, 45 | ) ++ specs2("-core").value, 46 | emptyDoc, 47 | inConfig(Test)(resourcePrepare(Def.task { 48 | val cp = (`compat-test` / Runtime / fullClasspath).value 49 | .map(_.data.getAbsolutePath) 50 | .mkString(":") 51 | s"""compat-test.classpath="$cp"""" 52 | })), 53 | Test / test := (Test / test) 54 | .dependsOn(`compat-test` / Compile / compile) 55 | .value, 56 | ) 57 | 58 | lazy val `couchbase-play` = projectMatrix 59 | .in(file("play")) 60 | .playAxis( 61 | play26, 62 | Seq(scala212), 63 | _.dependsOn(`couchbase-scala`.finder(config13)(scala212)) 64 | ) 65 | .playAxis( 66 | play27, 67 | Seq(scala212), 68 | _.dependsOn(`couchbase-scala`.finder(config13)(scala212)) 69 | ) 70 | .playAxis( 71 | play27, 72 | Seq(scala213), 73 | _.dependsOn(`couchbase-scala`.finder(config13)(scala213)) 74 | ) 75 | .playAxis( 76 | play28, 77 | Seq(scala212), 78 | _.dependsOn(`couchbase-scala`.finder(config14)(scala212)) 79 | ) 80 | .playAxis( 81 | play28, 82 | Seq(scala213), 83 | _.dependsOn(`couchbase-scala`.finder(config14)(scala213)) 84 | ) 85 | .settings( 86 | libraryDependencies ++= play("play", "guice").value ++ 87 | specs2("-core").value ++ 88 | Seq( 89 | "ch.qos.logback" % "logback-classic" % "1.2.7" % Test, 90 | ), 91 | emptyDoc, 92 | inConfig(Test)(resourcePrepare(Def.task(""))), 93 | ) 94 | 95 | // only aggregating project 96 | lazy val `couchbase-scala-root` = (project in file(".")) 97 | .disablePlugins(MimaPlugin) 98 | .settings(skipPublish) 99 | .aggregate(`couchbase-play`.projectRefs ++ `couchbase-scala`.projectRefs: _*) 100 | 101 | inThisBuild( 102 | Seq( 103 | versionScheme := Some("semver-spec"), 104 | developers := List( 105 | Developer( 106 | "thanhbv", 107 | "Bui Viet Thanh", 108 | "thanhbv@sandinh.net", 109 | url("https://sandinh.com") 110 | ), 111 | Developer( 112 | "vinhbt", 113 | "Bui The Vinh", 114 | "vinhbt@sandinh.net", 115 | url("https://sandinh.com") 116 | ), 117 | Developer( 118 | "thanhpv", 119 | "Phan Van Thanh", 120 | "thanhpv@sandinh.net", 121 | url("https://sandinh.com") 122 | ), 123 | ), 124 | ) 125 | ) 126 | 127 | // In Test code: com.sandinh.couchbase.GuiceSpecBase.setup 128 | // We use Guice's injectMembers that inject value for the GuiceSpecBase's private var `_cb` 129 | // using reflection which is deny by default in java 16+ 130 | inThisBuild(addOpensForTest()) 131 | -------------------------------------------------------------------------------- /cb-test-prepare.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # https://developer.couchbase.com/documentation/server/5.0/cli/cbcli-intro.html 4 | /opt/couchbase/bin/couchbase-cli cluster-init \ 5 | -c 127.0.0.1:8091 \ 6 | --cluster-username=Administrator \ 7 | --cluster-password=password \ 8 | --cluster-ramsize=256 9 | 10 | /opt/couchbase/bin/couchbase-cli bucket-create \ 11 | -c 127.0.0.1:8091 \ 12 | -u Administrator -p password \ 13 | --bucket=fodi \ 14 | --bucket-type=ephemeral \ 15 | --bucket-ramsize=100 \ 16 | --bucket-replica=0 17 | 18 | /opt/couchbase/bin/couchbase-cli bucket-create \ 19 | -c 127.0.0.1:8091 \ 20 | -u Administrator -p password \ 21 | --bucket=acc \ 22 | --bucket-type=couchbase \ 23 | --bucket-ramsize=100 \ 24 | --bucket-replica=0 25 | 26 | # https://developer.couchbase.com/documentation/server/5.0/cli/cbcli/couchbase-cli-user-manage.html 27 | /opt/couchbase/bin/couchbase-cli user-manage \ 28 | -c 127.0.0.1:8091 \ 29 | -u Administrator -p password \ 30 | --set \ 31 | --rbac-username cb \ 32 | --rbac-password cb_password \ 33 | --roles data_reader[fodi],data_writer[fodi],data_reader[acc],data_writer[acc] \ 34 | --auth-domain local 35 | -------------------------------------------------------------------------------- /compat-test/src/main/scala/com/sandinh/couchbase/Main.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | import com.couchbase.client.java.document.{JsonLongDocument, StringDocument} 3 | import com.sandinh.couchbase.Implicits.DocNotExistFuture 4 | import com.sandinh.couchbase.document.CompatStringDocument 5 | import com.typesafe.config.ConfigFactory 6 | 7 | import scala.concurrent.Await 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import scala.concurrent.duration._ 10 | 11 | object Main { 12 | def main(args: Array[String]): Unit = { 13 | val cluster = new CBCluster(ConfigFactory.load()) 14 | val bucket = cluster.openBucketSync("fodi") 15 | 16 | val f = args match { 17 | case Array("set", "counter", key, value) => 18 | bucket.counter(key, 0, value.toLong) 19 | case Array("get", "counter", key) => 20 | bucket 21 | .get[JsonLongDocument](key) 22 | .map(_.content.longValue) 23 | .recoverNotExist(0L) 24 | 25 | case Array("set", "CompatString", key, value) => 26 | bucket.upsert(new CompatStringDocument(key, value, 60)).map(_ => value) 27 | case Array("set", "String", key, value) => 28 | bucket.upsert(StringDocument.create(key, 60, value)).map(_ => value) 29 | case Array("get", "CompatString", key) => 30 | import Implicits.DocCls.CompatStringDocCls 31 | bucket.getT[String](key) 32 | 33 | case _ => ??? 34 | } 35 | 36 | val v = Await.result(f, 5.seconds) 37 | print(v) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/main/mima-filters/7.4.5.backward.excludes: -------------------------------------------------------------------------------- 1 | # * the type hierarchy of class com.sandinh.couchbase.access.JsCao1 is different in current version. Missing types {com.sandinh.couchbase.access.CaoBase,com.sandinh.couchbase.access.JsCao} 2 | ProblemFilters.exclude[MissingTypesProblem]("com.sandinh.couchbase.access.JsCao1") 3 | # * the type hierarchy of class com.sandinh.couchbase.access.StrCao1 is different in current version. Missing types {com.sandinh.couchbase.access.CaoBase,com.sandinh.couchbase.access.StrCao} 4 | ProblemFilters.exclude[MissingTypesProblem]("com.sandinh.couchbase.access.StrCao1") 5 | # * abstract method self()com.sandinh.couchbase.access.CaoBase in interface com.sandinh.couchbase.access.WithCaoKey1 is present only in current version 6 | ProblemFilters.exclude[ReversedMissingMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.self") 7 | # * abstract method reads(java.lang.Object)java.lang.Object in interface com.sandinh.couchbase.access.CaoTrait is inherited by class WithCaoKey1 in current version. 8 | ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.reads") 9 | # * abstract method writes(java.lang.Object)java.lang.Object in interface com.sandinh.couchbase.access.CaoTrait is inherited by class WithCaoKey1 in current version. 10 | ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.writes") 11 | # * abstract method createDoc(java.lang.String,Int,java.lang.Object,Long)com.couchbase.client.java.document.Document in interface com.sandinh.couchbase.access.CaoTrait is inherited by class WithCaoKey1 in current version. 12 | ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.createDoc") 13 | 14 | # Additional filters for scala 2.11 15 | # * method update(java.lang.Object,java.lang.Object,Long)scala.concurrent.Future in trait com.sandinh.couchbase.access.WithCaoKey1 is present only in current version 16 | ProblemFilters.exclude[ReversedMissingMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.update") 17 | # * synthetic method update$default$3()Long in trait com.sandinh.couchbase.access.WithCaoKey1 is present only in current version 18 | ProblemFilters.exclude[ReversedMissingMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.update$default$3") 19 | # * method expiry()Int in trait com.sandinh.couchbase.access.CaoTrait is inherited by class WithCaoKey1 in current version. 20 | ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.expiry") 21 | # * synthetic method createDoc$default$4()Long in trait com.sandinh.couchbase.access.CaoTrait is inherited by class WithCaoKey1 in current version. 22 | ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.createDoc$default$4") 23 | # * method update(java.lang.Object,java.lang.Object,Long)scala.concurrent.Future in trait com.sandinh.couchbase.access.CaoTrait is inherited by class WithCaoKey1 in current version. 24 | ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.update") 25 | # * synthetic method update$default$3()Long in trait com.sandinh.couchbase.access.CaoTrait is inherited by class WithCaoKey1 in current version. 26 | ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("com.sandinh.couchbase.access.WithCaoKey1.update$default$3") 27 | 28 | # The following CaoBase's methods don't have compatible problems in scala 29 | # because CaoBase extends CaoTrait[_, A,..] with type A = String 30 | # * method getOrElse(java.lang.String,scala.Function0)scala.concurrent.Future in class com.sandinh.couchbase.access.CaoBase's type is different in current version, where it is (java.lang.Object,scala.Function0)scala.concurrent.Future instead of (java.lang.String,scala.Function0)scala.concurrent.Future 31 | ProblemFilters.exclude[IncompatibleMethTypeProblem]("com.sandinh.couchbase.access.CaoBase.getOrElse") 32 | # * method getOrElseWithCAS(java.lang.String,scala.Function0)scala.concurrent.Future in class com.sandinh.couchbase.access.CaoBase's type is different in current version, where it is (java.lang.Object,scala.Function0)scala.concurrent.Future instead of (java.lang.String,scala.Function0)scala.concurrent.Future 33 | ProblemFilters.exclude[IncompatibleMethTypeProblem]("com.sandinh.couchbase.access.CaoBase.getOrElseWithCAS") 34 | # * method getOrUpdate(java.lang.String,scala.Function0)scala.concurrent.Future in class com.sandinh.couchbase.access.CaoBase's type is different in current version, where it is (java.lang.Object,scala.Function0)scala.concurrent.Future instead of (java.lang.String,scala.Function0)scala.concurrent.Future 35 | ProblemFilters.exclude[IncompatibleMethTypeProblem]("com.sandinh.couchbase.access.CaoBase.getOrUpdate") 36 | # * method setT(java.lang.String,java.lang.Object)scala.concurrent.Future in class com.sandinh.couchbase.access.CaoBase's type is different in current version, where it is (java.lang.Object,java.lang.Object)scala.concurrent.Future instead of (java.lang.String,java.lang.Object)scala.concurrent.Future 37 | ProblemFilters.exclude[IncompatibleMethTypeProblem]("com.sandinh.couchbase.access.CaoBase.setT") -------------------------------------------------------------------------------- /core/src/main/mima-filters/9.0.0.backward.excludes: -------------------------------------------------------------------------------- 1 | # The following methods of CaoBase is moved to `CaoTrait` which is a super trait of CaoBase 2 | 3 | # * method getOrElse(java.lang.String,scala.Function0)scala.concurrent.Future in class com.sandinh.couchbase.access.CaoBase does not have a correspondent in current version 4 | ProblemFilters.exclude[DirectMissingMethodProblem]("com.sandinh.couchbase.access.CaoBase.getOrElse") 5 | # * method getOrElseWithCAS(java.lang.String,scala.Function0)scala.concurrent.Future in class com.sandinh.couchbase.access.CaoBase does not have a correspondent in current version 6 | ProblemFilters.exclude[DirectMissingMethodProblem]("com.sandinh.couchbase.access.CaoBase.getOrElseWithCAS") 7 | # * method getOrUpdate(java.lang.String,scala.Function0)scala.concurrent.Future in class com.sandinh.couchbase.access.CaoBase's type is different in current version, where it is (java.lang.Object,scala.Function0)scala.concurrent.Future instead of (java.lang.String,scala.Function0)scala.concurrent.Future 8 | ProblemFilters.exclude[IncompatibleMethTypeProblem]("com.sandinh.couchbase.access.CaoBase.getOrUpdate") 9 | # * method setT(java.lang.String,java.lang.Object)scala.concurrent.Future in class com.sandinh.couchbase.access.CaoBase does not have a correspondent in current version 10 | ProblemFilters.exclude[DirectMissingMethodProblem]("com.sandinh.couchbase.access.CaoBase.setT") 11 | 12 | # 13 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method getOrElse(java.lang.Object,scala.Function0)scala.concurrent.Future 14 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.getOrElse") 15 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method getBulk(scala.collection.immutable.Seq)scala.concurrent.Future 16 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.getBulk") 17 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method getOrElseWithCAS(java.lang.Object,scala.Function0)scala.concurrent.Future 18 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.getOrElseWithCAS") 19 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method getBulkWithCAS(scala.collection.immutable.Seq)scala.concurrent.Future 20 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.getBulkWithCAS") 21 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method setT(java.lang.Object,java.lang.Object)scala.concurrent.Future 22 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.setT") 23 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method setBulk(scala.collection.immutable.Seq,scala.collection.immutable.Seq)scala.concurrent.Future 24 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.setBulk") 25 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method change(java.lang.Object,scala.Function1)scala.concurrent.Future 26 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.change") 27 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method flatChange(java.lang.Object,scala.Function1)scala.concurrent.Future 28 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.flatChange") 29 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method changeBulk(scala.collection.immutable.Seq,scala.Function1)scala.concurrent.Future 30 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.changeBulk") 31 | # * in current version, classes mixing com.sandinh.couchbase.access.WithCaoKey1 need be recompiled to wire to the new static mixin forwarder method all super calls to method flatChangeBulk(scala.collection.immutable.Seq,scala.Function1)scala.concurrent.Future 32 | ProblemFilters.exclude[NewMixinForwarderProblem]("com.sandinh.couchbase.access.WithCaoKey1.flatChangeBulk") 33 | -------------------------------------------------------------------------------- /core/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | # see com.couchbase.client.core.env.TimeoutConfig 2 | com.couchbase.timeout { 3 | #management=75s 4 | #query=75s 5 | #view=75s 6 | #kv=2500ms 7 | #connect=10s 8 | #disconnect=10s 9 | # other options can be set as params in the connectionString 10 | } 11 | com.sandinh.couchbase { 12 | # connectionString = "cb1.sandinh.com:8091,cb2.sandinh.com:8091" 13 | user="?" 14 | password="?" 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scala/com/couchbase/client/scala/json/ToPlayJs.scala: -------------------------------------------------------------------------------- 1 | package com.couchbase.client.scala.json 2 | 3 | import play.api.libs.json.{ 4 | JsArray, 5 | JsBoolean, 6 | JsNull, 7 | JsNumber, 8 | JsObject, 9 | JsString, 10 | JsValue 11 | } 12 | 13 | import scala.collection.mutable 14 | import scala.collection.mutable.ListBuffer 15 | import scala.jdk.CollectionConverters._ 16 | 17 | object ToPlayJs { 18 | 19 | /** @see [[com.couchbase.client.scala.json.JsonObject.toMap]] */ 20 | def apply(o: JsonObject): JsObject = { 21 | val m = mutable.Map.empty[String, JsValue] 22 | for (entry <- o.content.entrySet.asScala) 23 | m.put( 24 | entry.getKey, 25 | entry.getValue match { 26 | case null => JsNull 27 | case x: Boolean => JsBoolean(x) 28 | case x: String => JsString(x) 29 | case x: Int => JsNumber(BigDecimal(x)) 30 | case x: Long => JsNumber(BigDecimal(x)) 31 | case x: Double => JsNumber(BigDecimal(x)) 32 | case x: Float => JsNumber(BigDecimal(x.toDouble)) 33 | case x: Short => JsNumber(BigDecimal(x)) 34 | case x: JsonObject => apply(x) 35 | case x: JsonObjectSafe => apply(x.o) 36 | case x: JsonArray => apply(x) 37 | case x: JsonArraySafe => apply(x.a) 38 | case _ => ??? // can NOT go here 39 | } 40 | ) 41 | JsObject(m) 42 | } 43 | 44 | /** @see [[com.couchbase.client.scala.json.JsonArray.toSeq]] */ 45 | def apply(a: JsonArray): JsArray = { 46 | val l = ListBuffer.empty[JsValue] 47 | for (x <- a.iterator) 48 | l += (x match { 49 | case null => JsNull 50 | case x: Boolean => JsBoolean(x) 51 | case x: String => JsString(x) 52 | case x: Int => JsNumber(BigDecimal(x)) 53 | case x: Long => JsNumber(BigDecimal(x)) 54 | case x: Double => JsNumber(BigDecimal(x)) 55 | case x: Float => JsNumber(BigDecimal(x.toDouble)) 56 | case x: Short => JsNumber(BigDecimal(x)) 57 | case x: JsonObject => apply(x) 58 | case x: JsonObjectSafe => apply(x.o) 59 | case x: JsonArray => apply(x) 60 | case x: JsonArraySafe => apply(x.a) 61 | case _ => ??? // can NOT go here 62 | }) 63 | JsArray(l) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/scala/com/couchbase/client/scala/kv/OptionsConvert.scala: -------------------------------------------------------------------------------- 1 | package com.couchbase.client.scala.kv 2 | 3 | object OptionsConvert { 4 | implicit class InsertOptionsOps(private val o: InsertOptions) extends AnyVal { 5 | def toReplaceOptions( 6 | cas: Long = 0, 7 | preserveExpiry: Boolean = false 8 | ): ReplaceOptions = 9 | ReplaceOptions( 10 | cas, 11 | o.durability, 12 | o.timeout, 13 | o.parentSpan, 14 | o.retryStrategy, 15 | o.transcoder, 16 | o.expiry, 17 | o.expiryTime, 18 | preserveExpiry 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sandinh/couchbase/CBBucket.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import com.couchbase.client.scala.codec.{ 4 | JsonDeserializer, 5 | JsonSerializer, 6 | Transcoder 7 | } 8 | import com.couchbase.client.scala.durability.Durability 9 | import com.couchbase.client.scala.durability.Durability.Disabled 10 | import com.couchbase.client.scala.kv.{ 11 | AppendOptions, 12 | CounterResult, 13 | GetAllReplicasOptions, 14 | GetAndLockOptions, 15 | GetAndTouchOptions, 16 | GetAnyReplicaOptions, 17 | GetOptions, 18 | GetReplicaResult, 19 | GetResult, 20 | IncrementOptions, 21 | InsertOptions, 22 | MutationResult, 23 | PrependOptions, 24 | RemoveOptions, 25 | ReplaceOptions, 26 | TouchOptions, 27 | UnlockOptions, 28 | UpsertOptions 29 | } 30 | import com.couchbase.client.scala.manager.bucket.AsyncBucketManager 31 | import com.couchbase.client.scala.manager.view.AsyncViewIndexManager 32 | import com.couchbase.client.scala.query.{ 33 | QueryOptions, 34 | QueryParameters, 35 | QueryResult 36 | } 37 | import com.couchbase.client.core.error.DocumentNotFoundException 38 | import com.couchbase.client.scala.view.{ViewOptions, ViewResult} 39 | import com.couchbase.client.scala.{AsyncBucket, AsyncCluster, AsyncCollection} 40 | import play.api.libs.json.{JsValue, Reads} 41 | 42 | import java.time.Instant 43 | import scala.concurrent.ExecutionContext 44 | import scala.concurrent.Future 45 | import scala.concurrent.duration._ 46 | import scala.concurrent.duration.Duration.MinusInf 47 | import scala.reflect.ClassTag 48 | import scala.reflect.runtime.universe.WeakTypeTag 49 | 50 | /** @define CounterDoc though it is common to use Couchbase to store exclusively JSON, Couchbase is actually 51 | * agnostic to what is stored. It is possible to use a document as a 'counter' - e.g. it 52 | * stores an integer. This is useful for use-cases such as implementing 53 | * AUTO_INCREMENT-style functionality, where each new document can be given a unique 54 | * monotonically increasing id. 55 | * @define OnlyBinary This method should not be used with JSON documents. This operates 56 | * at the byte level and is unsuitable for dealing with JSON documents. Use this method only 57 | * when explicitly dealing with binary or UTF-8 documents. It may invalidate an existing JSON 58 | * document. 59 | * @define OnlyCounter this method should not be used with JSON documents. Use this method only 60 | * when explicitly dealing with counter documents. It may invalidate an existing JSON 61 | * document. 62 | * @define Id the unique identifier of the document 63 | * @define CAS Couchbase documents all have a CAS (Compare-And-Set) field, a simple integer that allows 64 | * optimistic concurrency - e.g. it can detect if another agent has modified a document 65 | * in-between this agent getting and modifying the document. See 66 | * [[https://docs.couchbase.com/scala-sdk/1.0/howtos/json.html these JSON docs]] for a full 67 | * description. The default is 0, which disables CAS checking. 68 | * @define Timeout when the operation will timeout. This will default to `timeoutConfig().kvTimeout()` in the 69 | * provided [[com.couchbase.client.scala.env.ClusterEnvironment]]. 70 | * @define ErrorHandling any `scala.util.control.NonFatal` error returned will derive ultimately from 71 | * `com.couchbase.client.core.error.CouchbaseException`. See 72 | * [[https://docs.couchbase.com/scala-sdk/1.0/howtos/error-handling.html the error handling docs]] 73 | * for more detail. 74 | * @define SupportedTypes this can be of any type for which an implicit 75 | * `com.couchbase.client.scala.codec.Conversions.JsonSerializer` can be found: a list 76 | * of types that are supported 'out of the box' is available at 77 | * [[https://docs.couchbase.com/scala-sdk/1.0/howtos/json.html these JSON docs]] 78 | * @define Durability writes in Couchbase are written to a single node, and from there the Couchbase Server will 79 | * take care of sending that mutation to any configured replicas. This parameter provides 80 | * some control over ensuring the success of the mutation's replication. See 81 | * [[com.couchbase.client.scala.durability.Durability]] 82 | * for a detailed discussion. 83 | * @define Options configure options that affect this operation 84 | * @define OrNotFound or fail with a `CouchbaseException`. 85 | * This could be [[com.couchbase.client.core.error.DocumentNotFoundException]], 86 | * indicating the document could not be found. 87 | * @define ExpiryNote On mutations if this is left at the default (0), then any expiry 88 | * will be removed and the document will never expire. If the application wants to 89 | * preserve expiration then they should use the `withExpiration` parameter on any gets, 90 | * and provide the returned expiration parameter to any mutations. 91 | * 92 | * @define ExpiryTimeNote If both `expiry` and `expiryTime` are provided then `expiryTime` is used - 93 | * 94 | * see [[com.couchbase.client.scala.util.ExpiryUtil.expiryActual]] 95 | * @define Transcoder control over how JSON is converted and stored on the Couchbase Server. 96 | * If not specified it will default to to `transcoder()` in the 97 | * [[com.couchbase.client.scala.env.ClusterEnvironment]] 98 | */ 99 | final class CBBucket(val underlying: AsyncBucket, val cluster: AsyncCluster) { 100 | @inline def name: String = underlying.name 101 | 102 | lazy val defaultCol: AsyncCollection = underlying.defaultCollection 103 | 104 | @deprecated("Use underlying", "10.0.0") 105 | def asJava: AsyncBucket = underlying 106 | 107 | def getJsT[T: Reads]( 108 | id: String, 109 | options: GetOptions = GetOptions() 110 | )(implicit ec: ExecutionContext): Future[T] = 111 | get(id, options).map(_.contentAs[JsValue].get.as[T]) 112 | 113 | /** Fetches a full document from this collection. 114 | * 115 | * This overload provides only the most commonly used options. If you need to configure something more 116 | * esoteric, use the overload that takes an [[com.couchbase.client.scala.kv.GetOptions]] instead, which supports all available options. 117 | * 118 | * @param id $Id 119 | * @param timeout $Timeout 120 | * @param transcoder $Transcoder 121 | * @param withExpiry Couchbase documents optionally can have an expiration field set, e.g. when they will 122 | * automatically expire. For efficiency reasons, by default the value of this expiration 123 | * field is not fetched upon getting a document. If expiry is being used, then set this 124 | * field to true to ensure the expiration is fetched. This will not only make it available 125 | * in the returned result, but also ensure that the expiry is available to use when mutating 126 | * the document, to avoid accidentally resetting the expiry to the default of 0. 127 | * @param project Projection is an advanced feature allowing one or more fields to be fetched from a JSON 128 | * document, and the results combined into a `JsonObject` result. 129 | * 130 | * It combines the efficiency of a Sub-Document fetch, in that only specific fields need to be retrieved, with 131 | * the ease-of-handling of a regular fetch, in that the results can be handled as one JSON. 132 | * @return Future success with a `GetResult`, $OrNotFound 133 | * 134 | * $ErrorHandling 135 | */ 136 | def get( 137 | id: String, 138 | timeout: Duration = MinusInf, // MinusInf will be converted to kvReadTimeout 139 | transcoder: Transcoder = null, 140 | withExpiry: Boolean = false, 141 | project: Seq[String] = Nil 142 | ): Future[GetResult] = defaultCol.get( 143 | id, 144 | GetOptions( 145 | withExpiry, 146 | project, 147 | timeout, 148 | transcoder = Option(transcoder) 149 | ) 150 | ) 151 | 152 | /** See doc of the other overload method */ 153 | def get( 154 | id: String, 155 | options: GetOptions 156 | ): Future[GetResult] = defaultCol.get(id, options) 157 | 158 | /** usage: {{{ 159 | * import com.sandinh.couchbase.Implicits._ 160 | * 161 | * bucket.getT[String](id) 162 | * bucket.getT[JsValue](id) 163 | * }}} 164 | */ 165 | def getT[T](id: String)( 166 | implicit ec: ExecutionContext, 167 | ser: JsonDeserializer[T], 168 | tt: WeakTypeTag[T], 169 | tag: ClassTag[T] 170 | ): Future[T] = 171 | get(id).map(_.contentAs[T].get) 172 | 173 | /** Retrieves any available version of the document. 174 | * 175 | * The application should default to using `.get()` instead. This method is intended for high-availability 176 | * situations where, say, a `.get()` operation has failed, and the 177 | * application wants to return any - even possibly stale - data as soon as possible. 178 | * 179 | * Under the hood this sends a request to all configured replicas for the document, including the active, and 180 | * whichever returns first is returned. 181 | * 182 | * @param id $Id 183 | * @param timeout $Timeout 184 | * @param transcoder $Transcoder 185 | * @return Future success with a `GetReplicaResult`, $OrNotFound 186 | * 187 | * $ErrorHandling 188 | */ 189 | def getAnyReplica( 190 | id: String, 191 | timeout: Duration = MinusInf, // MinusInf will be converted to kvReadTimeout 192 | transcoder: Transcoder = null, 193 | ): Future[GetReplicaResult] = defaultCol.getAnyReplica( 194 | id, 195 | GetAnyReplicaOptions( 196 | timeout, 197 | transcoder = Option(transcoder) 198 | ) 199 | ) 200 | 201 | /** See doc of the other overload method */ 202 | def getAnyReplica( 203 | id: String, 204 | options: GetAnyReplicaOptions 205 | ): Future[GetReplicaResult] = defaultCol.getAnyReplica(id, options) 206 | 207 | /** Retrieves all available versions of the document. 208 | * 209 | * The application should default to using `.get()` instead. This method is intended for advanced scenarios, 210 | * including where a particular write has ambiguously failed (e.g. it may or may not have succeeded), and the 211 | * application wants to attempt manual verification and resolution. 212 | * 213 | * @param id $Id 214 | * @param timeout $Timeout 215 | * @param transcoder $Transcoder 216 | * @return Future success with a `GetReplicaResult`, $OrNotFound 217 | * 218 | * $ErrorHandling 219 | */ 220 | def getAllReplicas( 221 | id: String, 222 | timeout: Duration = MinusInf, // MinusInf will be converted to kvReadTimeout 223 | transcoder: Transcoder = null, 224 | ): Seq[Future[GetReplicaResult]] = defaultCol.getAllReplicas( 225 | id, 226 | GetAllReplicasOptions( 227 | timeout, 228 | transcoder = Option(transcoder) 229 | ) 230 | ) 231 | 232 | /** See doc of the other overload method */ 233 | def getAllReplicas( 234 | id: String, 235 | options: GetAllReplicasOptions 236 | ): Seq[Future[GetReplicaResult]] = defaultCol.getAllReplicas(id, options) 237 | 238 | /** Fetches a full document from this collection, and simultaneously lock the document from writes. 239 | * 240 | * The CAS value returned in the [[com.couchbase.client.scala.kv.GetResult]] is the document's 'key': 241 | * during the locked period, the document may only be modified by providing this CAS. 242 | * @param id $Id 243 | * @param lockTime how long to lock the document for 244 | * @param timeout $Timeout 245 | * @param transcoder $Transcoder 246 | * @return Future success with a `GetResult`, $OrNotFound 247 | * 248 | * $ErrorHandling 249 | */ 250 | def getAndLock( 251 | id: String, 252 | lockTime: Duration, 253 | timeout: Duration = MinusInf, // MinusInf will be converted to kvReadTimeout 254 | transcoder: Transcoder = null, 255 | ): Future[GetResult] = defaultCol.getAndLock( 256 | id, 257 | lockTime, 258 | GetAndLockOptions( 259 | timeout, 260 | transcoder = Option(transcoder) 261 | ) 262 | ) 263 | 264 | /** See doc of the other overload method */ 265 | def getAndLock( 266 | id: String, 267 | lockTime: Duration, 268 | options: GetAndLockOptions 269 | ): Future[GetResult] = defaultCol.getAndLock(id, lockTime, options) 270 | 271 | /** Fetches a full document from this collection, and simultaneously update the expiry value of the document. 272 | * @param id $Id 273 | * @param expiry $Expiry 274 | * @param timeout $Timeout 275 | * @param transcoder $Transcoder 276 | * @return Future success with a `GetResult`, $OrNotFound 277 | * 278 | * $ErrorHandling 279 | */ 280 | def getAndTouch( 281 | id: String, 282 | expiry: Duration, 283 | timeout: Duration = MinusInf, // MinusInf will be converted to kvReadTimeout 284 | transcoder: Transcoder = null, 285 | ): Future[GetResult] = defaultCol.getAndTouch( 286 | id, 287 | expiry, 288 | GetAndTouchOptions( 289 | timeout, 290 | transcoder = Option(transcoder) 291 | ) 292 | ) 293 | 294 | /** See doc of the other overload method */ 295 | def getAndTouch( 296 | id: String, 297 | expiry: Duration, 298 | options: GetAndTouchOptions 299 | ): Future[GetResult] = defaultCol.getAndTouch(id, expiry, options) 300 | 301 | /** Inserts a full document into this collection, if it does not exist already. 302 | * @param id $Id 303 | * @param content $SupportedTypes 304 | * @param durability $Durability 305 | * @param timeout $Timeout 306 | * @param transcoder $Transcoder 307 | * @param expiry should be used for any expiration times 30 days. 308 | * If over that, use `expiryTime: Instant` instead. 309 | * @param expiryTime should be used for any expiration times >= 30 days. 310 | * If below that, use `expiry: Duration` instead. 311 | * @return Future success with a `MutationResult`, or fail with a `CouchbaseException`. 312 | * This could be [[com.couchbase.client.core.error.DocumentExistsException]], 313 | * indicating the document already exists. 314 | * 315 | * $ErrorHandling 316 | * @note $ExpiryNote 317 | * @note $ExpiryTimeNote 318 | * @see [[upsert]], [[replace]] 319 | */ 320 | def insert[T: JsonSerializer]( 321 | id: String, 322 | content: T, 323 | durability: Durability = Disabled, 324 | timeout: Duration = MinusInf, 325 | transcoder: Transcoder = null, 326 | expiry: Duration = null, 327 | expiryTime: Instant = null, 328 | ): Future[MutationResult] = defaultCol.insert( 329 | id, 330 | content, 331 | InsertOptions( 332 | durability, 333 | timeout, 334 | transcoder = Option(transcoder), 335 | expiry = expiry, 336 | expiryTime = Option(expiryTime), 337 | ) 338 | ) 339 | 340 | /** See doc of the other overload method */ 341 | def insert[T: JsonSerializer]( 342 | id: String, 343 | content: T, 344 | options: InsertOptions 345 | ): Future[MutationResult] = defaultCol.insert(id, content, options) 346 | 347 | /** Upserts the contents of a full document in this collection. 348 | * 349 | * Upsert here means to insert the document if it does not exist, or replace the content if it does. 350 | * 351 | * @param id $Id 352 | * @param content $SupportedTypes 353 | * @param durability $Durability 354 | * @param timeout $Timeout 355 | * @param transcoder $Transcoder 356 | * @param expiry should be used for any expiration times 30 days. 357 | * If over that, use `expiryTime: Instant` instead. 358 | * @param expiryTime should be used for any expiration times >= 30 days. 359 | * If below that, use `expiry: Duration` instead. 360 | * @param preserveExpiry Whether an existing document's expiry should be preserved. 361 | * Requires Couchbase Server 7.0 or later. 362 | * @return Future success with a `MutationResult`, or fail with a `CouchbaseException`. 363 | * 364 | * $ErrorHandling 365 | * @note $ExpiryNote 366 | * @note $ExpiryTimeNote 367 | * @see [[insert]], [[replace]] 368 | */ 369 | def upsert[T: JsonSerializer]( 370 | id: String, 371 | content: T, 372 | durability: Durability = Disabled, 373 | timeout: Duration = MinusInf, 374 | transcoder: Transcoder = null, 375 | expiry: Duration = null, 376 | expiryTime: Instant = null, 377 | preserveExpiry: Boolean = false, 378 | ): Future[MutationResult] = defaultCol.upsert( 379 | id, 380 | content, 381 | UpsertOptions( 382 | durability, 383 | timeout, 384 | transcoder = Option(transcoder), 385 | expiry = expiry, 386 | expiryTime = Option(expiryTime), 387 | preserveExpiry = preserveExpiry 388 | ) 389 | ) 390 | 391 | /** See doc of the other overload method */ 392 | def upsert[T: JsonSerializer]( 393 | id: String, 394 | content: T, 395 | options: UpsertOptions 396 | ): Future[MutationResult] = defaultCol.upsert(id, content, options) 397 | 398 | /** Replaces the contents of a full document in this collection, if it already exists. 399 | * @param id $Id 400 | * @param content $SupportedTypes 401 | * @param cas $CAS 402 | * @param durability $Durability 403 | * @param timeout $Timeout 404 | * @param transcoder $Transcoder 405 | * @param expiry should be used for any expiration times 30 days. 406 | * If over that, use `expiryTime: Instant` instead. 407 | * @param expiryTime should be used for any expiration times >= 30 days. 408 | * If below that, use `expiry: Duration` instead. 409 | * @param preserveExpiry Whether an existing document's expiry should be preserved. 410 | * Requires Couchbase Server 7.0 or later. 411 | * @return Future success with a `MutationResult`, $OrNotFound 412 | * 413 | * $ErrorHandling 414 | * @note $ExpiryNote 415 | * @note $ExpiryTimeNote 416 | * @see [[insert]], [[upsert]] 417 | */ 418 | def replace[T: JsonSerializer]( 419 | id: String, 420 | content: T, 421 | cas: Long = 0, 422 | durability: Durability = Disabled, 423 | timeout: Duration = MinusInf, 424 | transcoder: Transcoder = null, 425 | expiry: Duration = null, 426 | expiryTime: Instant = null, 427 | preserveExpiry: Boolean = false, 428 | ): Future[MutationResult] = defaultCol.replace( 429 | id, 430 | content, 431 | ReplaceOptions( 432 | cas, 433 | durability, 434 | timeout, 435 | transcoder = Option(transcoder), 436 | expiry = expiry, 437 | expiryTime = Option(expiryTime), 438 | preserveExpiry = preserveExpiry 439 | ) 440 | ) 441 | 442 | /** See doc of the other overload method */ 443 | def replace[T: JsonSerializer]( 444 | id: String, 445 | content: T, 446 | options: ReplaceOptions 447 | ): Future[MutationResult] = defaultCol.replace(id, content, options) 448 | 449 | /** Removes a document from this collection, if it exists. 450 | * @param id $Id 451 | * @param cas $CAS 452 | * @param durability $Durability 453 | * @param timeout $Timeout 454 | * @return Future success with a `MutationResult`, $OrNotFound 455 | * 456 | * $ErrorHandling 457 | */ 458 | def remove( 459 | id: String, 460 | cas: Long = 0, 461 | durability: Durability = Disabled, 462 | timeout: Duration = MinusInf 463 | ): Future[MutationResult] = defaultCol.remove( 464 | id, 465 | RemoveOptions( 466 | cas, 467 | durability, 468 | timeout 469 | ) 470 | ) 471 | 472 | /** See doc of the other overload method */ 473 | def remove( 474 | id: String, 475 | options: RemoveOptions 476 | ): Future[MutationResult] = defaultCol.remove(id, options) 477 | 478 | /** Performs a view query against the cluster. 479 | * @param designDoc the view design document to use 480 | * @param viewName the view to use 481 | * @param options any view query options - see [[com.couchbase.client.scala.view.ViewOptions]] for documentation 482 | */ 483 | def viewQuery( 484 | designDoc: String, 485 | viewName: String, 486 | options: ViewOptions = ViewOptions() 487 | ): Future[ViewResult] = underlying.viewQuery(designDoc, viewName, options) 488 | 489 | /** See doc of the other overload method */ 490 | def query(statement: String, options: QueryOptions): Future[QueryResult] = 491 | cluster.query(statement, options) 492 | 493 | /** Performs a N1QL query against the cluster. 494 | * @param statement the N1QL statement to execute 495 | * @param parameters provides named or positional parameters for queries parameterised that way. 496 | * @param timeout sets a maximum timeout for processing. 497 | * @param adhoc if true (the default), adhoc mode is enabled: queries are just run. If false, adhoc mode is disabled 498 | * and transparent prepared statement mode is enabled: queries are first prepared so they can be executed 499 | * more efficiently in the future. 500 | */ 501 | def query( 502 | statement: String, 503 | parameters: QueryParameters = QueryParameters.None, 504 | timeout: Duration = 505 | cluster.env.core.timeoutConfig.queryTimeout().toNanos.nanos, 506 | adhoc: Boolean = true 507 | ): Future[QueryResult] = 508 | cluster.query(statement, parameters, timeout, adhoc) 509 | 510 | /** Unlock a locked document. 511 | * @param id $Id 512 | * @param cas must match the CAS value return from a previous `.getAndLock()` to successfully 513 | * unlock the document 514 | * @param timeout $Timeout 515 | * @return Future success with a `Unit`, $OrNotFound 516 | * 517 | * $ErrorHandling 518 | */ 519 | def unlock( 520 | id: String, 521 | cas: Long, 522 | timeout: Duration = MinusInf 523 | ): Future[Unit] = defaultCol.unlock(id, cas, UnlockOptions(timeout)) 524 | 525 | /** See doc of the other overload method */ 526 | def unlock( 527 | id: String, 528 | cas: Long, 529 | options: UnlockOptions 530 | ): Future[Unit] = defaultCol.unlock(id, cas, options) 531 | 532 | /** Updates the expiry of the document with the given id. 533 | * @param id $Id 534 | * @param timeout $Timeout 535 | * 536 | * @return Future success with a `MutationResult`, $OrNotFound 537 | * 538 | * $ErrorHandling 539 | */ 540 | def touch( 541 | id: String, 542 | expiry: Duration, 543 | timeout: Duration = MinusInf 544 | ): Future[MutationResult] = defaultCol.touch( 545 | id, 546 | expiry, 547 | TouchOptions( 548 | timeout 549 | ) 550 | ) 551 | 552 | /** See doc of the other overload method */ 553 | def touch( 554 | id: String, 555 | expiry: Duration, 556 | options: TouchOptions 557 | ): Future[MutationResult] = defaultCol.touch(id, expiry, options) 558 | 559 | /** Increment a Couchbase 'counter' document. $CounterDoc 560 | * 561 | * $OnlyCounter 562 | * 563 | * @param id $Id 564 | * @param delta the amount to increment by 565 | * @param initial if not-None, the amount to initialise the document too, if it does not exist. If this is 566 | * not set, and the document does not exist, the result Future will failed with DocumentNotFoundException 567 | * @param durability $Durability 568 | * @param timeout $Timeout 569 | * @return Future success with a `CounterResult`, $OrNotFound 570 | * 571 | * $ErrorHandling 572 | * 573 | * @note $ExpiryNote 574 | * @note $ExpiryTimeNote 575 | */ 576 | def counter( 577 | id: String, 578 | delta: Long = 0L, 579 | initial: Option[Long] = None, 580 | durability: Durability = Disabled, 581 | timeout: Duration = MinusInf, // MinusInf will be converted to kvReadTimeout 582 | expiry: Duration = null, 583 | expiryTime: Instant = null, 584 | ): Future[CounterResult] = defaultCol.binary.increment( 585 | id, 586 | delta, 587 | IncrementOptions( 588 | initial, 589 | durability, 590 | timeout, 591 | expiry = expiry, 592 | expiryTime = Option(expiryTime) 593 | ) 594 | ) 595 | 596 | /** convenient method. {{{ = counter(id).map(_.content).recoverNotExist(default) }}} */ 597 | def getCounter(id: String, default: Long = 0L)( 598 | implicit ec: ExecutionContext 599 | ): Future[Long] = counter(id) 600 | .map(_.content) 601 | .recover { case _: DocumentNotFoundException => default } 602 | 603 | /** See doc of the other overload method */ 604 | def counter( 605 | id: String, 606 | delta: Long, 607 | options: IncrementOptions 608 | ): Future[CounterResult] = defaultCol.binary.increment(id, delta, options) 609 | 610 | /** See doc of the other overload method */ 611 | def counter( 612 | id: String, 613 | delta: Long, 614 | initial: Long 615 | ): Future[CounterResult] = 616 | defaultCol.binary.increment(id, delta, IncrementOptions(Some(initial))) 617 | 618 | /** See doc of the other overload method */ 619 | def counter( 620 | id: String, 621 | delta: Long, 622 | initial: Long, 623 | expiry: Duration 624 | ): Future[CounterResult] = 625 | defaultCol.binary.increment( 626 | id, 627 | delta, 628 | IncrementOptions(Some(initial), expiry = expiry) 629 | ) 630 | 631 | /** Add bytes to the end of a Couchbase binary document. 632 | * 633 | * $OnlyBinary 634 | * 635 | * @param id $Id 636 | * @param content the bytes to append 637 | * @param cas $CAS 638 | * @param durability $Durability 639 | * @param timeout $Timeout 640 | * 641 | * @return Future success with a `MutationResult`, $OrNotFound 642 | * 643 | * $ErrorHandling 644 | */ 645 | def append( 646 | id: String, 647 | content: Array[Byte], 648 | cas: Long = 0, 649 | durability: Durability = Disabled, 650 | timeout: Duration = MinusInf 651 | ): Future[MutationResult] = defaultCol.binary.append( 652 | id, 653 | content, 654 | AppendOptions( 655 | cas, 656 | durability, 657 | timeout 658 | ) 659 | ) 660 | 661 | /** See doc of the other overload method */ 662 | def append( 663 | id: String, 664 | content: Array[Byte], 665 | options: AppendOptions 666 | ): Future[MutationResult] = defaultCol.binary.append(id, content, options) 667 | 668 | /** Add bytes to the beginning of a Couchbase binary document. 669 | * 670 | * $OnlyBinary 671 | * 672 | * @param id $Id 673 | * @param content the bytes to prepend 674 | * @param cas $CAS 675 | * @param durability $Durability 676 | * @param timeout $Timeout 677 | * 678 | * @return Future success with a `MutationResult`, $OrNotFound 679 | * 680 | * $ErrorHandling 681 | */ 682 | def prepend( 683 | id: String, 684 | content: Array[Byte], 685 | cas: Long = 0, 686 | durability: Durability = Disabled, 687 | timeout: Duration = MinusInf 688 | ): Future[MutationResult] = defaultCol.binary.prepend( 689 | id, 690 | content, 691 | PrependOptions( 692 | cas, 693 | durability, 694 | timeout 695 | ) 696 | ) 697 | 698 | /** See doc of the other overload method */ 699 | def prepend( 700 | id: String, 701 | content: Array[Byte], 702 | options: PrependOptions 703 | ): Future[MutationResult] = defaultCol.binary.prepend(id, content, options) 704 | 705 | def viewIndexes: AsyncViewIndexManager = underlying.viewIndexes 706 | 707 | /** The AsyncBucketManager provides access to creating and getting buckets. */ 708 | def bucketManager: AsyncBucketManager = cluster.buckets 709 | } 710 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sandinh/couchbase/CBCluster.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import com.couchbase.client.scala.env.TimeoutConfig 4 | import com.couchbase.client.scala.ClusterOptions 5 | import com.couchbase.client.scala.env.{ 6 | ClusterEnvironment, 7 | PasswordAuthenticator 8 | } 9 | import com.couchbase.client.scala.AsyncCluster 10 | import com.typesafe.config.Config 11 | 12 | import javax.inject._ 13 | import scala.concurrent.{Await, Future} 14 | import scala.concurrent.duration._ 15 | 16 | /** @note ensure call #disconnect() at the end of application life */ 17 | @Singleton 18 | class CBCluster @Inject() (config: Config) { 19 | val env: ClusterEnvironment = CbEnvBuilder(config) 20 | 21 | @deprecated("Use underlying", "10.0.0") 22 | def asJava: AsyncCluster = underlying 23 | 24 | lazy val underlying: AsyncCluster = { 25 | val conf = config.getConfig("com.sandinh.couchbase") 26 | AsyncCluster 27 | .connect( 28 | conf.getString("connectionString"), 29 | ClusterOptions( 30 | PasswordAuthenticator( 31 | conf.getString("user"), 32 | conf.getString("password") 33 | ), 34 | Some(env) 35 | ), 36 | ) 37 | .get 38 | } 39 | 40 | def bucket(bucketName: String): CBBucket = 41 | new CBBucket(underlying.bucket(bucketName), underlying) 42 | 43 | @deprecated("Use bucket", "10.0.0") 44 | def openBucket(bucketName: String): CBBucket = bucket(bucketName) 45 | 46 | @deprecated("Use bucket", "10.0.0") 47 | def openBucketSync(bucketName: String): CBBucket = bucket(bucketName) 48 | 49 | def disconnect(): Future[Unit] = underlying.disconnect() 50 | 51 | def disconnectSync(): Unit = Await 52 | .result( 53 | disconnect(), 54 | env.core.timeoutConfig().disconnectTimeout.toNanos.nanos 55 | ) 56 | } 57 | 58 | private object CbEnvBuilder { 59 | def apply(config: Config): ClusterEnvironment = { 60 | val conf = config.getConfig("com.couchbase.timeout") 61 | def c(k: String): Option[Duration] = 62 | if (conf.hasPath(k)) Some(conf.getDuration(k).toNanos.nanos) 63 | else None 64 | val timeoutConfig = TimeoutConfig( 65 | c("kv"), 66 | c("kvDurable"), 67 | c("management"), 68 | c("query"), 69 | c("view"), 70 | c("search"), 71 | c("analytics"), 72 | c("connect"), 73 | c("disconnect") 74 | ) 75 | ClusterEnvironment 76 | .Builder(owned = true) 77 | .timeoutConfig(timeoutConfig) 78 | .build 79 | .get 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sandinh/couchbase/Implicits.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import com.couchbase.client.core.error.DocumentNotFoundException 4 | import com.couchbase.client.scala.codec.JsonDeserializer.PlayConvert 5 | import com.couchbase.client.scala.codec.{JsonDeserializer, JsonSerializer} 6 | import com.couchbase.client.scala.codec.JsonSerializer.PlayEncode 7 | import com.couchbase.client.scala.json.{JsonArray, JsonObject, ToPlayJs} 8 | import play.api.libs.json._ 9 | 10 | import scala.concurrent.ExecutionContext 11 | import scala.concurrent.Future 12 | 13 | object Implicits { 14 | @deprecated( 15 | "Use .recover { case _: DocumentNotFoundException => .. }", 16 | "10.0.0" 17 | ) 18 | implicit class DocNotExistFuture[T](private val underlying: Future[T]) 19 | extends AnyVal { 20 | @deprecated( 21 | "Use .recover { case _: DocumentNotFoundException => default } directly", 22 | "10.0.0" 23 | ) 24 | def recoverNotExist[U >: T](default: => U)( 25 | implicit ec: ExecutionContext 26 | ): Future[U] = 27 | underlying.recover { case _: DocumentNotFoundException => default } 28 | 29 | @deprecated( 30 | "Use .transform(..) or .map(Option(_)).recover { case _: DocumentNotFoundException => None }", 31 | "10.0.0" 32 | ) 33 | def optNotExist(implicit ec: ExecutionContext): Future[Option[T]] = 34 | underlying 35 | .map(Option(_)) 36 | .recover { case _: DocumentNotFoundException => None } 37 | } 38 | 39 | implicit final class RichJsonObject(private val o: JsonObject) 40 | extends AnyVal { 41 | @inline def toPlayJs: JsObject = ToPlayJs(o) 42 | } 43 | 44 | implicit final class RichJsonArray(private val a: JsonArray) extends AnyVal { 45 | @inline def toPlayJs: JsArray = ToPlayJs(a) 46 | } 47 | 48 | implicit def jsonSerializer[T: Writes]: JsonSerializer[T] = content => 49 | PlayEncode.serialize(Json.toJson(content)) 50 | 51 | implicit def jsonDeserializer[T: Reads]: JsonDeserializer[T] = bytes => 52 | PlayConvert.deserialize(bytes).map(_.as[T]) 53 | } 54 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sandinh/couchbase/access/CaoKey0.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase.access 2 | 3 | import com.couchbase.client.scala.kv.MutationResult 4 | 5 | import scala.concurrent.{ExecutionContext, Future} 6 | 7 | /** Base trait for Couchbase Access Object to access json documents 8 | * that can be decode/encode to/from the `T` type - 9 | * which is store in couchbase at a constance key 10 | */ 11 | private[access] trait CaoKey0[T] extends CaoKeyId[T] { 12 | protected val key: String 13 | 14 | final def get()(implicit ec: ExecutionContext): Future[T] = 15 | get(key) 16 | 17 | final def getOrElse(default: => T)( 18 | implicit ec: ExecutionContext 19 | ): Future[T] = 20 | getOrElse(key)(default) 21 | 22 | final def getWithCAS()( 23 | implicit ec: ExecutionContext 24 | ): Future[(T, Long)] = 25 | getWithCAS(key) 26 | 27 | final def getOrElseWithCAS(default: => T)( 28 | implicit ec: ExecutionContext 29 | ): Future[(T, Long)] = 30 | getOrElseWithCAS(key)(default) 31 | 32 | def upsert(content: T): Future[MutationResult] = 33 | upsert(key, content) 34 | 35 | def replace(content: T): Future[MutationResult] = 36 | replace(key, content) 37 | 38 | /** convenient method. = upsert(..).map(_ => t) */ 39 | final def setT(content: T)( 40 | implicit ec: ExecutionContext 41 | ): Future[T] = 42 | upsert(key, content).map(_ => content) 43 | 44 | final def change()(f: Option[T] => T)( 45 | implicit ec: ExecutionContext 46 | ): Future[MutationResult] = 47 | change(key)(f) 48 | 49 | final def flatChange()(f: Option[T] => Future[T])( 50 | implicit ec: ExecutionContext 51 | ): Future[MutationResult] = 52 | flatChange(key)(f) 53 | 54 | final def remove(): Future[MutationResult] = remove(key) 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sandinh/couchbase/access/CaoKey1.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase.access 2 | 3 | import com.couchbase.client.scala.kv.{ 4 | GetOptions, 5 | GetResult, 6 | InsertOptions, 7 | MutationResult, 8 | RemoveOptions, 9 | ReplaceOptions, 10 | UpsertOptions 11 | } 12 | import com.couchbase.client.scala.codec.JsonSerializer.PlayEncode 13 | import com.sandinh.couchbase.CBBucket 14 | import play.api.libs.json.{Format, Json} 15 | 16 | import scala.concurrent.Future 17 | 18 | /** @inheritdoc */ 19 | private[access] trait CaoKeyId[T] extends CaoKey1[T, String] { 20 | 21 | /** @inheritdoc */ 22 | override final protected def key(id: String): String = id 23 | } 24 | 25 | /** Base trait for Couchbase Access Object to access json documents 26 | * that can be decode/encode to/from the `T` type - 27 | * which is stored in couchbase at the key generated from the T.key(A) method. 28 | * 29 | * This trait permit we interact (get/upsert/replace/..) with couchbase server 30 | * through a typed interface: 31 | * instead of {{{ 32 | * bucket.get(id: String): GetResult 33 | * bucket.upsert(id: String, content: T)(implicit serializer: JsonSerializer[T]): MutationResult 34 | * }}} 35 | * , we can: {{{ 36 | * case class Acc(..) 37 | * object Acc { 38 | * implicit val fmt: OFormat[Acc] = Json.format[Acc] 39 | * // Used in upsert 40 | * implicit val ser: JsonSerializer[Trophy] = t => PlayEncode.serialize(Json.toJson(t)) 41 | * } 42 | * class AccCao(cluster: CBCluster) extends JsCao[Acc](cluster.bucket("acc")) 43 | * val cao: AccCao = ??? 44 | * cao.get(id: String): Future[Acc] 45 | * cao.upsert(id: String, content: Acc): Future[MutationResult] 46 | * }}} 47 | * , or: {{{ 48 | * class AccCao(cluster: CBCluster) extends JsCao1[Acc, Int](cluster.bucket("acc")) { 49 | * protected def key(uid: Int): String = "a" + uid 50 | * } 51 | * val cao: AccCao = ??? 52 | * cao.get(uid: Int): Future[Acc] 53 | * cao.upsert(uid: Int, content: Acc): Future[MutationResult] 54 | * }}} 55 | * @see [[JsCao]], [[JsCao1]], [[JsCao2]] 56 | */ 57 | private[access] trait CaoKey1[T, A] extends CaoTrait[T, A] { 58 | val bucket: CBBucket 59 | 60 | protected implicit val fmt: Format[T] 61 | 62 | /** Map param of type A to a CB key 63 | * @return CB key (id) 64 | */ 65 | protected def key(a: A): String 66 | 67 | /** @inheritdoc */ 68 | final def getResult( 69 | a: A, 70 | options: GetOptions = GetOptions() 71 | ): Future[GetResult] = bucket.get(key(a), options) 72 | 73 | /** @inheritdoc */ 74 | final def insert( 75 | a: A, 76 | content: T, 77 | options: InsertOptions = InsertOptions() 78 | ): Future[MutationResult] = 79 | bucket.insert(key(a), Json.toJson(content), options.expiry(expiry)) 80 | 81 | /** @inheritdoc */ 82 | final def upsert( 83 | a: A, 84 | content: T, 85 | options: UpsertOptions = UpsertOptions() 86 | ): Future[MutationResult] = 87 | bucket.upsert(key(a), Json.toJson(content), options.expiry(expiry)) 88 | 89 | /** @inheritdoc */ 90 | final def replace( 91 | a: A, 92 | content: T, 93 | options: ReplaceOptions 94 | ): Future[MutationResult] = 95 | bucket.replace(key(a), Json.toJson(content), options.expiry(expiry)) 96 | 97 | /** @inheritdoc */ 98 | final def remove( 99 | a: A, 100 | options: RemoveOptions = RemoveOptions() 101 | ): Future[MutationResult] = bucket.remove(key(a), options) 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sandinh/couchbase/access/CaoKey2.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase.access 2 | 3 | import com.couchbase.client.scala.kv.MutationResult 4 | import scala.concurrent.{ExecutionContext, Future} 5 | 6 | /** Base trait for Couchbase Access Object to access json documents 7 | * that can be decode/encode to/from the `T` type - 8 | * which is store in couchbase at key generated from the T.key(A, B) method 9 | */ 10 | private[access] trait CaoKey2[T, A, B] extends CaoKeyId[T] { 11 | 12 | /** Map 2 param of type A, B to a CB key 13 | * @return CB key 14 | */ 15 | protected def key(a: A, b: B): String 16 | 17 | final def get(a: A, b: B)(implicit ec: ExecutionContext): Future[T] = 18 | get(key(a, b)) 19 | 20 | final def getOrElse(a: A, b: B)(default: => T)( 21 | implicit ec: ExecutionContext 22 | ): Future[T] = 23 | getOrElse(key(a, b))(default) 24 | 25 | final def getBulk(aa: Seq[A], b: B)( 26 | implicit ec: ExecutionContext 27 | ): Future[Seq[T]] = 28 | Future.traverse(aa)(get(_, b)) 29 | 30 | final def getWithCAS(a: A, b: B)( 31 | implicit ec: ExecutionContext 32 | ): Future[(T, Long)] = 33 | getWithCAS(key(a, b)) 34 | 35 | final def getOrElseWithCAS(a: A, b: B)(default: => T)( 36 | implicit ec: ExecutionContext 37 | ): Future[(T, Long)] = 38 | getOrElseWithCAS(key(a, b))(default) 39 | 40 | @deprecated("Use upsert or replace. This `set` method use `upsert`", "10.0.0") 41 | final def set(a: A, b: B, content: T): Future[MutationResult] = 42 | upsert(key(a, b), content) 43 | 44 | def upsert(a: A, b: B, content: T): Future[MutationResult] = 45 | upsert(key(a, b), content) 46 | 47 | def replace(a: A, b: B, content: T): Future[MutationResult] = 48 | replace(key(a, b), content) 49 | 50 | /** convenient method. = upsert(..).map(_ => t) */ 51 | final def setT(a: A, b: B, content: T)( 52 | implicit ec: ExecutionContext 53 | ): Future[T] = 54 | upsert(key(a, b), content).map(_ => content) 55 | 56 | final def setBulk(aa: Seq[A], b: B, contents: Seq[T])( 57 | implicit ec: ExecutionContext 58 | ): Future[Seq[MutationResult]] = 59 | setBulk(aa.map(key(_, b)), contents) 60 | 61 | final def change(a: A, b: B)(f: Option[T] => T)( 62 | implicit ec: ExecutionContext 63 | ): Future[MutationResult] = 64 | change(key(a, b))(f) 65 | 66 | final def flatChange(a: A, b: B)(f: Option[T] => Future[T])( 67 | implicit ec: ExecutionContext 68 | ): Future[MutationResult] = 69 | flatChange(key(a, b))(f) 70 | 71 | final def changeBulk(aa: Seq[A], b: B)(f: Option[T] => T)( 72 | implicit ec: ExecutionContext 73 | ): Future[Seq[MutationResult]] = 74 | Future.traverse(aa)(change(_, b)(f)) 75 | 76 | final def flatChangeBulk(aa: Seq[A], b: B)( 77 | f: Option[T] => Future[T] 78 | )(implicit ec: ExecutionContext): Future[Seq[MutationResult]] = 79 | Future.traverse(aa)(flatChange(_, b)(f)) 80 | 81 | final def remove(a: A, b: B): Future[MutationResult] = remove(key(a, b)) 82 | } 83 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sandinh/couchbase/access/CaoTrait.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase.access 2 | 3 | import com.couchbase.client.scala.kv.OptionsConvert._ 4 | import com.couchbase.client.core.error.{ 5 | CasMismatchException, 6 | DocumentExistsException, 7 | DocumentNotFoundException 8 | } 9 | import com.couchbase.client.scala.durability.Durability 10 | import com.couchbase.client.scala.durability.Durability.Disabled 11 | import com.couchbase.client.scala.kv.{ 12 | GetOptions, 13 | GetResult, 14 | InsertOptions, 15 | MutationResult, 16 | RemoveOptions, 17 | ReplaceOptions, 18 | UpsertOptions 19 | } 20 | import play.api.libs.json.{Format, JsValue} 21 | 22 | import scala.concurrent.{ExecutionContext, Future} 23 | import scala.concurrent.duration.Duration 24 | 25 | /** Common interface for [[JsCao]] and [[JsCao1]] 26 | * 27 | * @tparam A String for document ID type, as in [[JsCao]] 28 | * Or some type that will be used to create the document id, as in JsCao1.key(A) 29 | */ 30 | private[access] trait CaoTrait[T, A] { 31 | protected implicit val fmt: Format[T] 32 | 33 | protected def expiry: Duration = null 34 | 35 | @deprecated("", "10.0.0") 36 | final type DocumentCAS = (T, Long) 37 | 38 | /** @param id document id or the param of JsCao1.key(a: A) */ 39 | def getResult( 40 | id: A, 41 | options: GetOptions = GetOptions() 42 | ): Future[GetResult] 43 | 44 | /** @param id document id or the param of JsCao1.key(a: A) */ 45 | final def get( 46 | id: A, 47 | options: GetOptions = GetOptions() 48 | )(implicit ec: ExecutionContext): Future[T] = 49 | getResult(id, options).map(_.contentAs[JsValue].get.as[T]) 50 | 51 | /** @param id document id or the param of JsCao1.key(a: A) */ 52 | final def getWithCAS( 53 | id: A, 54 | options: GetOptions = GetOptions() 55 | )(implicit ec: ExecutionContext): Future[(T, Long)] = 56 | getResult(id, options).map { r => 57 | r.contentAs[JsValue].get.as[T] -> r.cas 58 | } 59 | 60 | /** @param id document id or the param of JsCao1.key(a: A) */ 61 | final def getOrElse( 62 | id: A, 63 | options: GetOptions = GetOptions() 64 | )(default: => T)(implicit ec: ExecutionContext): Future[T] = 65 | get(id, options).recover { case _: DocumentNotFoundException => 66 | default 67 | } 68 | 69 | /** @param id document id or the param of JsCao1.key(a: A) */ 70 | final def getOrElseWithCAS( 71 | id: A, 72 | options: GetOptions = GetOptions() 73 | )(default: => T)(implicit ec: ExecutionContext): Future[(T, Long)] = 74 | getWithCAS(id, options).recover { case _: DocumentNotFoundException => 75 | (default, -1) 76 | } 77 | 78 | /** @param id document id or the param of JsCao1.key(a: A) */ 79 | final def getOrUpdate( 80 | id: A, 81 | options: GetOptions = GetOptions() 82 | )(default: => T)(implicit ec: ExecutionContext): Future[T] = 83 | get(id, options).recoverWith { case _: DocumentNotFoundException => 84 | setT(id, default) 85 | } 86 | 87 | /** @param ids Seq of document id or the param of JsCao1.key(a: A) */ 88 | final def getBulk(ids: Seq[A])( 89 | implicit ec: ExecutionContext 90 | ): Future[Seq[T]] = 91 | Future.traverse(ids)(get(_, GetOptions())) 92 | 93 | /** @param ids Seq of document id or the param of JsCao1.key(a: A) */ 94 | final def getBulkWithCAS(ids: Seq[A])( 95 | implicit ec: ExecutionContext 96 | ): Future[Seq[(T, Long)]] = 97 | Future.traverse(ids)(getWithCAS(_, GetOptions())) 98 | 99 | /** @param ids Seq of document id or the param of JsCao1.key(a: A) */ 100 | final def setBulk(ids: Seq[A], contents: Seq[T])( 101 | implicit ec: ExecutionContext 102 | ): Future[Seq[MutationResult]] = 103 | Future.traverse(ids zip contents) { case (a, t) => upsert(a, t) } 104 | 105 | @deprecated("Use upsert or replace. This `set` method use `upsert`", "10.0.0") 106 | final def set( 107 | id: A, 108 | content: T, 109 | options: UpsertOptions = UpsertOptions() 110 | ): Future[MutationResult] = upsert(id, content, options) 111 | 112 | /** Inserts a full document into this collection, if it does not exist already. 113 | * @param id document id or the param of JsCao1.key(a: A) 114 | * @param content the object of your own type `T` ex T=`case class User(...)` 115 | * to be insert into cb server 116 | * @see [[upsert]], [[replace]] 117 | */ 118 | def insert( 119 | id: A, 120 | content: T, 121 | options: InsertOptions = InsertOptions() 122 | ): Future[MutationResult] 123 | 124 | /** Upserts the contents of a full document in this collection. 125 | * @param id document id or the param of JsCao1.key(a: A) 126 | * @param content the object of your own type `T` ex T=`case class User(...)` 127 | * to be upsert into cb server 128 | * @see [[insert]], [[replace]] 129 | */ 130 | def upsert( 131 | id: A, 132 | content: T, 133 | options: UpsertOptions = UpsertOptions() 134 | ): Future[MutationResult] 135 | 136 | /** Replaces the contents of a full document, if it already exists. 137 | * @param id document id or the param of JsCao1.key(a: A) 138 | * @param content the object of your own type `T` ex T=`case class User(...)` 139 | * to be replace into cb server 140 | * @see [[insert]], [[upsert]] 141 | */ 142 | def replace( 143 | id: A, 144 | content: T, 145 | options: ReplaceOptions 146 | ): Future[MutationResult] 147 | 148 | /** Replaces the contents of a full document, if it already exists. 149 | * @param id document id or the param of JsCao1.key(a: A) 150 | * @param content the object of your own type `T` ex T=`case class User(...)` 151 | * to be replace into cb server 152 | * @see [[insert]], [[upsert]] 153 | */ 154 | final def replace( 155 | id: A, 156 | content: T, 157 | cas: Long = 0, 158 | durability: Durability = Disabled, 159 | timeout: Duration = Duration.MinusInf 160 | ): Future[MutationResult] = replace( 161 | id, 162 | content, 163 | ReplaceOptions().cas(cas).durability(durability).timeout(timeout) 164 | ) 165 | 166 | @deprecated("Use replace", "10.0.0") 167 | final def update(id: A, content: T, cas: Long = 0): Future[MutationResult] = 168 | replace(id, content, cas) 169 | 170 | @deprecated("Use replace", "10.0.0") 171 | final def updateWithCAS( 172 | id: A, 173 | content: T, 174 | cas: Long = 0 175 | ): Future[MutationResult] = 176 | replace(id, content, cas) 177 | 178 | /** convenient method. = set(..).map(_ => t) */ 179 | final def setT( 180 | id: A, 181 | content: T, 182 | options: UpsertOptions = UpsertOptions() 183 | )(implicit ec: ExecutionContext): Future[T] = 184 | upsert(id, content, options).map(_ => content) 185 | 186 | /** Removes a document from this collection, if it exists. 187 | * @param id document id or the param of JsCao1.key(a: A) 188 | */ 189 | def remove(id: A, options: RemoveOptions): Future[MutationResult] 190 | 191 | /** @param id document id or the param of JsCao1.key(a: A) 192 | * @note This method will retry on CasMismatchException 193 | */ 194 | final def change( 195 | id: A, 196 | getOptions: GetOptions = GetOptions(), 197 | setOptions: InsertOptions = InsertOptions() 198 | )( 199 | f: Option[T] => T 200 | )(implicit ec: ExecutionContext): Future[MutationResult] = 201 | getWithCAS(id, getOptions) 202 | .map(Option(_)) 203 | .recover { case _: DocumentNotFoundException => None } 204 | .flatMap { 205 | case None => 206 | insert(id, f(None), setOptions) 207 | .recoverWith { case _: DocumentExistsException => 208 | change(id, getOptions, setOptions)(f) 209 | } 210 | case Some((t, cas)) => 211 | replace(id, f(Some(t)), setOptions.toReplaceOptions(cas)) 212 | .recoverWith { case _: CasMismatchException => 213 | change(id, getOptions, setOptions)(f) 214 | } 215 | } 216 | 217 | /** @param id document id or the param of JsCao1.key(a: A) */ 218 | final def flatChange( 219 | id: A, 220 | getOptions: GetOptions = GetOptions(), 221 | setOptions: InsertOptions = InsertOptions() 222 | )( 223 | f: Option[T] => Future[T] 224 | )(implicit ec: ExecutionContext): Future[MutationResult] = 225 | getWithCAS(id, getOptions) 226 | .map(Option(_)) 227 | .recover { case _: DocumentNotFoundException => None } 228 | .flatMap { 229 | case None => 230 | f(None).flatMap { 231 | insert(id, _, setOptions).recoverWith { 232 | case _: DocumentExistsException => 233 | flatChange(id, getOptions, setOptions)(f) 234 | } 235 | } 236 | case Some((t, cas)) => 237 | f(Some(t)) 238 | .flatMap(replace(id, _, setOptions.toReplaceOptions(cas))) 239 | .recoverWith { case _: CasMismatchException => 240 | flatChange(id, getOptions, setOptions)(f) 241 | } 242 | } 243 | 244 | /** @param ids Seq of document id or the param of JsCao1.key(a: A) */ 245 | final def changeBulk(ids: Seq[A])( 246 | f: Option[T] => T 247 | )(implicit ec: ExecutionContext): Future[Seq[MutationResult]] = 248 | Future.traverse(ids)(change(_)(f)) 249 | 250 | /** @param ids Seq of document id or the param of JsCao1.key(a: A) */ 251 | final def flatChangeBulk(ids: Seq[A])( 252 | f: Option[T] => Future[T] 253 | )(implicit ec: ExecutionContext): Future[Seq[MutationResult]] = 254 | Future.traverse(ids)(flatChange(_)(f)) 255 | } 256 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sandinh/couchbase/access/JsCao.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase.access 2 | 3 | import com.sandinh.couchbase.CBBucket 4 | import play.api.libs.json.Format 5 | 6 | /** @inheritdoc 7 | * @see [[CaoKey1]], [[JsCao0]], [[JsCao1]], [[JsCao2]] 8 | */ 9 | class JsCao[T]( 10 | val bucket: CBBucket 11 | )( 12 | protected implicit val fmt: Format[T] 13 | ) extends CaoKeyId[T] 14 | 15 | /** @inheritdoc 16 | * @see [[CaoKey1]], [[JsCao]], [[JsCao1]], [[JsCao2]] 17 | */ 18 | class JsCao0[T]( 19 | val bucket: CBBucket, 20 | protected val key: String 21 | )( 22 | protected implicit val fmt: Format[T] 23 | ) extends CaoKey0[T] 24 | 25 | /** @inheritdoc 26 | * @see [[CaoKey1]], [[JsCao0]], [[JsCao]], [[JsCao2]] 27 | */ 28 | abstract class JsCao1[T, A]( 29 | val bucket: CBBucket 30 | )( 31 | protected implicit val fmt: Format[T] 32 | ) extends CaoKey1[T, A] { 33 | @deprecated("May be removed in later versions", "10.0.0") 34 | final lazy val self = new JsCao(bucket) 35 | } 36 | 37 | /** @inheritdoc 38 | * @see [[CaoKey1]], [[JsCao0]], [[JsCao1]], [[JsCao]] 39 | */ 40 | abstract class JsCao2[T, A, B]( 41 | val bucket: CBBucket 42 | )( 43 | protected implicit val fmt: Format[T] 44 | ) extends CaoKey2[T, A, B] 45 | -------------------------------------------------------------------------------- /core/src/main/scala/com/sandinh/couchbase/package.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh 2 | 3 | package object couchbase { 4 | @deprecated("Use CBBucket", "10.0.0") 5 | type ScalaBucket = CBBucket 6 | } 7 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sandinh/couchbase/BackwardCompatSpec.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import com.typesafe.config.Config 4 | import javax.inject.Inject 5 | import scala.util.Random 6 | import scala.sys.process._ 7 | 8 | class BackwardCompatSpec extends GuiceSpecBase { 9 | @Inject private var conf: Config = _ 10 | private lazy val cp = conf.getString("compat-test.classpath") 11 | 12 | private def run(args: String) = 13 | s"java -cp $cp com.sandinh.couchbase.Main $args".!!.trim 14 | 15 | "counter" should { 16 | def rndKv() = 17 | ("compat" + Random.nextLong(), Random.nextLong().abs + 1) 18 | "backward compat: new set, old get" in { 19 | val (k, v) = rndKv() 20 | cb.bk1.counter(k, 0, v).map(_.content) must beEqualTo(v).await 21 | run(s"get counter $k") === v.toString 22 | } 23 | "backward compat: old set, new get" in { 24 | val (k, v) = rndKv() 25 | run(s"set counter $k $v") === v.toString 26 | cb.bk1.getCounter(k) must beEqualTo(v).await 27 | } 28 | } 29 | 30 | "CompatString" should { 31 | def rndKv() = 32 | ("compat" + Random.nextLong(), "" + Random.nextLong()) 33 | "backward compat: new set, old get" in { 34 | val (k, v) = rndKv() 35 | cb.bk1.upsert(k, v).map(_.cas) must be_>(0L).await 36 | run(s"get CompatString $k") === v 37 | } 38 | "backward compat: old set, new get" in { 39 | val (k, v) = rndKv() 40 | run(s"set CompatString $k $v") === v 41 | cb.bk1.getT[String](k) must beEqualTo(v).await 42 | 43 | run(s"set String $k $v") === v 44 | cb.bk1.getT[String](k) must beEqualTo(v).await 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sandinh/couchbase/CB.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import javax.inject._ 4 | 5 | @Singleton 6 | class CB @Inject() (val cluster: CBCluster) { 7 | lazy val bk1 = cluster.bucket("fodi") 8 | lazy val bk2 = cluster.bucket("acc") 9 | } 10 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sandinh/couchbase/CaoSpec.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import javax.inject.Inject 4 | import com.sandinh.couchbase.access.JsCao1 5 | 6 | class CaoSpec extends GuiceSpecBase { 7 | @Inject private var trophyCao: TrophyCao = null 8 | 9 | val username = "giabao" 10 | "Cao" should { 11 | "success set & get" in { 12 | trophyCao 13 | .upsert(username, Trophy.t1) 14 | .map(_.cas) must be_>(0L).await 15 | trophyCao.get(username) must beEqualTo(Trophy.t1).await 16 | } 17 | } 18 | } 19 | 20 | import javax.inject._ 21 | 22 | @Singleton 23 | class TrophyCao @Inject() (cb: CB) extends JsCao1[Trophy, String](cb.bk1) { 24 | protected def key(username: String) = "test_cao_" + username 25 | } 26 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sandinh/couchbase/CompatStringSpec.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import com.couchbase.client.core.error.DecodingFailureException 4 | import com.couchbase.client.scala.codec.JsonSerializer.StringConvert 5 | import com.couchbase.client.scala.codec._ 6 | import com.couchbase.client.scala.kv.{GetOptions, UpsertOptions} 7 | import com.typesafe.config.Config 8 | import play.api.libs.json.Json 9 | 10 | import javax.inject.Inject 11 | import scala.concurrent.Future 12 | import scala.util.Random 13 | 14 | class CompatStringSpec extends GuiceSpecBase { 15 | @Inject private var config: Config = _ 16 | private lazy val bk1Compat = { 17 | val cluster = new CBCluster(config) 18 | cluster.bucket("bk1") 19 | } 20 | 21 | val id = "test_CompatStringSpec" 22 | val s = "ab!?-'yf89da3\"\"2$^$\"" 23 | 24 | def set(idSuffix: Long, t: Transcoder): Future[String] = 25 | cb.bk1.upsert(id + idSuffix, s, UpsertOptions().transcoder(t)).map(_ => s) 26 | 27 | // default use JsonTranscoder 28 | def get(idSuffix: Long, t: Transcoder): Future[String] = 29 | cb.bk1 30 | .get(id + idSuffix, GetOptions().transcoder(t)) 31 | .map(_.contentAs[String].get) 32 | 33 | "String transcoders" should { 34 | "1. set using RawStringTranscoder, get using other Transcoders" in { 35 | val suffix = Random.nextLong() 36 | set(suffix, RawStringTranscoder.Instance) must beEqualTo(s).await 37 | get(suffix, RawStringTranscoder.Instance) must beEqualTo(s).await 38 | get(suffix, LegacyTranscoder.Instance) must beEqualTo(s).await 39 | get(suffix, RawJsonTranscoder.Instance) must beEqualTo(s).await 40 | get(suffix, JsonTranscoder.Instance) must throwA[ 41 | DecodingFailureException 42 | ].await 43 | } 44 | 45 | "2. set using LegacyTranscoder, get using other Transcoders" in { 46 | val suffix = Random.nextLong() 47 | set(suffix, LegacyTranscoder.Instance) must beEqualTo(s).await 48 | get(suffix, RawStringTranscoder.Instance) must beEqualTo(s).await 49 | get(suffix, LegacyTranscoder.Instance) must beEqualTo(s).await 50 | get(suffix, RawJsonTranscoder.Instance) must beEqualTo(s).await 51 | get(suffix, JsonTranscoder.Instance) must throwA[ 52 | DecodingFailureException 53 | ].await 54 | } 55 | 56 | "3. set using RawJsonTranscoder, get using other Transcoders" in { 57 | val suffix = Random.nextLong() 58 | set(suffix, RawJsonTranscoder.Instance) must beEqualTo(s).await 59 | get(suffix, RawStringTranscoder.Instance) must beEqualTo(s).await 60 | get(suffix, LegacyTranscoder.Instance) must beEqualTo(s).await 61 | get(suffix, RawJsonTranscoder.Instance) must beEqualTo(s).await 62 | get(suffix, JsonTranscoder.Instance) must throwA[ 63 | DecodingFailureException 64 | ].await 65 | } 66 | 67 | "4. set using JsonTranscoder, get using other Transcoders" in { 68 | val suffix = Random.nextLong() 69 | val expected = Json.toJson(s).toString() 70 | set(suffix, JsonTranscoder.Instance) must beEqualTo(s).await 71 | get(suffix, RawStringTranscoder.Instance) must beEqualTo(expected).await 72 | get(suffix, LegacyTranscoder.Instance) must beEqualTo(expected).await 73 | get(suffix, RawJsonTranscoder.Instance) must beEqualTo(expected).await 74 | get(suffix, JsonTranscoder.Instance) must beEqualTo(s).await 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sandinh/couchbase/GuiceHelper.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import javax.inject.Inject 4 | import com.google.inject.{Guice, AbstractModule} 5 | import org.specs2.concurrent.ExecutionEnv 6 | import org.specs2.specification.core.{Env, Fragments} 7 | import scala.concurrent.duration._ 8 | import com.typesafe.config.{ConfigFactory, Config} 9 | import org.specs2.matcher.Matcher 10 | import org.specs2.mutable.Specification 11 | import scala.concurrent.Future 12 | import scala.language.implicitConversions 13 | 14 | trait GuiceSpecBase extends Specification { 15 | implicit val ee: ExecutionEnv = Env().setTimeout(5.seconds).executionEnv 16 | implicit class CustomFutureMatchable[T](m: Matcher[T]) { 17 | def await: Matcher[Future[T]] = m.awaitFor(5.seconds) 18 | } 19 | @Inject private[this] var _cb: CB = null 20 | protected def cb = _cb 21 | 22 | def setup() = Guice.createInjector(new CBModule).injectMembers(this) 23 | def teardown() = cb.cluster.disconnectSync() 24 | 25 | override def map(fs: => Fragments) = step(setup()) ^ fs ^ step(teardown()) 26 | } 27 | 28 | class CBModule extends AbstractModule { 29 | override def configure(): Unit = { 30 | bind(classOf[Config]).toInstance(ConfigFactory.load()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sandinh/couchbase/JCBC_642Spec.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import com.couchbase.client.scala.kv.UpsertOptions 4 | 5 | import javax.inject.Inject 6 | import com.typesafe.config.Config 7 | import scala.concurrent.duration._ 8 | import com.couchbase.client.scala.codec.JsonSerializer.StringConvert 9 | 10 | class JCBC_642Spec extends GuiceSpecBase { 11 | @Inject private[this] var cfg: Config = _ 12 | 13 | "CBCluster" should { 14 | "pass JCBC-642" in { 15 | cfg must !==(null) 16 | println("load config from: " + cfg.origin().description()) 17 | val cfgFile = getClass.getClassLoader.getResource("application.conf") 18 | cfg.origin().description() must contain(s"@ $cfgFile") 19 | val content = "test_value_JCBC_642" 20 | cb.bk1 21 | .upsert( 22 | "test_key_JCBC_642", 23 | content, 24 | UpsertOptions().expiry(10.seconds) 25 | ) 26 | .flatMap { _ => 27 | cb.bk2.upsert( 28 | "test_key_JCBC_642", 29 | content, 30 | UpsertOptions().expiry(10.seconds) 31 | ) 32 | } 33 | .map(_.cas) must be_>(0L).await 34 | cfg must !==(null) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sandinh/couchbase/JsCodecSpec.scala: -------------------------------------------------------------------------------- 1 | /** @author giabao 2 | * created: 2013-10-22 10:23 3 | * (c) 2011-2013 sandinh.com 4 | */ 5 | package com.sandinh.couchbase 6 | 7 | import com.couchbase.client.scala.codec.JsonSerializer.JsonObjectConvert 8 | import com.couchbase.client.scala.json.{JsonArray, JsonObject} 9 | import com.sandinh.couchbase.Implicits._ 10 | import org.specs2.matcher.MatchResult 11 | 12 | import scala.concurrent.Future 13 | import scala.util.Random 14 | 15 | class JsCodecSpec extends GuiceSpecBase { 16 | val id = "test_key" 17 | 18 | def jsGet(idSuffix: Long): MatchResult[Future[Trophy]] = 19 | cb.bk1.getJsT[Trophy](id + idSuffix) must beEqualTo(Trophy.t1).await 20 | 21 | def jsonGet(idSuffix: Long): MatchResult[Future[Trophy]] = 22 | cb.bk1 23 | .getT[JsonObject](id + idSuffix) 24 | .map(json => json.toPlayJs.as[Trophy]) must beEqualTo(Trophy.t1).await 25 | 26 | def jsSet(idSuffix: Long): MatchResult[Future[Long]] = 27 | cb.bk1.upsert(id + idSuffix, Trophy.t1).map(_.cas) must be_>(0L).await 28 | 29 | def jsonSet(idSuffix: Long): MatchResult[Future[Long]] = { 30 | import Trophy.t1 31 | val arr = JsonArray.create 32 | for (d <- t1.d) { 33 | val a2 = JsonArray.create 34 | for (i <- d) a2.add(i) 35 | arr.add(a2) 36 | } 37 | val js = JsonObject.create 38 | .put("n", t1.n) 39 | .put("d", arr) 40 | cb.bk1.upsert(id + idSuffix, js).map(_.cas) must be_>(0L).await 41 | } 42 | 43 | "JsCodec" should { 44 | "upsert using PlayEncode + play-json then get using play-json or JsonObject" in { 45 | val suffix = Random.nextLong() 46 | jsSet(suffix) 47 | jsGet(suffix) 48 | jsonGet(suffix) 49 | } 50 | 51 | "upsert using JsonObject then get using play-json or JsonObject" in { 52 | val suffix = Random.nextLong() 53 | jsonSet(suffix) 54 | jsGet(suffix) 55 | jsonGet(suffix) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sandinh/couchbase/OptimisticLocking.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import com.couchbase.client.core.error.CasMismatchException 4 | import javax.inject.Inject 5 | import scala.concurrent.duration._ 6 | import scala.concurrent.Await 7 | 8 | class OptimisticLocking extends GuiceSpecBase { 9 | @Inject private var trophyCao: TrophyCao = null 10 | 11 | "CBCluster" should { 12 | "pass optimistic-locking" in { 13 | val key = "test_optimistic_locking" 14 | trophyCao.upsert(key, Trophy.t1).map(_.cas) must be_>(0L).await 15 | val old: (Trophy, Long) = 16 | Await.result(trophyCao.getWithCAS(key), 2.second) 17 | val t2 = trophyCao.replace(key, Trophy.t2, old._2) 18 | val t3 = trophyCao.replace(key, Trophy.t3, old._2) 19 | t2.zip(t3) must throwAn[CasMismatchException].await 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/scala/com/sandinh/couchbase/Trophy.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import play.api.libs.json.{Json, OFormat} 4 | 5 | case class Trophy( 6 | /** user name */ 7 | n: String, 8 | /** [ [trophyType, firstRoundId, winTime],...] */ 9 | d: List[List[Int]] 10 | ) 11 | 12 | object Trophy { 13 | implicit val fmt: OFormat[Trophy] = Json.format[Trophy] 14 | val t1: Trophy = Trophy("giabao", List(List(1, 2, 3), List(4, 5, 6))) 15 | val t2: Trophy = Trophy("thanhpv", List(List(1, 2, 3), List(4, 5, 6))) 16 | val t3: Trophy = Trophy("truongnx", List(List(1, 2, 3), List(4, 5, 6))) 17 | } 18 | -------------------------------------------------------------------------------- /play/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | play.modules.enabled += "com.sandinh.couchbase.PlayModule" 2 | -------------------------------------------------------------------------------- /play/src/main/scala/com/sandinh/couchbase/PlayCBCluster.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import javax.inject.{Inject, Singleton} 4 | import play.api.inject.ApplicationLifecycle 5 | import com.typesafe.config.Config 6 | 7 | @Singleton 8 | class PlayCBCluster @Inject() (cfg: Config, lifecycle: ApplicationLifecycle) 9 | extends CBCluster(cfg) { 10 | lifecycle.addStopHook(disconnect _) 11 | } 12 | -------------------------------------------------------------------------------- /play/src/main/scala/com/sandinh/couchbase/PlayModule.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import play.api.{Configuration, Environment} 4 | import play.api.inject.Module 5 | 6 | class PlayModule extends Module { 7 | def bindings(environment: Environment, configuration: Configuration) = Seq( 8 | bind[CBCluster].to[PlayCBCluster] 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /play/src/test/scala/com/sandinh/couchbase/PlayCBClusterSpec.scala: -------------------------------------------------------------------------------- 1 | package com.sandinh.couchbase 2 | 3 | import org.specs2.mutable.Specification 4 | import play.api.inject.guice.GuiceApplicationBuilder 5 | 6 | class PlayCBClusterSpec extends Specification { 7 | "PlayCBCluster" >> { 8 | val app = new GuiceApplicationBuilder().build() 9 | val c = app.configuration 10 | c.get[String]("com.sandinh.couchbase.connectionString") mustNotEqual "" 11 | c.get[String]("com.sandinh.couchbase.user") === "cb" 12 | c.get[Seq[String]]("play.modules.enabled") must contain( 13 | "com.sandinh.couchbase.PlayModule" 14 | ) 15 | 16 | app.injector.instanceOf[CBCluster] must beAnInstanceOf[PlayCBCluster] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.sandinh" % "sd-devops-oss" % "6.7.0") 2 | addSbtPlugin("com.sandinh" % "sd-matrix" % "6.7.0") 3 | -------------------------------------------------------------------------------- /run-cb-test-container.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker run -d \ 4 | --ulimit nofile=40960:40960 \ 5 | --ulimit core=100000000:100000000 \ 6 | --ulimit memlock=100000000:100000000 \ 7 | --name cb \ 8 | -p 8091-8094:8091-8094 -p 11210:11210 \ 9 | -v "$(pwd)"/cb-test-prepare.sh:/cb-test-prepare.sh \ 10 | "couchbase:enterprise-$CB_VERSION" 11 | 12 | for _ in {1..5}; do 13 | docker exec cb /cb-test-prepare.sh && break || sleep 8; 14 | done 15 | --------------------------------------------------------------------------------