├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── cla ├── cla-corporate.doc ├── cla-corporate.pdf ├── cla-individual.doc └── cla-individual.pdf ├── cloudant-supported-features.md ├── code-style └── intellij-code-style.xml ├── couchdb-supported-features.md ├── examples └── src │ └── main │ ├── resources │ └── logback.xml │ └── scala │ └── com │ └── ibm │ └── couchdb │ └── examples │ └── Basic.scala ├── project └── plugins.sbt ├── scalastyle-config.xml └── src ├── main └── scala │ └── com │ └── ibm │ └── couchdb │ ├── CouchDb.scala │ ├── Lenses.scala │ ├── Model.scala │ ├── Req.scala │ ├── Res.scala │ ├── TypeMapping.scala │ ├── api │ ├── Databases.scala │ ├── Design.scala │ ├── Documents.scala │ ├── Query.scala │ ├── Server.scala │ └── builders │ │ ├── GetDocumentQueryBuilder.scala │ │ ├── GetManyDocumentsQueryBuilder.scala │ │ ├── ListQueryBuilder.scala │ │ ├── QueryOps.scala │ │ ├── QueryStrategy.scala │ │ ├── ShowQueryBuilder.scala │ │ └── ViewQueryBuilder.scala │ ├── core │ └── Client.scala │ ├── implicits │ ├── TaskImplicits.scala │ └── UpickleImplicits.scala │ └── package.scala └── test ├── resources └── logback-test.xml └── scala └── com └── ibm └── couchdb ├── BasicAuthSpec.scala ├── CouchDbSpec.scala ├── api ├── DatabasesSpec.scala ├── DesignSpec.scala ├── DocumentsSpec.scala ├── QueryListSpec.scala ├── QueryShowSpec.scala ├── QueryTemporaryViewSpec.scala ├── QueryViewSpec.scala └── ServerSpec.scala ├── implicits ├── TaskImplicitsSpec.scala └── UpickleImplicitsSpec.scala └── spec ├── CouchDbSpecification.scala ├── Fixtures.scala └── SpecConfig.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | project/target 4 | project/project/target 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.8 4 | jdk: 5 | - oraclejdk8 6 | services: 7 | - couchdb 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CouchDB-Scala 2 | 3 | [![Build Status](https://travis-ci.org/beloglazov/couchdb-scala.svg?branch=master)](https://travis-ci.org/beloglazov/couchdb-scala) 4 | [![Join the chat at https://gitter.im/beloglazov/couchdb-scala](https://badges.gitter.im/beloglazov/couchdb-scala.svg)](https://gitter.im/beloglazov/couchdb-scala?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.ibm/couchdb-scala_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.ibm/couchdb-scala_2.11) 6 | [![Stories in Ready](https://badge.waffle.io/beloglazov/couchdb-scala.png?label=ready&title=Ready)](https://waffle.io/beloglazov/couchdb-scala) 7 | 8 | 9 | This is a purely functional Scala client for 10 | [CouchDB](http://couchdb.apache.org/). The design goals are compositionality, 11 | expressiveness, type-safety, and ease of use. 12 | 13 | It's based on these awesome libraries: 14 | [Scalaz](https://github.com/scalaz/scalaz), 15 | [Http4s](https://github.com/http4s/http4s), 16 | [uPickle](https://github.com/lihaoyi/upickle-pprint), and 17 | [Monocle](https://github.com/julien-truffaut/Monocle). 18 | 19 | 20 | ## Getting started 21 | 22 | Add the following dependency to your SBT config: 23 | 24 | ```Scala 25 | libraryDependencies += "com.ibm" %% "couchdb-scala" % "0.7.2" 26 | ``` 27 | 28 | 29 | ## Tutorial 30 | 31 | This Scala client tries to stay as close to the native CouchDB API as possible, 32 | while adding type-safety and automatic serialization/deserialization of Scala 33 | objects to and from JSON using uPickle. The best way to get up to speed with the 34 | client is to first obtain a good understanding of how CouchDB works and its 35 | native API. Some good resources to learn CouchDB are: 36 | 37 | - [CouchDB: The Definitive Guide](http://guide.couchdb.org/) 38 | - [CouchDB Documentation](http://docs.couchdb.org/en/) 39 | 40 | To get started, add the following import to your Scala file (or just start the 41 | SBT console with `sbt console`, which automatically adds the required imports): 42 | 43 | ```Scala 44 | import com.ibm.couchdb._ 45 | ``` 46 | 47 | Then, you need to create a client instance by passing in the IP address or host 48 | name of the CouchDB server, port number, and optionally the scheme (which 49 | defaults to `http`): 50 | 51 | ```Scala 52 | val couch = CouchDb("127.0.0.1", 5984) 53 | ``` 54 | 55 | Through this object, you get access to the following CouchDB API sections: 56 | 57 | - Server: server-level operations 58 | - Databases: operations on databases 59 | - Design: operations for creating and managing design documents 60 | - Documents: creating, modifying, and querying documents and attachments 61 | - Query: querying views, shows, and lists 62 | 63 | 64 | ### Server API 65 | 66 | The server API section provides only 3 operations: getting the server info, 67 | which is equivalent to making a GET request to the `/` resource of the CouchDB 68 | server; generating a UUID; and generating a sequence of UUIDs. For example, to 69 | make a server info request using the client instance created above: 70 | 71 | ```Scala 72 | couch.server.info.run 73 | ``` 74 | 75 | The `couch.server` property refers to an instance of the `Server` class, which 76 | represents the server API section. Then, calling the `info` method generates a 77 | [scalaz.concurrent.Task](https://github.com/scalaz/scalaz/blob/scalaz-seven/concurrent/src/main/scala/scalaz/concurrent/Task.scala), 78 | which describes an action of making a GET request to the server. At this point, 79 | an actual request is not yet made. Instead, `Task` encapsulates a description of 80 | a computation, which can be executed later. This allows us to control 81 | side-effects and keep the functions pure. Tim Perrett has written a very nice 82 | [blog 83 | post](http://timperrett.com/2014/07/20/scalaz-task-the-missing-documentation/) 84 | with more background and documentation on Scalaz's `Task`. 85 | 86 | The return type of `couch.server.info` is `Task[Res.ServerInfo]`, which means 87 | that when this task is executed, it may return a `ServerInfo` object or fail. To 88 | execute a `Task`, we need to call the `run` method, which triggers the actual 89 | GET request to server, whose response is then automatically parsed and mapped 90 | onto the `ServerInfo` case class that contains a few fields describing the 91 | server instance like the CouchDB version, etc. Ideally, instead of executing a 92 | `Task` and causing side-effects in the middle of a program, we should delay the 93 | execution as much as possible to keep the core application logic pure. Rather 94 | then executing `Task`s to obtain the query result, we can perform action on the 95 | query results and compose `Task`s in a functional way using higher-order 96 | functions like `map` and `flatMap`, or for-comprehensions. We will see more 97 | examples of this later. In further code snippets, I will omit calls to the `run` 98 | method assuming that the point where effectful computations are executed is 99 | externalized. 100 | 101 | The other operations of the server API can be performed in a similar way. To 102 | generate a UUID, you just need to call `couch.server.mkUuid`, which returns 103 | `Task[String]`. To generate `n` UUIDs, call `couch.server.mkUuids(n)`, which 104 | returns `Task[Seq[String]]` representing a task of generating a sequence of `n` 105 | UUIDs. For more usage examples, please refer to 106 | [ServerSpec](src/test/scala/com/ibm/couchdb/api/ServerSpec.scala). 107 | 108 | 109 | ### Databases API 110 | 111 | The databases API implements more useful functionality like creating, deleting, 112 | and getting information about databases. To create a database: 113 | 114 | ```Scala 115 | couch.dbs.create("awesome-database") 116 | ``` 117 | 118 | The `couch.dbs` property refers to an instance of the `Databases` class, which 119 | represents the databases API section. A call to the `create` method returns a 120 | `Task[Res.Ok]`, which represents a request returning an instance of the `Res.Ok` 121 | case class if it succeeds, or a failure object if it fails. Failure handling is 122 | done using methods on `Task`, part of which are covered in Tim Perrett's [blog 123 | post](http://timperrett.com/2014/07/20/scalaz-task-the-missing-documentation/). 124 | In two words the actual result of a `Task` execution is `Throwable \/ A`, which 125 | is 126 | [either](https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Either.scala) 127 | an exception or the desired type `A`. In the case or `dbs.create`, the desired 128 | result is of type `Res.Ok`, which is a case class representing a response from 129 | the server in case of a succeeded request. 130 | 131 | Other methods provided by the databases API are `dbs.delete("awesome-database")` 132 | to delete a database, `dbs.get("awesome-database")` to get information about a 133 | database returned as an instance of `DbInfo` case class that includes such 134 | fields as data size, number of documents in the database, etc. For some examples 135 | of using the databases API, please refer to 136 | [DatabasesSpec](src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala). 137 | 138 | 139 | ### Design API 140 | 141 | While the API sections described earlier operate at the level above databases, 142 | the Design, Documents, and Query APIs are applied within the context of a single 143 | database. Therefore, to obtain instances of these interfaces, the context needs 144 | to be specialized by specifying the name of a database of interest: 145 | 146 | ```Scala 147 | val db = couch.db("awesome-database", TypeMapping.empty) 148 | ``` 149 | 150 | This method call returns an instance of the `CouchDbApi` case class representing 151 | the context of a single database, through which we can get access to the Design, 152 | Documents, and Query APIs. The `db` method takes 2 arguments: the database name 153 | and an instance of `TypeMapping`. We will discuss `TypeMapping` later, for now 154 | we can just pass an empty mapping using `TypeMapping.empty`. Through 155 | `CouchDbApi` we can obtain an instance of the `Design` class representing the 156 | Design API section for our database: 157 | 158 | ```Scala 159 | db.design 160 | ``` 161 | 162 | The Design API allows us to create, retrieve, update, delete, and manage 163 | attachments to design documents stored in the current database (you can get the 164 | name of the database from an instance of `CouchDbApi` using `db.name`). 165 | 166 | Let's take a look at an example of a design document with a single view. First, 167 | assume we have a collection of people each corresponding to an object of a case 168 | class `Person` with a name and age fields: 169 | 170 | ```Scala 171 | case class Person(name: String, age: Int) 172 | ``` 173 | 174 | Let's define a view with just a map function that emits person names as keys and 175 | ages as values. To do that, we are going to use a `CouchView` case class: 176 | 177 | ```Scala 178 | val ageView = CouchView(map = 179 | """ 180 | |function(doc) { 181 | | emit(doc.doc.name, doc.doc.age); 182 | |} 183 | """.stripMargin) 184 | ``` 185 | 186 | Basically, we define our map function in plain JavaScript and assign it to the 187 | `map` field of a `CouchView` object. This function maps each document to a pair 188 | of the person's name as the key and age as the value. Notice, that we need to 189 | use `doc.doc` to get to the fields of the person object for reasons that will 190 | become clear later. 191 | To define a view that contains a reduce operation, specify the relevant Javascript 192 | function to the `reduce` attribute of the `CouchView` case class constructor like so: 193 | 194 | ```Scala 195 | val totalAgeView = CouchView(map = 196 | """ 197 | |function(doc) { 198 | | emit(doc._id, doc.doc.age); 199 | |} 200 | """.stripMargin, 201 | reduce = 202 | """ 203 | |function(key, values, rereduce) { 204 | | return sum(values); 205 | |} 206 | """.stripMargin) 207 | ``` 208 | 209 | We can now create an instance of our design document using 210 | the defined `ageView` and `totalAgeView`: 211 | 212 | ```Scala 213 | val designDoc = CouchDesign( 214 | name = "test-design", 215 | views = Map("age-view" -> ageView, "total-age-view" -> totalAgeView)) 216 | ``` 217 | 218 | `CouchDesign` supports other fields like `shows` and `lists`, but for this 219 | simple example we only specify the design `name` and `views` as a `Map` from 220 | view names to `CouchView` objects. Proper management of complex design documents 221 | is a separate topic (e.g., JavaScript functions can be stored in separate `.js` 222 | files and loaded dynamically). We can finally proceed to submitting the defined 223 | design document to our database: 224 | 225 | ```Scala 226 | db.design.create(designDoc) 227 | ``` 228 | 229 | This method call returns an object of type `Task[Res.DocOk]`. The `DocOk` case 230 | class represents a response from the server to a succeeded request involving 231 | creating, modifying, and deleting documents. Compared with `Res.Ok`, it includes 232 | 2 extra fields: `id` (the ID of the created/updated/deleted document) and `rev` 233 | (the revision of the created/updated/deleted document). In the case of design 234 | documents, the ID is composed of the design name prefixed with `_design/`. In 235 | other words, `designDoc` will get the `_design/test-design` ID. Each revision is 236 | a unique 32-character UUID string. We can now retrieve the design document from 237 | the database by name or by ID: 238 | 239 | ```Scala 240 | db.design.get("test-design") 241 | db.design.getById("_design/test-design") 242 | ``` 243 | 244 | Once the returned `Task`s are executed, each of these calls returns an instance 245 | of `CouchDesign` corresponding to our design document with some extra fields, 246 | e.g., `_id`, `_rev`, `_attachments`, etc. To update a design document, we must 247 | first retrieve it from the database to know the current revision and avoid 248 | [conflicts](http://guide.couchdb.org/draft/conflicts.html), make changes to the 249 | content, and submit the updated version. Let's say we want to add another view, 250 | which emits ages as keys and names as values assigned to a `nameView` variable, 251 | then our updated view `Map` is: 252 | 253 | ```Scala 254 | val updatedViews = Map( 255 | "age-view" -> ageView, 256 | "name-view" -> nameView) 257 | ``` 258 | 259 | We can now submit the changes to the database as follows: 260 | 261 | ```Scala 262 | for { 263 | initial <- db.design.get("test-design") 264 | docOk <- db.design.update(initial.copy(views = updatedViews)) 265 | } yield docOk 266 | ``` 267 | 268 | Here, we use a for-comprehension to chain 2 monadic actions. If both actions 269 | succeed, we get a `Res.DocOk` object as a result containing the new revision of 270 | the design document stored in the `_rev` field. The Design API supports a few 271 | other operations, to see their usage examples please refer to 272 | [DesignSpec](src/test/scala/com/ibm/couchdb/api/DesignSpec.scala). 273 | 274 | 275 | ### Documents API 276 | 277 | The Documents API implements operations for creating, querying, modifying, and 278 | deleting documents and their attachments. At this stage, it's time to discuss 279 | how Scala objects are represented in CouchDB and what `TypeMapping` is used for. 280 | One of the design goals of `CouchDB-Scala` is to make it as easy as possible to 281 | store and retrieve documents by automating the process of serialization and 282 | deserialization to and from JSON. This functionality is based on 283 | [uPickle](https://github.com/lihaoyi/upickle-pprint), which uses macros to 284 | automatically generate readers and writers for case classes. However, it also 285 | allows implementing custom readers and writers for your domain classes if they 286 | are not *case classes*. For example, these can be 287 | [Thrift](https://thrift.apache.org/) / 288 | [Scrooge](https://github.com/twitter/scrooge) generated entities or your custom 289 | classes. 290 | 291 | CouchDB automatically adds several fields to every document containing metadata 292 | about the document, such as `_id`, `_rev`, `_attachments`, `_conflicts`, etc. To 293 | take advantage of uPickle's support for case classes, a decision was made to 294 | have a case class called `CouchDoc[D]` that has all the metadata fields 295 | generated by CouchDB and also includes 2 special fields: `doc` for storing an 296 | instance of your domain class `D`, and `kind` for storing a string 297 | representation of the document type that can be used for filtering in views, 298 | shows, and lists (we use `kind` instead of `type` here, as `type` is a reserved 299 | keyword in Scala). In other words, if your domain model is represented by a set 300 | of case classes, the serialization and deserialization will be handled 301 | completely transparently for you. `TypeMapping` is used for defining a mapping 302 | from you domain model classes to a string representation of the corresponding 303 | document type. Continuing the previous example with the `Person` case class, we 304 | can define a `TypeMapping`, for example, as follows: 305 | 306 | ```Scala 307 | val typeMapping = TypeMapping(classOf[Person] -> "Person") 308 | ``` 309 | 310 | Here, we are specifying a mapping from the class name `Person` to a document 311 | kind as a string. The `TypeMapping` factory maps classes to their canonical 312 | names to preserve uniqueness. Whenever a document is submitted to the database, 313 | the `kind` field is automatically populated based on the specified mapping. If 314 | the type mapping is not specified (as we did above by using 315 | `TypeMapping.empty`), the `kind` field is ignored. We can now provide the newly 316 | defined `TypeMapping` to create a fully specified database context: 317 | 318 | ```Scala 319 | val db = couch.db("awesome-database", typeMapping) 320 | ``` 321 | 322 | Similarly to the other API sections, we can use the database context to get an 323 | instance of the `Documents` class representing the Documents API section: 324 | 325 | ```Scala 326 | db.docs 327 | ``` 328 | 329 | Let's define some data: 330 | 331 | ```Scala 332 | val alice = Person("Alice", 25) 333 | val bob = Person("Bob", 30) 334 | val carl = Person("Carl", 20) 335 | ``` 336 | 337 | We can now store these objects in the database as follows: 338 | 339 | ```Scala 340 | db.docs.create(alice) 341 | ``` 342 | 343 | This method assigns a UUID generated with `server.mkUuid` that we've seen above 344 | to the document being stored. Another option is to specify our own document ID 345 | if it's known to be unique: 346 | 347 | ```Scala 348 | db.docs.create(bob, "bob") 349 | ``` 350 | 351 | As another alternative, we can create multiple documents with auto-generated 352 | UUIDs at once using a batch request: 353 | 354 | ```Scala 355 | db.docs.createMany(Seq(alice, bob, carl)) 356 | ``` 357 | 358 | We can retrieve a document from the database by ID: 359 | 360 | ```Scala 361 | db.docs.get[Person]("bob") 362 | ``` 363 | 364 | Here, we have to be explicit about the expected object type to allow uPickle to 365 | do its magic, that's why we specify the type parameter to the `get` method. This 366 | method returns `Task[CouchDoc[Person]]`, which basically means that we are 367 | getting back a task that after executing successfully will give us an instance 368 | of `CouchDoc[Person]`. This object will contain an instance of `Person` in the 369 | `doc` field equivalent to the original `Person("Bob", 30)`. 370 | 371 | You can also retrieve a set of documents by IDs using: 372 | 373 | ```Scala 374 | db.docs.getMany.includeDocs[Person].withIds(Seq("id1", "id1")).build.query 375 | ``` 376 | 377 | A call to `getMany` returns an instance of `GetManyDocumentsQueryBuilder`, which 378 | is a class allowing you to build a query in a type-safe way. Under the hood, it 379 | makes a request to the 380 | [/{db}/_all_docs](http://docs.couchdb.org/en/1.6.1/api/database/bulk-api.html#get--db-_all_docs) 381 | endpoint. As you can see from the linked documentation on this endpoint, it has 382 | many optional parameters. The `GetManyDocumentsQueryBuilder` class provides a 383 | fluent interface for constructing queries to this endpoint. For example, to 384 | limit the number of documents to the maximum of 10 and return them in the 385 | descending order: 386 | 387 | ```Scala 388 | db.docs.getMany.limit(10).descending.includeDocs[Person].withIds(Seq("id1", "id2")).build.query 389 | ``` 390 | 391 | This creates an instance of `Task[CouchDocs[String, CouchDocRev, Person]]`, 392 | which looks complicated but just represents a task that returns basically a 393 | sequence of documents. The `queryIncludeDocs` method serves as a way to complete 394 | the query construction process, which also sets the `include_docs` option to 395 | include the full content of the documents mapped to `Person` objects on arrival. 396 | 397 | It's also possible to execute a query without including the document content 398 | using `db.docs.getMany.build.query`, which is equivalent to keeping the `include_docs` 399 | set to its default `false` value. This query will only return metadata on the 400 | matching documents. In this case, we don't need to specify the type parameter as 401 | no mapping is required since the document content is not retrieved. 402 | 403 | To retrieve all documents in the database of a given type without specifying ids, you could use 404 | one of the following approaches: 405 | ```Scala 406 | val allPeople1 = db.docs.getMany.byTypeUsingTemporaryView[Person].build.query 407 | ``` 408 | 409 | The first approach, `byTypeUsingTemporaryView[T]`, uses a temporary view 410 | under the hood for type based filtering. While convenient for development purposes, it is inefficient 411 | and should not be used in production. 412 | 413 | For efficiency you should instead use `byType[K, V](view_name)`, or the simpler 414 | `byType[V](view_name)`, which require that you first create 415 | a type filtering permanent view, and then pass its name as argument to one of these methods. 416 | Because a permanent view is used, these approaches are more efficient and are thus the recommended 417 | approach for type based document filtering. 418 | 419 | Note in `byType[K, V](view_name)` the parameters `K` and `V` represent the key and value types 420 | of the permanent view. The document's `kind` attribute must be the first key of such a 421 | view, as in the type filter view function example shown below. 422 | 423 | ```javascript 424 | function(doc) { 425 | emit([doc.kind, doc._id], doc._id); 426 | } 427 | ``` 428 | 429 | The above function could then be used as follows: 430 | ```scala 431 | val allPeople2 = db.docs.getMany.byType[(String, String), String](your_view_name).build.query 432 | ``` 433 | 434 | In the simpler `byType[V](view_name)`, `K` is implicitly assumed to be of type Tuple of two strings 435 | `(String, String)`. Note the document's `kind` attribute must be the first key of such a 436 | view, as in the type filter view function example defined above. 437 | 438 | The above function could then be used as follows: 439 | ```scala 440 | val allPeople3 = db.docs.getMany.byType[String](your_view_name).build.query 441 | ``` 442 | 443 | There is a similar query builder for retrieving single documents 444 | `GetDocumentQueryBuilder` that makes GET requests to the 445 | [/{db}/{docid}](http://docs.couchdb.org/en/1.6.1/api/document/common.html#get--db-docid) 446 | endpoint. This query builder can accessed through `db.docs.get`. 447 | 448 | There are other operations provided by the Documents API, such as updating 449 | documents, deleting documents, adding attachments, retrieving attachments, etc. 450 | For more usage examples, please refer to 451 | [DocumentsSpec](src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala). 452 | 453 | 454 | ### Query API 455 | 456 | The Query API provides an interface for querying views, shows, and lists. Let's 457 | say we want to query our `age-view` defined earlier. To do that, we first 458 | obtain an instance of `ViewQueryBuilder` as follows: 459 | 460 | ```Scala 461 | val ageView = db.query.view[String, Int]("test-design", "age-view").get 462 | val totalAgeView = db.query.view[String, Int]("test-design", "total-age-view").get 463 | ``` 464 | 465 | We need to specify 2 type parameters to the `view` method representing the types 466 | of the key and value emitted by the view. In the case of `age-view` and `total-age-view`, it's 467 | `String` for the key (person name) and `Int` for the value (person age). 468 | 469 | We can now use the `ageView` query builder to retrieve all the documents from the view: 470 | 471 | ```Scala 472 | ageView.build.query 473 | ``` 474 | 475 | This method call returns an instance of `Task[CouchKeyVals[String, Int]]`. 476 | Since we haven't specified the `include_docs` option, this query only retrieves 477 | a sequence of document IDs, keys, and values emitted by the view's map function. 478 | This method makes a call to the 479 | [/{db}/_design/{ddoc}/_view/{view}](http://docs.couchdb.org/en/1.6.1/api/ddoc/views.html#get--db-_design-ddoc-_view-view) 480 | endpoint, and the builder supports all the relevant options. 481 | 482 | Similarly, to query the total age of Persons in the document using the 483 | `totalAgeView` builder we can do: 484 | 485 | ```Scala 486 | totalAgeView.reduce[Int].build.query 487 | ``` 488 | 489 | The type parameter `T` specified to `queryWithReduce[T]`, in this case `Int`, 490 | is the expected return type of the view's `reduce` function. 491 | 492 | We can also make more complex queries. Let's say we want to get 10 people 493 | starting from the name Bob and include the document content: 494 | 495 | ```Scala 496 | ageView.startKey("Bob").limit(10).includeDocs[Person].build.query 497 | ``` 498 | 499 | This returns an instance of `Task[CouchDocs[String, Int, Person]]`, which once 500 | executed results in a sequence of objects encapsulating the metadata about the 501 | documents (`id`, `key`, `value`, `offset`, `total_rows`) and the corresponding 502 | `Person` objects. Please follow the definitions of case classes in 503 | [Model](src/main/scala/com/ibm/couchdb/Model.scala) 504 | to fully understand the structure of the returned objects. 505 | 506 | It's also possible to only get the documents from a view that match the 507 | specified keys. For example, we can use that to get only documents of Alice and 508 | Carl: 509 | 510 | ```Scala 511 | ageView.withIds(Seq("Alice", "Carl")).build.query 512 | ``` 513 | 514 | This return an instance of `Task[CouchKeyVals[String, Int]]`. For other usage 515 | examples of the view Query API, please refer to 516 | [QueryViewSpec](src/test/scala/com/ibm/couchdb/api/QueryViewSpec.scala). 517 | 518 | The APIs for querying shows and lists are structured similarly to view querying 519 | and follow the official CouchDB specification. Please refer to 520 | [QueryShowSpec](src/test/scala/com/ibm/couchdb/api/QueryShowSpec.scala) 521 | and 522 | [QueryListSpec](src/test/scala/com/ibm/couchdb/api/QueryListSpec.scala) 523 | for more details and examples. 524 | 525 | 526 | ### Authentication 527 | 528 | At the moment, the client supports only the [basic 529 | authentication](http://docs.couchdb.org/en/1.6.1/api/server/authn.html#basic-authentication) 530 | method. To use it, just pass your username and password to the `CouchDb` 531 | factory: 532 | 533 | ```Scala 534 | val couch = CouchDb("127.0.0.1", 6984, https = true, "username", "password") 535 | ``` 536 | 537 | Please note that [enabling 538 | HTTPS](http://docs.couchdb.org/en/1.6.1/config/http.html#config-ssl) is 539 | recommended to avoid sending your credentials in plain text. The default CouchDB 540 | HTTPS port is 6984. 541 | 542 | 543 | ### Complete example 544 | 545 | Here is a basic example of an application that stores a set of case class 546 | instances in a database, retrieves them back, and prints out afterwards: 547 | 548 | ```Scala 549 | object Basic extends App { 550 | 551 | // Define a simple case class to represent our data model 552 | case class Person(name: String, age: Int) 553 | 554 | // Define a type mapping used to transform class names into the doc kind 555 | val typeMapping = TypeMapping(classOf[Person] -> "Person") 556 | 557 | // Define some sample data 558 | val alice = Person("Alice", 25) 559 | val bob = Person("Bob", 30) 560 | val carl = Person("Carl", 20) 561 | 562 | // Create a CouchDB client instance 563 | val couch = CouchDb("127.0.0.1", 5984) 564 | // Define a database name 565 | val dbName = "couchdb-scala-basic-example" 566 | // Get an instance of the DB API by name and type mapping 567 | val db = couch.db(dbName, typeMapping) 568 | 569 | val actions = for { 570 | // Delete the database or ignore the error if it doesn't exist 571 | _ <- couch.dbs.delete(dbName).ignoreError 572 | // Create a new database 573 | _ <- couch.dbs.create(dbName) 574 | // Insert documents into the database 575 | _ <- db.docs.createMany(Seq(alice, bob, carl)) 576 | // Retrieve all documents from the database and unserialize to Person 577 | docs <- db.docs.getMany.includeDocs[Person].build.query 578 | } yield docs.getDocsData 579 | 580 | // Execute the actions and process the result 581 | actions.attemptRun match { 582 | // In case of an error (left side of Either), print it 583 | case -\/(e) => println(e) 584 | // In case of a success (right side of Either), print each object 585 | case \/-(a) => a.map(println(_)) 586 | } 587 | 588 | } 589 | ``` 590 | 591 | You can run this example from the project directory using `sbt`: 592 | 593 | ```Bash 594 | sbt "run-main com.ibm.couchdb.examples.Basic" 595 | ``` 596 | 597 | 598 | ## Mailing list 599 | 600 | Please feel free to join our mailing list, we welcome all questions and 601 | suggestions: https://groups.google.com/forum/#!forum/couchdb-scala 602 | 603 | 604 | ## Contributing 605 | 606 | We welcome contributions, but request you follow these guidelines. Please raise 607 | any bug reports on the project's [issue 608 | tracker](https://github.com/beloglazov/couchdb-scala/issues). 609 | 610 | In order for us to accept pull-requests, the contributor must first complete a 611 | Contributor License Agreement (CLA). This clarifies the intellectual property 612 | license granted with any contribution. It is for your protection as a 613 | Contributor as well as the protection of IBM and its customers; it does not 614 | change your rights to use your own Contributions for any other purpose. 615 | 616 | You can download the CLAs here: 617 | 618 | - [individual](cla/cla-individual.pdf) 619 | - [corporate](cla/cla-corporate.pdf) 620 | 621 | If you are an IBMer, please contact us directly as the contribution process is 622 | slightly different. 623 | 624 | 625 | ## Contributors 626 | 627 | - [Anton Beloglazov](http://beloglazov.info/) ([@beloglazov](https://github.com/beloglazov)) 628 | - Ermyas Abebe ([@ermyas](https://github.com/ermyas)) 629 | 630 | 631 | ## Copyright and license 632 | 633 | © Copyright 2015 IBM Corporation, Google Inc. Distributed under the [Apache 2.0 634 | license](LICENSE). 635 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import xerial.sbt.Sonatype.SonatypeKeys._ 2 | 3 | sonatypeSettings 4 | 5 | profileName := "com.ibm.couchdb-scala" 6 | 7 | organization := "com.ibm" 8 | 9 | name := "couchdb-scala" 10 | 11 | version := "0.8.0-SNAPSHOT" 12 | 13 | scalaVersion := "2.11.8" 14 | 15 | description := "A purely functional Scala client for CouchDB" 16 | 17 | homepage := Some(url("https://github.com/beloglazov/couchdb-scala")) 18 | 19 | licenses := Seq("The Apache Software License, Version 2.0" 20 | -> url("http://www.apache.org/licenses/LICENSE-2.0.txt")) 21 | 22 | libraryDependencies ++= Seq( 23 | "org.scalaz" %% "scalaz-core" % "7.2.6", 24 | "org.scalaz" %% "scalaz-effect" % "7.2.6", 25 | "org.http4s" %% "http4s-core" % "0.14.6a", 26 | "org.http4s" %% "http4s-client" % "0.14.6a", 27 | "org.http4s" %% "http4s-blaze-client" % "0.14.6a", 28 | "com.lihaoyi" %% "upickle" % "0.4.1", 29 | "com.github.julien-truffaut" %% "monocle-core" % "1.2.2", 30 | "com.github.julien-truffaut" %% "monocle-macro" % "1.2.2", 31 | "org.log4s" %% "log4s" % "1.3.0", 32 | "ch.qos.logback" % "logback-classic" % "1.1.7", 33 | "org.specs2" %% "specs2" % "3.7" % "test", 34 | "org.typelevel" %% "scalaz-specs2" % "0.3.0" % "test", 35 | "org.scalacheck" %% "scalacheck" % "1.13.0" % "test", 36 | "org.scalaz" %% "scalaz-scalacheck-binding" % "7.2.1" % "test" 37 | ) 38 | 39 | scalacOptions ++= Seq( 40 | "-deprecation", 41 | "-encoding", "UTF-8", 42 | "-feature", 43 | "-language:existentials", 44 | "-language:higherKinds", 45 | "-language:implicitConversions", 46 | "-language:postfixOps", 47 | "-unchecked", 48 | "-Xfatal-warnings", 49 | "-Xlint", 50 | "-Yno-adapted-args", 51 | "-Ywarn-numeric-widen", 52 | "-Ywarn-value-discard", 53 | "-Xfuture", 54 | "-Ywarn-unused-import" 55 | ) 56 | 57 | scalacOptions in (Compile, console) ~= (_ filterNot ( 58 | List("-Ywarn-unused-import", "-Xfatal-warnings").contains(_))) 59 | 60 | wartremover.wartremoverSettings 61 | 62 | wartremover.wartremoverErrors in (Compile, compile) ++= Seq( 63 | wartremover.Wart.Any, 64 | wartremover.Wart.Any2StringAdd, 65 | wartremover.Wart.EitherProjectionPartial, 66 | wartremover.Wart.OptionPartial, 67 | wartremover.Wart.Product, 68 | wartremover.Wart.Serializable, 69 | wartremover.Wart.ListOps 70 | ) 71 | 72 | lazy val compileScalastyle = taskKey[Unit]("compileScalastyle") 73 | 74 | compileScalastyle := org.scalastyle.sbt.ScalastylePlugin.scalastyle.in(Compile).toTask("").value 75 | 76 | (compile in Compile) <<= (compile in Compile) dependsOn compileScalastyle 77 | 78 | lazy val testScalastyle = taskKey[Unit]("testScalastyle") 79 | 80 | testScalastyle := org.scalastyle.sbt.ScalastylePlugin.scalastyle.in(Test).toTask("").value 81 | 82 | (test in Test) <<= (test in Test) dependsOn testScalastyle 83 | 84 | testFrameworks := Seq(TestFrameworks.Specs2, TestFrameworks.ScalaCheck) 85 | 86 | parallelExecution in Test := false 87 | 88 | unmanagedSourceDirectories in Compile += baseDirectory.value / "examples" / "src" / "main" / "scala" 89 | 90 | initialCommands in console := "import scalaz._, Scalaz._, com.ibm.couchdb._" 91 | 92 | initialCommands in console in Test := "import scalaz._, Scalaz._, scalacheck.ScalazProperties._, " + 93 | "scalacheck.ScalazArbitrary._,scalacheck.ScalaCheckBinding._" 94 | 95 | logBuffered := false 96 | 97 | publishMavenStyle := true 98 | 99 | publishArtifact in Test := false 100 | 101 | pomExtra := { 102 | 103 | scm:git:git@github.com:beloglazov/couchdb-scala.git 104 | scm:git:git@github.com:beloglazov/couchdb-scala.git 105 | https://github.com/beloglazov/couchdb-scala 106 | 107 | 108 | 109 | beloglazov 110 | Anton Beloglazov 111 | anton.beloglazov@gmail.com 112 | http://beloglazov.info 113 | 114 | 115 | } 116 | -------------------------------------------------------------------------------- /cla/cla-corporate.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beloglazov/couchdb-scala/b97f173c5f2803d18d0536f39e8063e4abe28f6b/cla/cla-corporate.doc -------------------------------------------------------------------------------- /cla/cla-corporate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beloglazov/couchdb-scala/b97f173c5f2803d18d0536f39e8063e4abe28f6b/cla/cla-corporate.pdf -------------------------------------------------------------------------------- /cla/cla-individual.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beloglazov/couchdb-scala/b97f173c5f2803d18d0536f39e8063e4abe28f6b/cla/cla-individual.doc -------------------------------------------------------------------------------- /cla/cla-individual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beloglazov/couchdb-scala/b97f173c5f2803d18d0536f39e8063e4abe28f6b/cla/cla-individual.pdf -------------------------------------------------------------------------------- /cloudant-supported-features.md: -------------------------------------------------------------------------------- 1 | IBM Cloudant API V2 Support 2 | 3 | Endpoints are prefixed with `/{username}.cloudant.com/` 4 | 5 | | Cloudant feature | HTTP API | Support | Since | Example | 6 | |---|---|:---:|:---:|:----:| 7 | |**Databases**||||| 8 | | Create a new database |`PUT /{db}` | | | | 9 | | Get information about a database | `GET /{db}` | | | | 10 | | Delete a specified database |`DELETE /{db}` | | | | 11 | | List databases | `GET /_all_dbs ` | | | | 12 | | Get all documents in a database |`GET /{db}/_all_docs` | | | | 13 | | Create a new index | `POST /{db}/_index` | | | | 14 | | Delete an index | `DELETE /{db}/_index/{design_doc}/{type}/{name}` | | | | 15 | |**Documents**||||| 16 | | Create document | `POST /{db}/` | | | | 17 | | Retrieve document | `GET /{db}/{doc_id}` | | | | 18 | | Update document | `PUT /{db}/{doc_id}` | | | | 19 | | Query database | `GET /{db}/_all_docs` | | | | 20 | | Delete document | `DELETE /{db}/{doc_id}?rev={rev}` | | | | 21 | | Create and update multiple documents | `POST /{db}/_bulk_docs` | | | | 22 | | Create attachment | `PUT /{db}/{doc_id}/{att_name}?rev={rev}` | | | | 23 | | Retrieve attachment | `GET /{db}/{doc_id}/{att_name}` | | | | 24 | | Delete attachment | `DELETE /{db}/{doc_id}/{attachment_name}?rev={rev}` | | | | 25 | | Find document using an index | `POST /{db}/_find` | | | | 26 | | Get documents given multiple keys | `POST /{db}/_design/{design_doc}/_view` | | | | 27 | | Get list of changes to documents | `GET /{db}/_changes` | | | | 28 | |**Design Documents**||||| 29 | | Create design document | `PUT /{db}/_design/design-doc` | | | | 30 | | Update design document | `PUT /{db}/_design/design-doc`  | | | | 31 | | Get design document | `GET /{db}/_design/{des_doc}` | | | | 32 | | Get meta-data about design document | `GET /{db}/_design/{des_doc}/_info` | | | | 33 | | Copy design document | `COPY /{db}/_design/{des_doc}?rev={rev}` | | | | 34 | | Delete design document | `DELETE /{db}/_design/{des_doc}?rev={rev}` | | | | 35 | | Get List Functions | `GET /{db}/{design_id}/_list/{list_function}/{map_reduce_index}` | | | | 36 | | Get Show Functions | `GET /{db}/{design_id}/_show/{show_function}/{document_id}` | | | | 37 | | Query Update Handlers | `POST /{db}/{design_id}/_update/{update_handler}` | | | | 38 | |**Views**||||| 39 | | Add view to design document | `PUT /{db}/_design/` | | | | 40 | | Query a view| `GET /{db}/_design/{design_id}/_view` | | | | 41 | | Query a view using a list of keys| `POST /{db}/_design/{design_id}/_view/{view_name}` | | | | 42 | |**Replication**||||| 43 | | | | | | | 44 | |**Server**||||| 45 | | List active tasks | `GET /_active_tasks` | | | | 46 | | List database events | `GET /_db_updates` | | | | 47 | | Request a Universally Unique Identifier | `GET /_uuid` | | | | 48 | -------------------------------------------------------------------------------- /code-style/intellij-code-style.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /couchdb-supported-features.md: -------------------------------------------------------------------------------- 1 | Apache Couch Db version **1.6** 2 | 3 | | CouchDb feature | HTTP API | Support | Since | Example | 4 | |---|---|:---:|:---:|:----:| 5 | |**Databases**||||| 6 | | Get information about a database | `/{db}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala#L36-43)| 7 | | Create a new database | `/{db}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala#L30-34)| 8 | | Delete a specified database | `/{db}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala#L51-56)| 9 | | Batch mode writes | `/{db}?batch=ok` | | | | 10 | |**Documents**||||| 11 | | Retrieve document | `/{db}/{doc_id}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L45-54)| 12 | | Retrieve document by revision number | `/{db}/{doc_id}` | | | | 13 | | Create document | `/{db}/` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L40-43)| 14 | | Get list of document revisions | `/{db}/{doc_id}` | | | | 15 | | Update document | `/{db}/{doc_id}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L122-132)| 16 | | Delete document | `/{db}/{doc_id}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/cohdb/api/DocumentsSpec.scala#L149-154)| 17 | | Copy document | `/{db}/{doc_id}` | | | | 18 | | Copy document by revision | `/{db}/{doc_id}` | | | | 19 | | Copy to an existing document | `/{db}/{doc_id}` | | | | 20 | | Get attachment information | `/{db}/{doc_id}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L172-188)| 21 | | Create single attachment | `/{db}/{doc_id}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L156-160)| 22 | | Create multiple attachment | `/{db}/{doc_id}` | | | | 23 | | Retrieve attachment | `/{db}/{doc_id}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L162-170)| 24 | | Retrieve multiple attachments | `/{db}/{doc_id}` | | | | 25 | | Delete attachment | `/{db}/{doc_id}/{attachment_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L209-218)| 26 | | Get all documents in a database | ` /{db}/_all_docs` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L70-79)| 27 | | Get documents given multiple keys | `/{db}/_all_docs` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L81-91)| 28 | | Create and update multiple documents | `/{db}/_bulk_docs` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L62-68)| 29 | | Get list of changes to documents | `/{db}/_changes` | | | | 30 | | Get list of changes for specified document ids | `/{db}/_changes` | | | | 31 | | Compact database | `/{db}/_compact` | | | | 32 | | Compact view indexes associated with specified design document | `/{db}/_compact/{des_doc}` | | | | 33 | | Commit recent changes to disk | `/{db}/_ensure_full_commit` | | | | 34 | | Remove view index files that are not required | `/{db}/_view_cleanup` | | | | 35 | | Get security object from database | `/{db}/_security` | | | | 36 | | Create and execute temporary view | `/{db}/_temp_view` | | | | 37 | | Permanently remove references to delete documents | `/{db}/_purge` | | | | 38 | | Given list of document revisions, return this that do not exist| `/{db}/_missing_revs` | | | | 39 | | Given list of revision ids, returns those that do not correspond to revisions in database | `/{db}/_revs_diff` | | | | 40 | | Get the current revision limit setting | `/{db}/_revs_limit` | | | | 41 | |**Design Documents**||||| 42 | | Get content of design document | `/{db}/_design/{des_doc}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L46-53)| 43 | | Get meta-data about design document | `/{db}/_design/{des_doc}/_info` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L37-44)| 44 | | Create design document | `/{db}/_design/{des_doc}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L32-35)| 45 | | Update design document | `/{db}/_design/{des_doc}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L64-75)| 46 | | Delete design document | `/{db}/_design/{des_doc}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L77-82)| 47 | | Copy design document | `/{db}/_design/{des_doc}` | | | | 48 | | Create attachment | `/{db}/_design/{des_doc}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L90-95)| 49 | | Get attachment | `/{db}/_design/{des_doc}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L97-103)| 50 | | Delete design document attachment | `/{db}/_design/{des_doc}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L77-82)| 51 | | Execute specified view function from the specified design document | `/{db}/_design/{des_doc}/_view/{view_name}` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/QueryViewSpec.scala)| 52 | | Applies show function for specified documents| `/{db}/_design/{des_doc} /_show/{show_name}/{doc_id}` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/QueryShowSpec.scala)| 53 | | Applies list function for the view function from the same design document | `/{db}/_design/{des_doc} /_list/{list_name}/{view_name}` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/QueryListSpec.scala)| 54 | | Applies list function for the view function from another design document | `/{db}/_design/{des_doc} / _list/{list_name}/{other_design_doc}/{view_name}`| | | | 55 | | Rewrite the specified path by rules in specified design document | `/{db}/_design/{des-doc}/_rewrite/path` | | | | 56 | |**Local Documents**||||| 57 | | Get local document | `/{db}/_local/{doc_id}` | | | | 58 | | Store local document | `/{db}/_local/{doc_id}` | | | | 59 | | Delete local document | `/{db}/_local/{doc_id}` | | | | 60 | | Copy local document | `/{db}/_local/{doc_id}` | | | | 61 | |**Server**||||| 62 | | Get meta information about instance | `/` | ✓ | 0.5 | [view] (https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/ServerSpec.scala#L28-33) | 63 | | Request a Universally Unique Identifier | `/_uuid` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/ServerSpec.scala#L31-37) | 64 | | List databases | `/_all_dbs` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala#L45-49)| 65 | | List active tasks | `/_active_tasks` | | | | 66 | | List database events | `/_db_updates` | | | | 67 | | View Logs | `/_log` | | | | 68 | | Request, configure or stop a replication request | `/_replicate` | | | | 69 | | Restart instance | `/_restart` | | | | 70 | | Statistics about server | `/_stats` | | | | 71 | -------------------------------------------------------------------------------- /examples/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/src/main/scala/com/ibm/couchdb/examples/Basic.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.examples 18 | 19 | import com.ibm.couchdb._ 20 | import org.slf4j.LoggerFactory 21 | 22 | import scalaz._ 23 | import scalaz.concurrent.Task 24 | 25 | object Basic extends App { 26 | private val logger = LoggerFactory.getLogger(Basic.getClass) 27 | 28 | // Define a simple case class to represent our data model 29 | case class Person(name: String, age: Int) 30 | 31 | // Define a type mapping used to transform class names into the doc kind 32 | val typeMapping = TypeMapping(classOf[Person] -> "Person") 33 | 34 | // Define some sample data 35 | val alice = Person("Alice", 25) 36 | val bob = Person("Bob", 30) 37 | val carl = Person("Carl", 20) 38 | 39 | // Create a CouchDB client instance 40 | val couch = CouchDb("127.0.0.1", 5984) 41 | // Define a database name 42 | val dbName = "couchdb-scala-basic-example" 43 | // Get an instance of the DB API by name and type mapping 44 | val db = couch.db(dbName, typeMapping) 45 | 46 | typeMapping.get(classOf[Person]).foreach { mType => 47 | val actions: Task[Seq[Person]] = for { 48 | // Delete the database or ignore the error if it doesn't exist 49 | _ <- couch.dbs.delete(dbName).ignoreError 50 | // Create a new database 51 | _ <- couch.dbs.create(dbName) 52 | // Insert documents into the database 53 | _ <- db.docs.createMany(Seq(alice, bob, carl)) 54 | // Retrieve all documents from the database and unserialize to Person 55 | docs <- db.docs.getMany.includeDocs[Person].byTypeUsingTemporaryView(mType).build.query 56 | } yield docs.getDocsData 57 | 58 | // Execute the actions and process the result 59 | actions.unsafePerformSyncAttempt match { 60 | // In case of an error (left side of Either), print it 61 | case -\/(e) => logger.error(e.getMessage, e) 62 | // In case of a success (right side of Either), print each object 63 | case \/-(a) => a.foreach(x => logger.info(x.toString)) 64 | } 65 | couch.client.client.shutdownNow() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.sonatypeRepo("releases") 2 | 3 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.0-RC1") 4 | 5 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.2") 6 | 7 | addSbtPlugin("org.brianmckenna" % "sbt-wartremover" % "0.14") 8 | 9 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0") 10 | 11 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 12 | 13 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "0.2.1") -------------------------------------------------------------------------------- /scalastyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | Scalastyle standard configuration 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true 12 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/CouchDb.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb 18 | 19 | import com.ibm.couchdb.api.{Databases, Design, Documents, Query, Server} 20 | import com.ibm.couchdb.core.Client 21 | 22 | import scalaz.Scalaz._ 23 | import scalaz._ 24 | 25 | case class CouchDbApi(name: String, docs: Documents, design: Design, query: Query) 26 | 27 | class CouchDb private( 28 | host: String, 29 | port: Int, 30 | https: Boolean, 31 | credentials: Option[(String, String)]) { 32 | 33 | val client = new Client(Config(host, port, https, credentials)) 34 | val server = new Server(client) 35 | val dbs = new Databases(client) 36 | 37 | private val memo = Memo.mutableHashMapMemo[(String, TypeMapping), CouchDbApi] { 38 | case (db, types) => 39 | CouchDbApi( 40 | db, 41 | new Documents(client, db, types), 42 | new Design(client, db), 43 | new Query(client, db)) 44 | } 45 | 46 | def db(name: String, types: TypeMapping): CouchDbApi = memo((name, types)) 47 | 48 | } 49 | 50 | object CouchDb { 51 | 52 | def apply(host: String, port: Int, https: Boolean = false): CouchDb = { 53 | new CouchDb(host, port, https, none) 54 | } 55 | 56 | def apply( 57 | host: String, port: Int, https: Boolean, username: String, password: String): CouchDb = { 58 | new CouchDb(host, port, https, (username, password).some) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/Lenses.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb 18 | 19 | import monocle.PLens 20 | 21 | object Lenses { 22 | 23 | def _couchDoc[A, B]: PLens[CouchDoc[A], CouchDoc[B], A, B] = 24 | PLens[CouchDoc[A], CouchDoc[B], A, B](_.doc)(d => cd => cd.copy(doc = d)) 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/Model.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation, Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb 18 | 19 | import java.util.Base64 20 | 21 | import org.http4s.headers.`Content-Type` 22 | 23 | import scalaz.\/ 24 | 25 | case class Config(host: String, port: Int, https: Boolean, credentials: Option[(String, String)]) 26 | 27 | case class CouchDoc[D]( 28 | doc: D, 29 | kind: String, 30 | _id: String = "", 31 | _rev: String = "", 32 | _deleted: Boolean = false, 33 | _attachments: Map[String, CouchAttachment] = Map.empty[String, CouchAttachment], 34 | _conflicts: Seq[String] = Seq.empty[String], 35 | _deleted_conflicts: Seq[String] = Seq.empty[String], 36 | _local_seq: Int = 0) 37 | 38 | case class CouchDocRev(rev: String) 39 | 40 | case class CouchKeyVal[K, V](id: String, key: K, value: V) 41 | 42 | case class CouchReducedKeyVal[K, V](key: K, value: V) 43 | 44 | case class CouchKeyError[K](key: K, error: String) 45 | 46 | case class CouchKeyValWithDoc[K, V, D](id: String, key: K, value: V, doc: CouchDoc[D]) 47 | 48 | case class CouchKeyVals[K, V](offset: Int, total_rows: Int, rows: Seq[CouchKeyVal[K, V]]) 49 | 50 | case class CouchReducedKeyVals[K, V](rows: Seq[CouchReducedKeyVal[K, V]]) 51 | 52 | case class CouchKeyValsIncludesMissing[K, V]( 53 | offset: Int, 54 | total_rows: Int, 55 | rows: Seq[\/[CouchKeyError[K], CouchKeyVal[K, V]]]) 56 | 57 | case class CouchDocs[K, V, D]( 58 | offset: Int, total_rows: Int, rows: Seq[CouchKeyValWithDoc[K, V, D]]) { 59 | 60 | def getDocs: Seq[CouchDoc[D]] = rows.map(_.doc) 61 | 62 | def getDocsData: Seq[D] = rows.map(_.doc.doc) 63 | } 64 | 65 | case class CouchDocsIncludesMissing[K, V, D]( 66 | offset: Int, 67 | total_rows: Int, 68 | rows: Seq[\/[CouchKeyError[K], CouchKeyValWithDoc[K, V, D]]]) { 69 | 70 | def getDocs: Seq[CouchDoc[D]] = rows.flatMap(_.toOption).map(_.doc) 71 | 72 | def getDocsData: Seq[D] = rows.flatMap(_.toOption).map(_.doc.doc) 73 | } 74 | 75 | case class CouchAttachment( 76 | content_type: String, 77 | revpos: Int = -1, 78 | digest: String = "", 79 | data: String = "", 80 | length: Int = -1, 81 | stub: Boolean = false) { 82 | def toBytes: Array[Byte] = Base64.getDecoder.decode(data) 83 | } 84 | 85 | case object CouchAttachment { 86 | def fromBytes(data: Array[Byte], content_type: String = ""): CouchAttachment = { 87 | CouchAttachment( 88 | content_type = content_type, 89 | data = Base64.getEncoder.encodeToString(data)) 90 | } 91 | 92 | def fromBytes(data: Array[Byte], content_type: `Content-Type`): CouchAttachment = { 93 | CouchAttachment( 94 | content_type = content_type.toString, 95 | data = Base64.getEncoder.encodeToString(data)) 96 | } 97 | } 98 | 99 | case class CouchView(map: String, reduce: String = "") 100 | 101 | case class CouchDesign( 102 | name: String, 103 | _id: String = "", 104 | _rev: String = "", 105 | language: String = "javascript", 106 | validate_doc_update: String = "", 107 | views: Map[String, CouchView] = Map.empty[String, CouchView], 108 | shows: Map[String, String] = Map.empty[String, String], 109 | lists: Map[String, String] = Map.empty[String, String], 110 | _attachments: Map[String, CouchAttachment] = Map.empty[String, CouchAttachment], 111 | signatures: Map[String, String] = Map.empty[String, String]) 112 | 113 | case class CouchException[D](content: D) extends Throwable { 114 | override def toString: String = "CouchException: " + content 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/Req.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation, Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb 18 | 19 | object Req { 20 | 21 | case class Docs[D](docs: Seq[CouchDoc[D]]) 22 | 23 | case class DocKeys[K](keys: Seq[K]) 24 | 25 | case class ViewWithKeys[K](keys: Seq[K], view: CouchView) 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/Res.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb 18 | 19 | import org.http4s.Status 20 | 21 | import scalaz.concurrent.Task 22 | 23 | object Res { 24 | 25 | case class Ok(ok: Boolean = true) 26 | 27 | case class Error( 28 | error: String, 29 | reason: String, 30 | status: Status = Status.ExpectationFailed, 31 | request: String = "", 32 | requestBody: String = "") { 33 | def toTask[T]: Task[T] = Task.fail(CouchException(this)) 34 | } 35 | 36 | case class ServerInfo( 37 | couchdb: String, 38 | uuid: String, 39 | version: String, 40 | vendor: ServerVendor) 41 | 42 | case class ServerVendor(version: String, name: String) 43 | 44 | case class DbInfo( 45 | committed_update_seq: Int, 46 | compact_running: Boolean, 47 | data_size: Int, 48 | db_name: String, 49 | disk_format_version: Int, 50 | disk_size: Int, 51 | doc_count: Int, 52 | doc_del_count: Int, 53 | instance_start_time: String, 54 | purge_seq: Int, 55 | update_seq: Int) 56 | 57 | case class ViewIndexInfo( 58 | compact_running: Boolean, 59 | data_size: Int, 60 | disk_size: Int, 61 | language: String, 62 | purge_seq: Int, 63 | signature: String, 64 | update_seq: Int, 65 | updater_running: Boolean, 66 | waiting_clients: Int, 67 | waiting_commit: Boolean) 68 | 69 | case class DesignInfo(name: String, view_index: ViewIndexInfo) 70 | 71 | case class Uuids(uuids: Seq[String]) 72 | 73 | case class DocOk(ok: Boolean, id: String, rev: String) 74 | } 75 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/TypeMapping.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb 18 | 19 | final case class MappedDocType private(name: String) 20 | 21 | final class TypeMapping private(private val types: Map[String, String]) { 22 | def get(t: Class[_]): Option[MappedDocType] = { 23 | types.get(t.getCanonicalName).map(MappedDocType) 24 | } 25 | 26 | def contains(t: Class[_]): Boolean = { 27 | types.contains(t.getCanonicalName) 28 | } 29 | 30 | override def toString: String = types.toString 31 | } 32 | 33 | object TypeMapping { 34 | val empty = new TypeMapping(Map.empty[String, String]) 35 | 36 | def apply(mapping: (Class[_], String)*): TypeMapping = { 37 | new TypeMapping(mapping.map((x: (Class[_], String)) => (x._1.getCanonicalName, x._2)).toMap) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/Databases.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.Res 20 | import com.ibm.couchdb.core.Client 21 | import org.http4s.Status 22 | 23 | import scalaz.concurrent.Task 24 | 25 | class Databases(client: Client) { 26 | 27 | def get(name: String): Task[Res.DbInfo] = { 28 | client.get[Res.DbInfo](s"/$name", Status.Ok) 29 | } 30 | 31 | def getAll: Task[Seq[String]] = { 32 | client.get[Seq[String]]("/_all_dbs", Status.Ok) 33 | } 34 | 35 | def create(name: String): Task[Res.Ok] = { 36 | client.put[Res.Ok](s"/$name", Status.Created) 37 | } 38 | 39 | def delete(name: String): Task[Res.Ok] = { 40 | client.delete[Res.Ok](s"/$name", Status.Ok) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/Design.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb._ 20 | import com.ibm.couchdb.core.Client 21 | import org.http4s.Status 22 | 23 | import scalaz.concurrent.Task 24 | 25 | class Design(client: Client, db: String) { 26 | 27 | def create(design: CouchDesign): Task[Res.DocOk] = { 28 | if (design.name.isEmpty) 29 | Res.Error("cannot_create", "Design name must not be empty").toTask 30 | else 31 | client.put[CouchDesign, Res.DocOk]( 32 | s"/$db/_design/${design.name}", 33 | Status.Created, 34 | design) 35 | } 36 | 37 | def info(name: String): Task[Res.DesignInfo] = { 38 | if (name.isEmpty) 39 | Res.Error("not_found", "Design name must not be empty").toTask 40 | else 41 | client.get[Res.DesignInfo]( 42 | s"/$db/_design/$name/_info", 43 | Status.Ok) 44 | } 45 | 46 | def get(name: String): Task[CouchDesign] = { 47 | if (name.isEmpty) 48 | Res.Error("not_found", "Design name must not be empty").toTask 49 | else 50 | client.get[CouchDesign]( 51 | s"/$db/_design/$name", 52 | Status.Ok 53 | ) 54 | } 55 | 56 | def getWithAttachments(name: String): Task[CouchDesign] = { 57 | if (name.isEmpty) 58 | Res.Error("not_found", "Design name must not be empty").toTask 59 | else 60 | client.get[CouchDesign]( 61 | s"/$db/_design/$name?attachments=true", 62 | Status.Ok 63 | ) 64 | } 65 | 66 | def getById(id: String): Task[CouchDesign] = { 67 | if (id.isEmpty) 68 | Res.Error("not_found", "Design ID must not be empty").toTask 69 | else 70 | client.get[CouchDesign]( 71 | s"/$db/$id", 72 | Status.Ok 73 | ) 74 | } 75 | 76 | def update(design: CouchDesign): Task[Res.DocOk] = { 77 | if (design._id.isEmpty) 78 | Res.Error("cannot_update", "Design ID must not be empty").toTask 79 | else 80 | client.put[CouchDesign, Res.DocOk]( 81 | s"/$db/${design._id}", 82 | Status.Created, 83 | design) 84 | } 85 | 86 | def delete(design: CouchDesign): Task[Res.DocOk] = { 87 | if (design._id.isEmpty) 88 | Res.Error("cannot_delete", "Design ID must not be empty").toTask 89 | else 90 | client.delete[Res.DocOk]( 91 | s"/$db/${design._id}?rev=${design._rev}", 92 | Status.Ok) 93 | } 94 | 95 | def deleteByName(name: String): Task[Res.DocOk] = { 96 | if (name.isEmpty) 97 | Res.Error("cannot_delete", "Design name must not be empty").toTask 98 | else 99 | get(name) flatMap delete 100 | } 101 | 102 | def attach( 103 | design: CouchDesign, 104 | name: String, 105 | data: Array[Byte], 106 | contentType: String = ""): Task[Res.DocOk] = { 107 | if (design._id.isEmpty) 108 | Res.Error("cannot_attach", "Design ID must not be empty").toTask 109 | else 110 | client.put[Res.DocOk]( 111 | s"/$db/${design._id}/$name?rev=${design._rev}", 112 | Status.Created, 113 | data, 114 | contentType) 115 | } 116 | 117 | def getAttachment(design: CouchDesign, name: String): Task[Array[Byte]] = { 118 | if (design._id.isEmpty) 119 | Res.Error("not_found", "Design ID must not be empty").toTask 120 | else 121 | client.getBinary( 122 | s"/$db/${design._id}/$name", 123 | Status.Ok) 124 | } 125 | 126 | def deleteAttachment(design: CouchDesign, name: String): Task[Res.DocOk] = { 127 | if (design._id.isEmpty) 128 | Res.Error("cannot_delete", "Design ID must not be empty").toTask 129 | else if (name.isEmpty) 130 | Res.Error("cannot_delete", "Attachment name must not be empty").toTask 131 | else 132 | client.delete[Res.DocOk]( 133 | s"/$db/${design._id}/$name?rev=${design._rev}", 134 | Status.Ok) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/Documents.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation, Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import java.nio.file.{Files, Paths} 20 | 21 | import com.ibm.couchdb._ 22 | import com.ibm.couchdb.api.builders._ 23 | import com.ibm.couchdb.core.Client 24 | import org.http4s.Status 25 | import upickle.default.Aliases.{R, W} 26 | 27 | import scalaz.concurrent.Task 28 | 29 | class Documents(client: Client, db: String, typeMapping: TypeMapping) { 30 | 31 | val server = new Server(client) 32 | 33 | def create[D: W](obj: D): Task[Res.DocOk] = { 34 | server.mkUuid flatMap (create(obj, _)) 35 | } 36 | 37 | def create[D: W](obj: D, attachments: Map[String, CouchAttachment]): Task[Res.DocOk] = { 38 | server.mkUuid flatMap (create(obj, _, attachments)) 39 | } 40 | 41 | def create[D: W]( 42 | obj: D, id: String, 43 | attachments: Map[String, CouchAttachment] = Map.empty): Task[Res.DocOk] = { 44 | typeMapping.get(obj.getClass) match { 45 | case Some(t) => 46 | client.put[CouchDoc[D], Res.DocOk]( 47 | s"/$db/$id", 48 | Status.Created, 49 | CouchDoc[D](obj, t.name, _attachments = attachments)) 50 | case None => 51 | val cl = obj.getClass.getCanonicalName 52 | Res.Error( 53 | "cannot_create", "No type mapping for " + cl + " available: " + typeMapping).toTask 54 | } 55 | } 56 | 57 | private def postBulk[D: W](objs: Seq[CouchDoc[D]]): Task[Seq[Res.DocOk]] = { 58 | client.post[Req.Docs[D], Seq[Res.DocOk]]( 59 | s"/$db/_bulk_docs", 60 | Status.Created, Req.Docs(objs)) 61 | } 62 | 63 | def createMany[D: W](objs: Map[String, D]): Task[Seq[Res.DocOk]] = create(objs.toSeq) 64 | 65 | def createMany[D: W](objs: Seq[D]): Task[Seq[Res.DocOk]] = create(objs.map(("", _))) 66 | 67 | private def create[D: W, S](objs: Seq[(String, D)]): Task[Seq[Res.DocOk]] = { 68 | objs.find { x => !typeMapping.contains(x._2.getClass) } match { 69 | case Some(missing) => 70 | Res.Error("cannot_create", "No type mapping for " + missing).toTask 71 | case None => 72 | postBulk( 73 | objs.map { o => CouchDoc[D]( 74 | _id = o._1, 75 | doc = o._2, 76 | kind = typeMapping.get(o._2.getClass).fold("")(_.name)) 77 | }) 78 | } 79 | } 80 | 81 | def updateMany[D: W](objs: Seq[CouchDoc[D]]): Task[Seq[Res.DocOk]] = { 82 | def invalidDoc(x: CouchDoc[D]): Boolean = x._id.isEmpty || x._rev.isEmpty 83 | objs.find(invalidDoc) match { 84 | case Some(doc) => 85 | val missingField = if (doc._id.isEmpty) "an ID" else "a REV number" 86 | Res.Error("cannot_update", s"One or more documents do not contain $missingField.").toTask 87 | case None => postBulk(objs) 88 | } 89 | } 90 | 91 | def deleteMany[D: W](objs: Seq[CouchDoc[D]]): Task[Seq[Res.DocOk]] = { 92 | updateMany(objs.map(_.copy(_deleted = true))) 93 | } 94 | 95 | def get: GetDocumentQueryBuilder = GetDocumentQueryBuilder(client, db) 96 | 97 | def get[D: R](id: String): Task[CouchDoc[D]] = { 98 | get.query[D](id) 99 | } 100 | 101 | def getMany: GetManyDocumentsQueryBuilder[ExcludeDocs, MissingDisallowed, AnyDocType] = 102 | GetManyDocumentsQueryBuilder(client, db, typeMapping) 103 | 104 | def getMany[D: R](ids: Seq[String]): Task[CouchDocs[String, CouchDocRev, D]] = { 105 | getMany.includeDocs[D].withIds(ids).build.query 106 | } 107 | 108 | def update[D: W](obj: CouchDoc[D]): Task[Res.DocOk] = { 109 | if (obj._id.isEmpty) 110 | Res.Error("cannot_update", "Document ID must not be empty").toTask 111 | else 112 | client.put[CouchDoc[D], Res.DocOk]( 113 | s"/$db/${obj._id}", 114 | Status.Created, 115 | obj) 116 | } 117 | 118 | def delete[D](obj: CouchDoc[D]): Task[Res.DocOk] = { 119 | if (obj._id.isEmpty) 120 | Res.Error("cannot_delete", "Document ID must not be empty").toTask 121 | else { 122 | client.delete[Res.DocOk]( 123 | s"/$db/${obj._id}?rev=${obj._rev}", 124 | Status.Ok) 125 | } 126 | } 127 | 128 | def attach[D]( 129 | obj: CouchDoc[D], 130 | name: String, 131 | data: Array[Byte], 132 | contentType: String = ""): Task[Res.DocOk] = { 133 | if (obj._id.isEmpty) 134 | Res.Error("cannot_attach", "Document ID must not be empty").toTask 135 | else { 136 | client.put[Res.DocOk]( 137 | s"/$db/${obj._id}/$name?rev=${obj._rev}", 138 | Status.Created, 139 | data, 140 | contentType) 141 | } 142 | } 143 | 144 | def attach[D](obj: CouchDoc[D], name: String, path: String): Task[Res.DocOk] = { 145 | readFile(path) flatMap { 146 | attachment => attach(obj, name, attachment) 147 | } 148 | } 149 | 150 | def getAttachmentResource[D](obj: CouchDoc[D], name: String): Task[String] = { 151 | if (obj._id.isEmpty) 152 | Res.Error("not_found", "Document ID must not be empty").toTask 153 | else 154 | Task.now(s"/$db/${obj._id}/$name") 155 | } 156 | 157 | def getAttachmentUrl[D](obj: CouchDoc[D], name: String): Task[String] = { 158 | getAttachmentResource(obj, name).map(client.url(_).toString()) 159 | } 160 | 161 | def getAttachment[D](obj: CouchDoc[D], name: String): Task[Array[Byte]] = { 162 | getAttachmentResource(obj, name).flatMap(client.getBinary(_, Status.Ok)) 163 | } 164 | 165 | def deleteAttachment[D](obj: CouchDoc[D], name: String): Task[Res.DocOk] = { 166 | if (obj._id.isEmpty) 167 | Res.Error("cannot_delete", "Document ID must not be empty").toTask 168 | else if (name.isEmpty) 169 | Res.Error("cannot_delete", "The attachment name is empty").toTask 170 | else 171 | client.delete[Res.DocOk]( 172 | s"/$db/${obj._id}/$name?rev=${obj._rev}", 173 | Status.Ok) 174 | } 175 | 176 | private def readFile(path: String): Task[Array[Byte]] = Task { 177 | // TODO: replace with scodec / scalaz-stream / scodec-stream? 178 | Files.readAllBytes(Paths.get(path)) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/Query.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.CouchView 20 | import com.ibm.couchdb.api.builders._ 21 | import com.ibm.couchdb.core.Client 22 | import upickle.default.Aliases.{R, W} 23 | 24 | import scalaz.Scalaz._ 25 | 26 | class Query(client: Client, db: String) { 27 | 28 | def view[K: R, V: R](design: String, view: String)(implicit kw: W[K]): 29 | Option[ViewQueryBuilder[K, V, ExcludeDocs, MapOnly]] = { 30 | if (design.isEmpty || view.isEmpty) 31 | none[ViewQueryBuilder[K, V, ExcludeDocs, MapOnly]] 32 | else ViewQueryBuilder[K, V](client, db, design, view).some 33 | } 34 | 35 | def temporaryView[K: R, V: R](view: CouchView)(implicit kw: W[K]): 36 | Option[ViewQueryBuilder[K, V, ExcludeDocs, MapOnly]] = { 37 | if (view.map.isEmpty) 38 | none[ViewQueryBuilder[K, V, ExcludeDocs, MapOnly]] 39 | else ViewQueryBuilder[K, V](client, db, view).some 40 | } 41 | 42 | def show(design: String, show: String): Option[ShowQueryBuilder] = { 43 | if (design.isEmpty || show.isEmpty) none[ShowQueryBuilder] 44 | else ShowQueryBuilder(client, db, design, show).some 45 | } 46 | 47 | def list(design: String, list: String): Option[ListQueryBuilder] = { 48 | if (design.isEmpty || list.isEmpty) none[ListQueryBuilder] 49 | else ListQueryBuilder(client, db, design, list).some 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/Server.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.Res 20 | import com.ibm.couchdb.core.Client 21 | import org.http4s.Status 22 | 23 | import scalaz.concurrent.Task 24 | 25 | class Server(client: Client) { 26 | 27 | def info: Task[Res.ServerInfo] = { 28 | client.get[Res.ServerInfo]("/", Status.Ok) 29 | } 30 | 31 | def mkUuid: Task[String] = { 32 | client.get[Res.Uuids]("/_uuids", Status.Ok).map(_.uuids(0)) 33 | } 34 | 35 | def mkUuids(count: Int): Task[Seq[String]] = { 36 | client.get[Res.Uuids](s"/_uuids?count=$count", Status.Ok).map(_.uuids) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/builders/GetDocumentQueryBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation, Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api.builders 18 | 19 | import com.ibm.couchdb._ 20 | import com.ibm.couchdb.core.Client 21 | import org.http4s.Status 22 | import upickle.default.Aliases.R 23 | import upickle.default.write 24 | 25 | import scalaz.concurrent.Task 26 | 27 | case class GetDocumentQueryBuilder( 28 | client: Client, db: String, params: Map[String, String] = Map.empty[String, String]) { 29 | 30 | def attachments(attachments: Boolean = true): GetDocumentQueryBuilder = { 31 | set("attachments", attachments) 32 | } 33 | 34 | def attEncodingInfo(attEncodingInfo: Boolean = true): GetDocumentQueryBuilder = { 35 | set("att_encoding_info", attEncodingInfo) 36 | } 37 | 38 | def attsSince(attsSince: Seq[String]): GetDocumentQueryBuilder = { 39 | set("atts_since", write(attsSince)) 40 | } 41 | 42 | def conflicts(conflicts: Boolean = true): GetDocumentQueryBuilder = { 43 | set("conflicts", conflicts) 44 | } 45 | 46 | def deletedConflicts(deletedConflicts: Boolean = true): GetDocumentQueryBuilder = { 47 | set("deleted_conflicts", deletedConflicts) 48 | } 49 | 50 | def latest(latest: Boolean = true): GetDocumentQueryBuilder = { 51 | set("latest", latest) 52 | } 53 | 54 | def localSeq(localSeq: Boolean = true): GetDocumentQueryBuilder = { 55 | set("local_seq", localSeq) 56 | } 57 | 58 | def meta(meta: Boolean = true): GetDocumentQueryBuilder = { 59 | set("meta", meta) 60 | } 61 | 62 | def openRevs(openRevs: Seq[String]): GetDocumentQueryBuilder = { 63 | set("open_revs", write(openRevs)) 64 | } 65 | 66 | def openRevs(openRevs: String): GetDocumentQueryBuilder = { 67 | set("open_revs", openRevs) 68 | } 69 | 70 | def rev(rev: String): GetDocumentQueryBuilder = { 71 | set("rev", rev) 72 | } 73 | 74 | def revs(revs: Boolean = true): GetDocumentQueryBuilder = { 75 | set("revs", revs) 76 | } 77 | 78 | def revsInfo(revsInfo: Boolean = true): GetDocumentQueryBuilder = { 79 | set("revs_info", revsInfo) 80 | } 81 | 82 | private def set(key: String, value: String): GetDocumentQueryBuilder = { 83 | copy(params = params.updated(key, value)) 84 | } 85 | 86 | private def set(key: String, value: Any): GetDocumentQueryBuilder = { 87 | set(key, value.toString) 88 | } 89 | 90 | def query[D: R](id: String): Task[CouchDoc[D]] = { 91 | if (id.isEmpty) 92 | Res.Error("not_found", "No ID specified").toTask 93 | else 94 | client.get[CouchDoc[D]]( 95 | s"/$db/$id", 96 | Status.Ok, 97 | params.toSeq) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/builders/GetManyDocumentsQueryBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api.builders 18 | 19 | import com.ibm.couchdb._ 20 | import com.ibm.couchdb.core.Client 21 | import upickle.default.Aliases.{R, W} 22 | import upickle.default.write 23 | 24 | import scala.reflect.ClassTag 25 | 26 | sealed trait DocsInResult 27 | abstract class IncludeDocs[D: R] extends DocsInResult 28 | trait ExcludeDocs extends DocsInResult 29 | 30 | sealed trait MissingIdsInQuery 31 | trait MissingAllowed extends MissingIdsInQuery 32 | trait MissingDisallowed extends MissingIdsInQuery 33 | 34 | sealed trait DocType 35 | abstract class ForDocType[D: R, K: R, V: R] extends DocType 36 | trait AnyDocType extends DocType 37 | 38 | case class GetManyDocumentsQueryBuilder[ID <: DocsInResult, AM <: MissingIdsInQuery, 39 | BT <: DocType] private( 40 | client: Client, 41 | db: String, 42 | typeMappings: TypeMapping, 43 | params: Map[String, String] = Map.empty[String, String], 44 | ids: Seq[String] = Seq.empty, view: Option[CouchView] = None) { 45 | 46 | private[builders] val url: String = s"/$db/_all_docs" 47 | 48 | lazy val tempTypeFilterView: CouchView = { 49 | CouchView( 50 | map = 51 | """ 52 | |function(doc) { 53 | | emit([doc.kind, doc._id], doc._id); 54 | |} 55 | """.stripMargin) 56 | } 57 | 58 | def conflicts( 59 | conflicts: Boolean = true): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 60 | set("conflicts", conflicts) 61 | } 62 | 63 | def descending( 64 | descending: Boolean = true): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 65 | set("descending", descending) 66 | } 67 | 68 | def endKey[K: W](endKey: K): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 69 | set("endkey", write(endKey)) 70 | } 71 | 72 | def endKeyDocId( 73 | endKeyDocId: String): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 74 | set("endkey_docid", endKeyDocId) 75 | } 76 | 77 | def includeDocs[D: R]: GetManyDocumentsQueryBuilder[IncludeDocs[D], AM, BT] = { 78 | set("include_docs", true) 79 | } 80 | 81 | def excludeDocs: GetManyDocumentsQueryBuilder[ExcludeDocs, AM, BT] = { 82 | set("include_docs", false) 83 | } 84 | 85 | def allowMissing: GetManyDocumentsQueryBuilder[ID, MissingAllowed, BT] = { 86 | setType() 87 | } 88 | 89 | def disallowMissing: GetManyDocumentsQueryBuilder[ID, MissingDisallowed, BT] = { 90 | setType() 91 | } 92 | 93 | def withIds(ids: Seq[String]): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 94 | set(params, ids, view) 95 | } 96 | 97 | def byType[K: R, V: R](view: String, design: String, mappedType: MappedDocType) 98 | (implicit kw: W[K]): ViewQueryBuilder[K, V, ID, MapOnly] = { 99 | new ViewQueryBuilder[K, V, ID, MapOnly]( 100 | client, db, Option(design), Option(view), params = params). 101 | startKey(Tuple1(mappedType.name)).endKey(Tuple2(mappedType.name, {})) 102 | } 103 | 104 | def byType[V: R](view: String, design: String, mappedType: MappedDocType): 105 | ViewQueryBuilder[(String, String), V, ID, MapOnly] = { 106 | byType[(String, String), V](view, design, mappedType) 107 | } 108 | 109 | def byTypeUsingTemporaryView(mappedType: MappedDocType): 110 | ViewQueryBuilder[(String, String), String, ID, MapOnly] = { 111 | new ViewQueryBuilder[(String, String), String, ID, MapOnly]( 112 | client, db, None, None, temporaryView = Option(tempTypeFilterView), params = params). 113 | startKey(Tuple1(mappedType.name)).endKey(Tuple2(mappedType.name, {})) 114 | } 115 | 116 | def inclusiveEnd(inclusiveEnd: Boolean = true): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 117 | set("inclusive_end", inclusiveEnd) 118 | } 119 | 120 | def key[K: W](key: K): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 121 | set("key", write(key)) 122 | } 123 | 124 | def limit(limit: Int): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 125 | set("limit", limit) 126 | } 127 | 128 | def skip(skip: Int): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 129 | set("skip", skip) 130 | } 131 | 132 | def stale(stale: String): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 133 | set("stale", stale) 134 | } 135 | 136 | def startKey[K: W]( 137 | startKey: K): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 138 | set("startkey", write(startKey)) 139 | } 140 | 141 | def startKeyDocId( 142 | startKeyDocId: String): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 143 | set("startkey_docid", startKeyDocId) 144 | } 145 | 146 | def updateSeq( 147 | updateSeq: Boolean = true): GetManyDocumentsQueryBuilder[ID, AM, BT] = { 148 | set("update_seq", updateSeq) 149 | } 150 | 151 | private def setType[I <: DocsInResult, A <: MissingIdsInQuery, B <: DocType](): 152 | GetManyDocumentsQueryBuilder[I, A, B] = { 153 | set[I, A, B](params, ids, view) 154 | } 155 | 156 | private def set[I <: DocsInResult, A <: MissingIdsInQuery, B <: DocType] 157 | (_params: Map[String, String], _ids: Seq[String], _view: Option[CouchView]): 158 | GetManyDocumentsQueryBuilder[I, A, B] = { 159 | new GetManyDocumentsQueryBuilder(client, db, typeMappings, _params, _ids, _view) 160 | } 161 | 162 | private def set[I <: DocsInResult, A <: MissingIdsInQuery, B <: DocType]( 163 | key: String, value: String): GetManyDocumentsQueryBuilder[I, A, B] = { 164 | set(params.updated(key, value), ids, view) 165 | } 166 | 167 | private def set[I <: DocsInResult, A <: MissingIdsInQuery, B <: DocType]( 168 | key: String, value: Any): GetManyDocumentsQueryBuilder[I, A, B] = { 169 | set(key, value.toString) 170 | } 171 | } 172 | 173 | object GetManyDocumentsQueryBuilder { 174 | 175 | private type MDBuilder[ID <: DocsInResult, AM <: MissingIdsInQuery, BT <: DocType] = 176 | GetManyDocumentsQueryBuilder[ID, AM, BT] 177 | 178 | case class Builder[T: R, ID <: DocsInResult, AM <: MissingIdsInQuery] 179 | (builder: MDBuilder[ID, AM, AnyDocType]) { 180 | def build: QueryBasic[T] = QueryBasic( 181 | builder.client, builder.db, builder.url, 182 | builder.params, builder.ids) 183 | } 184 | 185 | case class ByTypeBuilder[K: R, V: R, D: R]( 186 | builder: MDBuilder[IncludeDocs[D], MissingDisallowed, ForDocType[K, V, D]]) 187 | (implicit tag: ClassTag[D], kw: W[K]) { 188 | def build: QueryByType[K, V, D] = { 189 | val view = builder.view.getOrElse(builder.tempTypeFilterView) 190 | QueryByType(builder.client, builder.db, view, builder.typeMappings) 191 | } 192 | } 193 | 194 | private type BasicBuilder = Builder[CouchKeyVals[String, CouchDocRev], ExcludeDocs, 195 | MissingDisallowed] 196 | 197 | private type AllowMissingBuilder = Builder[CouchKeyValsIncludesMissing[String, CouchDocRev], 198 | ExcludeDocs, MissingAllowed] 199 | 200 | private type IncludeDocsBuilder[D] = Builder[CouchDocs[String, CouchDocRev, D], IncludeDocs[D], 201 | MissingDisallowed] 202 | 203 | private type AllowMissingIncludeDocsBuilder[D] = Builder[CouchDocsIncludesMissing[String, 204 | CouchDocRev, D], IncludeDocs[D], MissingAllowed] 205 | 206 | implicit def buildBasic(builder: MDBuilder[ExcludeDocs, MissingDisallowed, AnyDocType]): 207 | BasicBuilder = new BasicBuilder(builder) 208 | 209 | implicit def buildAllowMissing(builder: MDBuilder[ExcludeDocs, MissingAllowed, AnyDocType]): 210 | AllowMissingBuilder = new AllowMissingBuilder(builder) 211 | 212 | implicit def buildIncludeDocs[D: R]( 213 | builder: MDBuilder[IncludeDocs[D], MissingDisallowed, AnyDocType]): IncludeDocsBuilder[D] = 214 | new IncludeDocsBuilder(builder) 215 | 216 | implicit def buildIncludeDocsAllowMissing[D: R]( 217 | builder: MDBuilder[IncludeDocs[D], MissingAllowed, AnyDocType]): 218 | AllowMissingIncludeDocsBuilder[D] = new AllowMissingIncludeDocsBuilder(builder) 219 | 220 | implicit def buildByTypeIncludeDocs[K: R, V: R, D: R]( 221 | builder: MDBuilder[IncludeDocs[D], MissingDisallowed, ForDocType[K, V, D]]) 222 | (implicit tag: ClassTag[D], kw: W[K]): ByTypeBuilder[K, V, D] = { 223 | ByTypeBuilder(builder) 224 | } 225 | 226 | def apply(client: Client, db: String, typeMapping: TypeMapping): 227 | MDBuilder[ExcludeDocs, MissingDisallowed, AnyDocType] = 228 | new MDBuilder(client, db, typeMapping) 229 | } 230 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/builders/ListQueryBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api.builders 18 | 19 | import com.ibm.couchdb.Res 20 | import com.ibm.couchdb.core.Client 21 | import org.http4s.Status 22 | import upickle.default.Aliases.W 23 | import upickle.default.write 24 | 25 | import scalaz.concurrent.Task 26 | 27 | case class ListQueryBuilder( 28 | client: Client, 29 | db: String, 30 | design: String, 31 | list: String, 32 | params: Map[String, String] = Map.empty[String, String]) { 33 | 34 | def format(format: String): ListQueryBuilder = { 35 | set("format", format) 36 | } 37 | 38 | def descending(descending: Boolean = true): ListQueryBuilder = { 39 | set("descending", descending) 40 | } 41 | 42 | def endKey[K: W](endKey: K): ListQueryBuilder = { 43 | set("endkey", write(endKey)) 44 | } 45 | 46 | def endKeyDocId(endKeyDocId: String): ListQueryBuilder = { 47 | set("endkey_docid", endKeyDocId) 48 | } 49 | 50 | def group(group: Boolean = true): ListQueryBuilder = { 51 | set("group", group) 52 | } 53 | 54 | def groupLevel(groupLevel: Int): ListQueryBuilder = { 55 | set("group_level", groupLevel) 56 | } 57 | 58 | def inclusiveEnd(inclusiveEnd: Boolean = true): ListQueryBuilder = { 59 | set("inclusive_end", inclusiveEnd) 60 | } 61 | 62 | def key[K: W](key: K): ListQueryBuilder = { 63 | set("key", write(key)) 64 | } 65 | 66 | def limit(limit: Int): ListQueryBuilder = { 67 | set("limit", limit) 68 | } 69 | 70 | def reduce(reduce: Boolean = true): ListQueryBuilder = { 71 | set("reduce", reduce) 72 | } 73 | 74 | def skip(skip: Int): ListQueryBuilder = { 75 | set("skip", skip) 76 | } 77 | 78 | def stale(stale: String): ListQueryBuilder = { 79 | set("stale", stale) 80 | } 81 | 82 | def startKey[K: W](startKey: K): ListQueryBuilder = { 83 | set("startkey", write(startKey)) 84 | } 85 | 86 | def startKeyDocId(startKeyDocId: String): ListQueryBuilder = { 87 | set("startkey_docid", startKeyDocId) 88 | } 89 | 90 | def updateSeq(updateSeq: Boolean = true): ListQueryBuilder = { 91 | set("update_seq", updateSeq) 92 | } 93 | 94 | def addParam(name: String, value: String): ListQueryBuilder = { 95 | set(name, value) 96 | } 97 | 98 | private def set(key: String, value: String): ListQueryBuilder = { 99 | copy(params = params.updated(key, value)) 100 | } 101 | 102 | private def set(key: String, value: Any): ListQueryBuilder = { 103 | set(key, value.toString) 104 | } 105 | 106 | def query(view: String): Task[String] = { 107 | if (view.isEmpty) 108 | Res.Error("not_found", "View name must not be empty").toTask 109 | else 110 | client.getRaw( 111 | s"/$db/_design/$design/_list/$list/$view", 112 | Status.Ok, 113 | params.toSeq) 114 | } 115 | 116 | def queryAnotherDesign(view: String, anotherDesign: String): Task[String] = { 117 | if (view.isEmpty) 118 | Res.Error("not_found", "View name must not be empty").toTask 119 | else 120 | client.getRaw( 121 | s"/$db/_design/$anotherDesign/_list/$list/$view", 122 | Status.Ok, 123 | params.toSeq) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/builders/QueryOps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api.builders 18 | 19 | import com.ibm.couchdb.core.Client 20 | import com.ibm.couchdb.Req 21 | import org.http4s.Status 22 | import upickle.default.Aliases.{R, W} 23 | import upickle.default._ 24 | 25 | import scalaz.concurrent.Task 26 | 27 | case class QueryOps(client: Client) { 28 | 29 | def query[Q: R]( 30 | url: String, 31 | params: Map[String, String]): Task[Q] = { 32 | client.get[Q](url, Status.Ok, params.toSeq) 33 | } 34 | 35 | def queryByIds[K: W, Q: R]( 36 | url: String, 37 | ids: Seq[K], 38 | params: Map[String, String]): Task[Q] = { 39 | postQuery[Req.DocKeys[K], Q](url, Req.DocKeys(ids), params) 40 | } 41 | 42 | def postQuery[B: W, Q: R]( 43 | url: String, 44 | body: B, 45 | params: Map[String, String]): Task[Q] = { 46 | client.post[B, Q](url, Status.Ok, body, params.toSeq) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/builders/QueryStrategy.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api.builders 18 | 19 | import com.ibm.couchdb.api.Query 20 | import com.ibm.couchdb.core.Client 21 | import com.ibm.couchdb.{CouchDocs, CouchView, Req, Res, TypeMapping} 22 | import upickle.default.Aliases.{R, W} 23 | import upickle.default._ 24 | import com.ibm.couchdb._ 25 | 26 | import scala.reflect.ClassTag 27 | import scalaz.concurrent.Task 28 | 29 | case class QueryBasic[C: R]( 30 | client: Client, db: String, url: String, params: Map[String, String] = Map.empty, 31 | ids: Seq[String] = Seq.empty) { 32 | 33 | private val queryOps = QueryOps(client) 34 | 35 | def query: Task[C] = { 36 | ids match { 37 | case Nil => queryOps.query[C](url, params) 38 | case _ => queryOps.queryByIds[String, C](url, ids, params) 39 | } 40 | } 41 | } 42 | 43 | case class QueryView[K: W, C: R]( 44 | client: Client, db: String, design: Option[String], params: Map[String, String] = Map.empty, 45 | ids: Seq[K] = Seq.empty, view: Option[String], tempView: Option[CouchView]) { 46 | 47 | private lazy val url = (view, design) match { 48 | case (Some(v), Some(d)) => s"/$db/_design/$d/_view/$v" 49 | case _ => s"/$db/_temp_view" 50 | } 51 | 52 | private val queryOps = QueryOps(client) 53 | 54 | def query: Task[C] = { 55 | ids match { 56 | case Nil => queryWithoutIds 57 | case _ => queryByIds 58 | } 59 | } 60 | 61 | private def queryWithoutIds: Task[C] = tempView match { 62 | case Some(t) => queryOps.postQuery[CouchView, C](url, t, params) 63 | case None => queryOps.query[C](url, params) 64 | } 65 | 66 | private def queryByIds: Task[C] = tempView match { 67 | case Some(t) => queryOps.postQuery[Req.ViewWithKeys[K], C]( 68 | url, Req.ViewWithKeys(ids, t), params) 69 | case None => queryOps.queryByIds[K, C](url, ids, params) 70 | } 71 | } 72 | 73 | case class QueryByType[K, V, D: R]( 74 | client: Client, db: String, typeFilterView: CouchView, 75 | typeMappings: TypeMapping, params: Map[String, String] = Map.empty, 76 | ids: Seq[String] = Seq.empty) 77 | (implicit tag: ClassTag[D], kr: R[K], kw: W[K], vr: R[V]) { 78 | 79 | def query: Task[CouchDocs[K, V, D]] = { 80 | typeMappings.get(tag.runtimeClass) match { 81 | case Some(k) => queryByType(typeFilterView, k.name) 82 | case None => Res.Error("not_found", s"type mapping not found").toTask 83 | } 84 | } 85 | 86 | private def queryByType(view: CouchView, kind: String, ps: Map[String, String] = Map.empty) = { 87 | new Query(client, db).temporaryView[K, V](view) match { 88 | case Some(v) => v.startKey(Tuple1(kind)).endKey(Tuple2(kind, {})).includeDocs[D].build.query 89 | case None => Res.Error("not_found", "invalid view specified").toTask 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/builders/ShowQueryBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api.builders 18 | 19 | import com.ibm.couchdb.Res 20 | import com.ibm.couchdb.core.Client 21 | import org.http4s.Status 22 | 23 | import scalaz.concurrent.Task 24 | 25 | case class ShowQueryBuilder( 26 | client: Client, 27 | db: String, 28 | design: String, 29 | show: String, 30 | params: Map[String, String] = Map.empty[String, String]) { 31 | 32 | def details(details: Boolean = true): ShowQueryBuilder = { 33 | set("details", details) 34 | } 35 | 36 | def format(format: String): ShowQueryBuilder = { 37 | set("format", format) 38 | } 39 | 40 | def addParam(name: String, value: String): ShowQueryBuilder = { 41 | set(name, value) 42 | } 43 | 44 | private def set(key: String, value: String): ShowQueryBuilder = { 45 | copy(params = params.updated(key, value)) 46 | } 47 | 48 | private def set(key: String, value: Any): ShowQueryBuilder = { 49 | set(key, value.toString) 50 | } 51 | 52 | def query: Task[String] = { 53 | client.getRaw( 54 | s"/$db/_design/$design/_show/$show", 55 | Status.Ok, 56 | params.toSeq) 57 | } 58 | 59 | def query(id: String): Task[String] = { 60 | if (id.isEmpty) 61 | Res.Error("not_found", "Document ID must not be empty").toTask 62 | else 63 | client.getRaw( 64 | s"/$db/_design/$design/_show/$show/$id", 65 | Status.Ok, 66 | params.toSeq) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/api/builders/ViewQueryBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api.builders 18 | 19 | import com.ibm.couchdb._ 20 | import com.ibm.couchdb.core.Client 21 | import upickle.default.Aliases.{R, W} 22 | import upickle.default.write 23 | 24 | sealed trait ViewOperation 25 | abstract class MapWithReduce[A: R] extends ViewOperation 26 | trait MapOnly extends ViewOperation 27 | 28 | case class ViewQueryBuilder[K: R, V: R, DR <: DocsInResult, MR <: ViewOperation] private[builders]( 29 | client: Client, 30 | db: String, 31 | design: Option[String], 32 | view: Option[String], 33 | temporaryView: Option[CouchView] = None, 34 | params: Map[String, String] = Map.empty[String, String], 35 | ids: Seq[K] = Seq.empty)(implicit kw: W[K], ckr: R[CouchKeyVals[K, V]]) { 36 | 37 | def conflicts(conflicts: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = { 38 | set("conflicts", conflicts) 39 | } 40 | 41 | def descending(descending: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = { 42 | set("descending", descending) 43 | } 44 | 45 | def endKey[K2: W](endKey: K2): ViewQueryBuilder[K, V, DR, MR] = { 46 | set("endkey", write(endKey)) 47 | } 48 | 49 | def endKeyDocId(endKeyDocId: String): ViewQueryBuilder[K, V, DR, MR] = { 50 | set("endkey_docid", endKeyDocId) 51 | } 52 | 53 | def group(group: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = { 54 | set("group", group) 55 | } 56 | 57 | def groupLevel(groupLevel: Int): ViewQueryBuilder[K, V, DR, MR] = { 58 | set("group_level", groupLevel) 59 | } 60 | 61 | def includeDocs[D: R]: ViewQueryBuilder[K, V, IncludeDocs[D], MR] = { 62 | set("include_docs", true) 63 | } 64 | 65 | def excludeDocs: ViewQueryBuilder[K, V, ExcludeDocs, MR] = { 66 | set("include_docs", false) 67 | } 68 | 69 | def attachments(attachments: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = { 70 | set("attachments", attachments) 71 | } 72 | 73 | def attEncodingInfo(attEncodingInfo: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = { 74 | set("att_encoding_info", attEncodingInfo) 75 | } 76 | 77 | def inclusiveEnd(inclusiveEnd: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = { 78 | set("inclusive_end", inclusiveEnd) 79 | } 80 | 81 | def key[D: W](key: D): ViewQueryBuilder[K, V, DR, MR] = { 82 | set("key", write(key)) 83 | } 84 | 85 | def limit(limit: Int): ViewQueryBuilder[K, V, DR, MR] = { 86 | set("limit", limit) 87 | } 88 | 89 | def reduce[A: R]: ViewQueryBuilder[K, V, DR, MapWithReduce[A]] = { 90 | set("reduce", true) 91 | } 92 | 93 | def noReduce: ViewQueryBuilder[K, V, DR, MapOnly] = { 94 | set("reduce", false) 95 | } 96 | 97 | def withIds(ids: Seq[K]): ViewQueryBuilder[K, V, DR, MR] = { 98 | set(params, ids) 99 | } 100 | 101 | def skip(skip: Int): ViewQueryBuilder[K, V, DR, MR] = { 102 | set("skip", skip) 103 | } 104 | 105 | def stale(stale: String): ViewQueryBuilder[K, V, DR, MR] = { 106 | set("stale", stale) 107 | } 108 | 109 | def startKey[K2: W](startKey: K2): ViewQueryBuilder[K, V, DR, MR] = { 110 | set("startkey", write(startKey)) 111 | } 112 | 113 | def startKeyDocId(startKeyDocId: String): ViewQueryBuilder[K, V, DR, MR] = { 114 | set("startkey_docid", startKeyDocId) 115 | } 116 | 117 | def updateSeq(updateSeq: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = { 118 | set("update_seq", updateSeq) 119 | } 120 | 121 | private def set[I <: DocsInResult, M <: ViewOperation](key: String, value: String): 122 | ViewQueryBuilder[K, V, I, M] = { 123 | set(params.updated(key, value), ids) 124 | } 125 | 126 | private def set[I <: DocsInResult, M <: ViewOperation](key: String, value: Any): 127 | ViewQueryBuilder[K, V, I, M] = { 128 | set(key, value.toString) 129 | } 130 | 131 | private def set[I <: DocsInResult, M <: ViewOperation]( 132 | _params: Map[String, String], 133 | _ids: Seq[K]): ViewQueryBuilder[K, V, I, M] = { 134 | new ViewQueryBuilder(client, db, design, view, temporaryView, _params, _ids) 135 | } 136 | } 137 | 138 | object ViewQueryBuilder { 139 | 140 | private type VBuilder[K, V, ID <: DocsInResult, MR <: ViewOperation] = 141 | ViewQueryBuilder[K, V, ID, MR] 142 | 143 | case class Builder[K: R, V: R, T: R, ID <: DocsInResult, MR <: ViewOperation]( 144 | builder: VBuilder[K, V, ID, MR])(implicit kr: W[K], cdr: R[CouchKeyVals[K, V]]) { 145 | def build: QueryView[K, T] = QueryView( 146 | builder.client, builder.db, builder.design, 147 | builder.params, builder.ids, builder.view, builder.temporaryView) 148 | } 149 | 150 | private type BasicBuilder[K, V] = Builder[K, V, CouchKeyVals[K, V], ExcludeDocs, MapOnly] 151 | 152 | private type MapWithReduceBuilder[K, V, A] = Builder[K, V, CouchReducedKeyVals[K, A], 153 | ExcludeDocs, MapWithReduce[A]] 154 | 155 | private type IncludeDocsBuilder[K, V, D] = Builder[K, V, CouchDocs[K, V, D], IncludeDocs[D], 156 | MapOnly] 157 | 158 | implicit def buildBasic[K: R, V: R]( 159 | builder: VBuilder[K, V, ExcludeDocs, MapOnly])(implicit kw: W[K]): 160 | Builder[K, V, CouchKeyVals[K, V], ExcludeDocs, MapOnly] = 161 | new BasicBuilder(builder) 162 | 163 | implicit def buildReduced[K: R, V: R, A: R]( 164 | builder: VBuilder[K, V, ExcludeDocs, MapWithReduce[A]])(implicit kw: W[K]): 165 | Builder[K, V, CouchReducedKeyVals[K, A], ExcludeDocs, MapWithReduce[A]] = { 166 | new MapWithReduceBuilder(if (builder.ids.isEmpty) builder else builder.group()) 167 | } 168 | 169 | implicit def includeDocsBuilder[K: R, V: R, D: R]( 170 | builder: VBuilder[K, V, IncludeDocs[D], MapOnly])(implicit kw: W[K]): 171 | Builder[K, V, CouchDocs[K, V, D], IncludeDocs[D], MapOnly] = new IncludeDocsBuilder(builder) 172 | 173 | def apply[K: R, V: R](client: Client, db: String, design: String, view: String) 174 | (implicit kw: W[K]): ViewQueryBuilder[K, V, ExcludeDocs, MapOnly] = { 175 | new ViewQueryBuilder(client, db, design = Option(design), view = Option(view)) 176 | } 177 | 178 | def apply[K: R, V: R](client: Client, db: String, view: CouchView)(implicit kw: W[K]): 179 | ViewQueryBuilder[K, V, ExcludeDocs, MapOnly] = { 180 | new ViewQueryBuilder(client, db, design = None, view = None, temporaryView = Option(view)) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/core/Client.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation, Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.core 18 | 19 | import com.ibm.couchdb._ 20 | import org.http4s.Http4s._ 21 | import org.http4s.Method._ 22 | import org.http4s._ 23 | import org.http4s.client.blaze.PooledHttp1Client 24 | import org.http4s.headers.Authorization 25 | import org.http4s.util.CaseInsensitiveString 26 | import scodec.bits.ByteVector 27 | import upickle.default.Aliases.{R, W} 28 | import upickle.default.{read, write} 29 | 30 | import scalaz.Scalaz._ 31 | import scalaz.concurrent.Task 32 | 33 | class Client(config: Config) { 34 | 35 | private val log = org.log4s.getLogger 36 | 37 | val client = PooledHttp1Client() 38 | 39 | val baseHeaders = config.credentials match { 40 | case Some(x) => Headers(Authorization(BasicCredentials(x._1, x._2))) 41 | case None => Headers() 42 | } 43 | 44 | val baseHeadersWithAccept = baseHeaders.put(Header("Accept", "application/json; charset=utf-8")) 45 | 46 | val baseUri = Uri( 47 | scheme = CaseInsensitiveString(if (config.https) "https" else "http").some, 48 | authority = Uri.Authority( 49 | host = Uri.IPv4(address = config.host), 50 | port = config.port.some 51 | ).some) 52 | 53 | def url(resource: String, params: Seq[(String, String)] = Seq.empty[(String, String)]): Uri = { 54 | baseUri.copy(path = resource).setQueryParams( 55 | params.map(x => (x._1, Seq(x._2))).toMap) 56 | } 57 | 58 | def req(request: Request, expectedStatus: Status): Task[Response] = { 59 | log.debug(s"Making a request $request") 60 | client.toHttpService.run(request) flatMap { response => 61 | log.debug(s"Received response $response") 62 | if (response.status == expectedStatus) { 63 | Task.now(response) 64 | } else { 65 | log.warn(s"Unexpected response status ${response.status}, expected $expectedStatus") 66 | for { 67 | responseBody <- response.as[String] 68 | requestBody <- EntityDecoder.decodeString(request) 69 | errorRaw = read[Res.Error](responseBody) 70 | error = errorRaw.copy( 71 | status = response.status, 72 | request = request.toString, 73 | requestBody = requestBody 74 | ) 75 | _ = log.warn(s"Request error $error") 76 | fail <- Task.fail(CouchException[Res.Error](error)) 77 | } yield fail 78 | } 79 | } 80 | } 81 | 82 | def reqAndRead[T: R](request: Request, expectedStatus: Status): Task[T] = { 83 | for { 84 | response <- req(request, expectedStatus) 85 | asString <- response.as[String] 86 | } yield read[T](asString) 87 | } 88 | 89 | def getRaw( 90 | resource: String, 91 | expectedStatus: Status, 92 | params: Seq[(String, String)] = Seq.empty[(String, String)]): Task[String] = { 93 | val request = Request( 94 | method = GET, 95 | uri = url(resource, params), 96 | headers = baseHeadersWithAccept) 97 | req(request, expectedStatus).as[String] 98 | } 99 | 100 | def get[T: R]( 101 | resource: String, 102 | expectedStatus: Status, 103 | params: Seq[(String, String)] = Seq.empty[(String, String)]): Task[T] = { 104 | val request = Request( 105 | method = GET, 106 | uri = url(resource, params), 107 | headers = baseHeadersWithAccept) 108 | reqAndRead[T](request, expectedStatus) 109 | } 110 | 111 | def getBinary(resource: String, expectedStatus: Status): Task[Array[Byte]] = { 112 | val request = Request( 113 | method = GET, 114 | uri = url(resource)) 115 | req(request, expectedStatus).as[ByteVector].map(_.toArray) 116 | } 117 | 118 | private def put[T: R]( 119 | resource: String, 120 | expectedStatus: Status, 121 | entity: EntityEncoder.Entity, 122 | contentType: String): Task[T] = { 123 | val headers = 124 | if (!contentType.isEmpty) baseHeadersWithAccept.put(Header("Content-Type", contentType)) 125 | else baseHeadersWithAccept 126 | val request = Request( 127 | method = PUT, 128 | uri = url(resource), 129 | headers = headers, 130 | body = entity.body) 131 | reqAndRead[T](request, expectedStatus) 132 | } 133 | 134 | def put[B: W, T: R]( 135 | resource: String, 136 | expectedStatus: Status, 137 | body: B): Task[T] = { 138 | EntityEncoder[String].toEntity(write(body)) flatMap { entity => 139 | put[T](resource, expectedStatus, entity, "") 140 | } 141 | } 142 | 143 | def put[T: R]( 144 | resource: String, 145 | expectedStatus: Status, 146 | body: Array[Byte] = Array(), 147 | contentType: String = ""): Task[T] = { 148 | EntityEncoder[Array[Byte]].toEntity(body) flatMap { entity => 149 | put[T](resource, expectedStatus, entity, contentType) 150 | } 151 | } 152 | 153 | def post[B: W, T: R]( 154 | resource: String, 155 | expectedStatus: Status, 156 | body: B, 157 | params: Seq[(String, String)] = Seq.empty[(String, String)]): Task[T] = { 158 | EntityEncoder[String].toEntity(write(body)) flatMap { entity => 159 | val request = Request( 160 | method = POST, 161 | uri = url(resource, params), 162 | headers = baseHeadersWithAccept.put( 163 | Header("Content-Type", "application/json")), 164 | body = entity.body) 165 | reqAndRead[T](request, expectedStatus) 166 | } 167 | } 168 | 169 | def delete[T: R](resource: String, expectedStatus: Status): Task[T] = { 170 | val request = Request( 171 | method = DELETE, 172 | uri = url(resource), 173 | headers = baseHeadersWithAccept) 174 | reqAndRead[T](request, expectedStatus) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/implicits/TaskImplicits.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.implicits 18 | 19 | import com.ibm.couchdb.Res 20 | 21 | import scalaz.concurrent.Task 22 | 23 | trait TaskImplicits { 24 | 25 | implicit class TaskOps[T](task: Task[T]) { 26 | def ignoreError: Task[Res.Ok] = { 27 | task.attempt.map(_ => Res.Ok()) 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/implicits/UpickleImplicits.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.implicits 18 | 19 | import com.ibm.couchdb._ 20 | import org.http4s.Status 21 | import upickle.Js.Value 22 | import upickle.default.Aliases.{R, W} 23 | import upickle.default.{Reader => Rr, Writer => Wr, readJs => rJs, writeJs => wJs} 24 | import upickle.{Js, Types} 25 | 26 | import scala.util.Try 27 | import scalaz.{-\/, \/, \/-} 28 | 29 | trait UpickleImplicits extends Types { 30 | 31 | implicit val statusW: W[Status] = Wr[Status] { 32 | x => Js.Num(x.code.toDouble) 33 | } 34 | 35 | implicit val statusR: R[Status] = Rr[Status] { 36 | case json: Js.Num => Status.fromInt(json.value.toInt).toOption.get 37 | } 38 | 39 | implicit def dockViewWithKeysW[K: W]: W[Req.ViewWithKeys[K]] = Wr[Req.ViewWithKeys[K]] { 40 | case Req.ViewWithKeys(keys, CouchView(map, reduce)) => 41 | Js.Obj(mapReduceParams(map, reduce) ++ Seq("keys" -> wJs(keys)): _*) 42 | } 43 | 44 | implicit val couchViewW: W[CouchView] = Wr[CouchView] { 45 | case CouchView(map, reduce) => Js.Obj(mapReduceParams(map, reduce): _*) 46 | } 47 | 48 | private def mapReduceParams(map: String, reduce: String = ""): Seq[(String, Value)] = { 49 | val m = Seq("map" -> wJs(map)) 50 | if (reduce.isEmpty) m else m ++ Seq("reduce" -> wJs(reduce)) 51 | } 52 | 53 | implicit def couchKeyValOrErrorR[K: R, V: R]: Rr[\/[CouchKeyError[K], CouchKeyVal[K, V]]] = Rr { 54 | case o: Js.Obj => Try(\/-(rJs[CouchKeyVal[K, V]](o))).getOrElse(-\/(rJs[CouchKeyError[K]](o))) 55 | } 56 | 57 | implicit def couchKeyValDocOrErrorR[K: R, V: R, D: R]: Rr[\/[CouchKeyError[K], 58 | CouchKeyValWithDoc[K, V, D]]] = Rr { 59 | case o: Js.Obj => Try(\/-(rJs[CouchKeyValWithDoc[K, V, D]](o))).getOrElse( 60 | -\/(rJs[CouchKeyError[K]](o))) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/com/ibm/couchdb/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm 18 | 19 | import com.ibm.couchdb.implicits.{TaskImplicits, UpickleImplicits} 20 | 21 | package object couchdb extends TaskImplicits with UpickleImplicits {} 22 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/BasicAuthSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb 18 | 19 | import com.ibm.couchdb.spec.{CouchDbSpecification, SpecConfig} 20 | import org.http4s.Status 21 | 22 | class BasicAuthSpec extends CouchDbSpecification { 23 | 24 | val couch = CouchDb(SpecConfig.couchDbHost, SpecConfig.couchDbPort) 25 | val couchAdmin = CouchDb( 26 | SpecConfig.couchDbHost, 27 | SpecConfig.couchDbPort, 28 | https = false, 29 | SpecConfig.couchDbUsername, 30 | SpecConfig.couchDbPassword) 31 | 32 | val db = "couchdb-scala-basic-auth-spec" 33 | val adminUrl = s"/_config/admins/${SpecConfig.couchDbUsername}" 34 | 35 | "Basic authentication" >> { 36 | 37 | "Only admin can create and delete databases" >> { 38 | awaitRight( 39 | couch.client.put[String, String]( 40 | adminUrl, Status.Ok, SpecConfig.couchDbPassword)) mustEqual "" 41 | awaitError(couch.dbs.create(db), "unauthorized") 42 | await(couchAdmin.dbs.delete(db)) 43 | awaitOk(couchAdmin.dbs.create(db)) 44 | awaitDocOk(couch.db(db, typeMapping).docs.create(fixAlice)) 45 | awaitDocOk(couchAdmin.db(db, typeMapping).docs.create(fixAlice)) 46 | awaitRight(couchAdmin.client.delete[String](adminUrl, Status.Ok)) must not be empty 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/CouchDbSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb 18 | 19 | import com.ibm.couchdb.api.{Design, Documents} 20 | import com.ibm.couchdb.spec.{CouchDbSpecification, SpecConfig} 21 | import org.http4s.Status 22 | import org.specs2.matcher.MatchResult 23 | 24 | class CouchDbSpec extends CouchDbSpecification { 25 | 26 | val couch = CouchDb(SpecConfig.couchDbHost, SpecConfig.couchDbPort) 27 | 28 | val db1 = "couchdb-scala-couchdb-spec1" 29 | val db2 = "couchdb-scala-couchdb-spec2" 30 | 31 | "User interface" >> { 32 | 33 | "Get info about the DB instance" >> { 34 | awaitRight(couch.server.info).couchdb mustEqual "Welcome" 35 | } 36 | 37 | "Create and query 2 DBs" >> { 38 | def testDb(dbName: String): MatchResult[Seq[CouchKeyVal[String, String]]] = { 39 | await(couch.dbs.delete(dbName)) 40 | val error = awaitLeft(couch.dbs.delete(dbName)) 41 | error.error mustEqual "not_found" 42 | error.reason mustEqual "missing" 43 | error.status mustEqual Status.NotFound 44 | error.request must contain("DELETE") 45 | error.request must contain(dbName) 46 | error.requestBody must beEmpty 47 | 48 | awaitOk(couch.dbs.create(dbName)) 49 | couch.db(dbName, typeMapping).name mustEqual dbName 50 | couch.db(dbName, typeMapping).docs must beAnInstanceOf[Documents] 51 | couch.db(dbName, typeMapping).design must beAnInstanceOf[Design] 52 | 53 | val db = couch.db(dbName, typeMapping) 54 | db must beTheSameAs(couch.db(dbName, typeMapping)) 55 | awaitDocOk(db.docs.create(fixAlice)) 56 | awaitDocOk(db.design.create(fixDesign)) 57 | 58 | val docs = awaitRight( 59 | db.query.view[String, String](fixDesign.name, FixViews.names).get.build.query) 60 | docs.rows must haveLength(1) 61 | } 62 | testDb(db1) 63 | testDb(db2) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.spec.CouchDbSpecification 20 | 21 | class DatabasesSpec extends CouchDbSpecification { 22 | 23 | val db = "couchdb-scala-databases-spec" 24 | val databases = new Databases(client) 25 | 26 | private def clear() = await(databases.delete(db)) 27 | 28 | "Databases API" >> { 29 | 30 | "Create a DB" >> { 31 | clear() 32 | awaitOk(databases.create(db)) 33 | awaitError(databases.create(db), "file_exists") 34 | } 35 | 36 | "Get a DB" >> { 37 | clear() 38 | awaitOk(databases.create(db)) 39 | val info = awaitRight(databases.get(db)) 40 | info.db_name mustEqual db 41 | info.doc_count mustEqual 0 42 | info.doc_del_count mustEqual 0 43 | } 44 | 45 | "Get all DBs" >> { 46 | awaitRight(databases.getAll) 47 | await(databases.create(db)) 48 | awaitRight(databases.getAll) must contain(db) 49 | } 50 | 51 | "Delete a DB" >> { 52 | clear() 53 | awaitError(databases.delete(db), "not_found") 54 | awaitOk(databases.create(db)) 55 | awaitOk(databases.delete(db)) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/api/DesignSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.spec.CouchDbSpecification 20 | 21 | class DesignSpec extends CouchDbSpecification { 22 | 23 | val db = "couchdb-scala-design-spec" 24 | val databases = new Databases(client) 25 | val design = new Design(client, db) 26 | val documents = new Documents(client, db, typeMapping) 27 | 28 | private def clear() = recreateDb(databases, db) 29 | 30 | "Design API" >> { 31 | 32 | "Create a design document" >> { 33 | clear() 34 | awaitDocOk(design.create(fixDesign), s"_design/${fixDesign.name}") 35 | } 36 | 37 | "Get info about a design document" >> { 38 | clear() 39 | awaitDocOk(design.create(fixDesign)) 40 | val info = awaitRight(design.info(fixDesign.name)) 41 | info.name mustEqual fixDesign.name 42 | info.view_index.language mustEqual "javascript" 43 | info.view_index.disk_size must beGreaterThan(0) 44 | } 45 | 46 | "Get a design document" >> { 47 | clear() 48 | awaitDocOk(design.create(fixDesign)) 49 | val doc = awaitRight(design.get(fixDesign.name)) 50 | doc._id mustEqual s"_design/${fixDesign.name}" 51 | doc._rev must beRev 52 | doc.views must haveKey("names") 53 | } 54 | 55 | "Get a design document by ID" >> { 56 | clear() 57 | val created = awaitRight(design.create(fixDesign)) 58 | val doc = awaitRight(design.getById(created.id)) 59 | doc._id mustEqual s"_design/${fixDesign.name}" 60 | doc._rev must beRev 61 | doc.views must haveKey("names") 62 | } 63 | 64 | "Update a design document" >> { 65 | clear() 66 | awaitDocOk(design.create(fixDesign)) 67 | val initial = awaitRight(design.get(fixDesign.name)) 68 | val docOk = awaitRight(design.update(initial.copy(language = "typescript"))) 69 | checkDocOk(docOk, s"_design/${fixDesign.name}") 70 | val updated = awaitRight(design.get(fixDesign.name)) 71 | updated._rev mustEqual docOk.rev 72 | updated._id mustEqual initial._id 73 | updated.views mustEqual initial.views 74 | updated.language mustEqual "typescript" 75 | } 76 | 77 | "Delete a design document" >> { 78 | clear() 79 | awaitDocOk(design.create(fixDesign)) 80 | val doc = awaitRight(design.get(fixDesign.name)) 81 | awaitDocOk(design.delete(doc), s"_design/${fixDesign.name}") 82 | } 83 | 84 | "Delete a design document by name" >> { 85 | clear() 86 | awaitDocOk(design.create(fixDesign)) 87 | awaitDocOk(design.deleteByName(fixDesign.name), s"_design/${fixDesign.name}") 88 | } 89 | 90 | "Attach a byte array to a design" >> { 91 | clear() 92 | awaitDocOk(design.create(fixDesign)) 93 | val doc = awaitRight(design.get(fixDesign.name)) 94 | awaitDocOk(design.attach(doc, fixAttachmentName, fixAttachmentData), doc._id) 95 | } 96 | 97 | "Get a byte array attachment" >> { 98 | clear() 99 | awaitDocOk(design.create(fixDesign)) 100 | val doc = awaitRight(design.get(fixDesign.name)) 101 | awaitDocOk(design.attach(doc, fixAttachmentName, fixAttachmentData), doc._id) 102 | awaitRight(design.getAttachment(doc, fixAttachmentName)) mustEqual fixAttachmentData 103 | } 104 | 105 | "Get a design with attachment stubs" >> { 106 | clear() 107 | awaitDocOk(design.create(fixDesign)) 108 | val designDoc = awaitRight(design.get(fixDesign.name)) 109 | awaitDocOk( 110 | design.attach(designDoc, fixAttachmentName, fixAttachmentData, fixAttachmentContentType), 111 | designDoc._id) 112 | val doc = awaitRight(design.get(fixDesign.name)) 113 | doc._id mustEqual designDoc._id 114 | doc._attachments must haveLength(1) 115 | doc._attachments must haveKey(fixAttachmentName) 116 | val meta = doc._attachments(fixAttachmentName) 117 | meta.content_type mustEqual fixAttachmentContentType 118 | meta.length mustEqual fixAttachmentData.length 119 | meta.stub mustEqual true 120 | meta.digest must not be empty 121 | } 122 | 123 | "Get a design with attachments inline" >> { 124 | clear() 125 | awaitDocOk(design.create(fixDesign)) 126 | val designDoc = awaitRight(design.get(fixDesign.name)) 127 | awaitDocOk( 128 | design.attach(designDoc, fixAttachmentName, fixAttachmentData, fixAttachmentContentType), 129 | designDoc._id) 130 | val doc = awaitRight(design.getWithAttachments(fixDesign.name)) 131 | doc._id mustEqual designDoc._id 132 | doc._attachments must haveLength(1) 133 | doc._attachments must haveKey(fixAttachmentName) 134 | val attachment = doc._attachments(fixAttachmentName) 135 | attachment.content_type mustEqual fixAttachmentContentType 136 | attachment.length mustEqual -1 137 | attachment.stub mustEqual false 138 | attachment.digest must not be empty 139 | attachment.toBytes mustEqual fixAttachmentData 140 | } 141 | 142 | "Delete an attachment to a design" >> { 143 | clear() 144 | awaitDocOk(design.create(fixDesign)) 145 | val doc = awaitRight(design.get(fixDesign.name)) 146 | val attachment = awaitRight(design.attach(doc, fixAttachmentName, fixAttachmentData)) 147 | val docWithAttachment = awaitRight(design.get(fixDesign.name)) 148 | awaitDocOk(design.deleteAttachment(docWithAttachment, fixAttachmentName), attachment.id) 149 | awaitError(design.getAttachment(doc, fixAttachmentName), "not_found") 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation, Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.Res.DocOk 20 | import com.ibm.couchdb._ 21 | import com.ibm.couchdb.spec.{CouchDbSpecification, SpecConfig} 22 | import org.http4s.Status 23 | import org.specs2.matcher.MatchResult 24 | 25 | class DocumentsSpec extends CouchDbSpecification { 26 | 27 | val db = "couchdb-scala-documents-spec" 28 | val server = new Server(client) 29 | val databases = new Databases(client) 30 | val documents = new Documents(client, db, typeMapping) 31 | 32 | private def clear() = recreateDb(databases, db) 33 | 34 | clear() 35 | 36 | "Documents API" >> { 37 | 38 | "Create a document with an auto-generated UUID" >> { 39 | awaitDocOk(documents.create(fixAlice)) 40 | } 41 | 42 | "Create a document with a specified UUID" >> { 43 | val uuid = awaitRight(server.mkUuid) 44 | awaitDocOk(documents.create(fixAlice, uuid), uuid) 45 | } 46 | 47 | "Get a document by a UUID" >> { 48 | val uuid = awaitRight(server.mkUuid) 49 | awaitRight(documents.create(fixAlice, uuid)) 50 | awaitRight(documents.get[FixPerson](uuid)).doc mustEqual fixAlice 51 | val aliceRes = awaitRight(documents.get[FixPerson](uuid)) 52 | aliceRes.doc.name mustEqual fixAlice.name 53 | aliceRes.doc.age mustEqual fixAlice.age 54 | aliceRes._id mustEqual uuid 55 | aliceRes._rev must beRev 56 | } 57 | 58 | "Get a document by a non-existent UUID" >> { 59 | val uuid = awaitRight(server.mkUuid) 60 | awaitError(documents.get[FixPerson](uuid), "not_found") 61 | awaitError(documents.get[FixPerson](""), "not_found") 62 | } 63 | 64 | "Create multiple documents in bulk" >> { 65 | clear() 66 | val res = awaitRight(documents.createMany(Seq(fixAlice, fixBob))) 67 | res must haveLength(2) 68 | checkDocOk(res.head) 69 | checkDocOk(res(1)) 70 | } 71 | 72 | "Create multiple documents in bulk with IDs" >> { 73 | clear() 74 | val docs = Map("1" -> fixAlice, "2" -> fixBob) 75 | val res = awaitRight(documents.createMany(docs)) 76 | res must haveLength(docs.size) 77 | res.foreach(checkDocOk) 78 | res.map(_.id) mustEqual docs.keys.toList 79 | val created = awaitRight(documents.getMany[FixPerson](docs.keys.toList)) 80 | created.getDocs.map(_.doc) mustEqual docs.values.toSeq 81 | created.rows.map(_.id) mustEqual docs.keys.toSeq 82 | } 83 | 84 | "Get all documents" >> { 85 | def verify(docs: CouchKeyVals[String, CouchDocRev], expected: Seq[Res.DocOk]): 86 | MatchResult[Seq[String]] = { 87 | docs.offset mustEqual 0 88 | docs.total_rows must beEqualTo(expected.size) 89 | docs.rows must haveLength(expected.size) 90 | docs.rows.map(_.id) must containTheSameElementsAs(expected.map(_.id)) 91 | } 92 | clear() 93 | val created = Seq( 94 | awaitRight(documents.create(fixAlice)), awaitRight(documents.create(fixAlice))) 95 | val docs = awaitRight(documents.getMany.build.query) 96 | val docsWithExcludeDocs = awaitRight(documents.getMany.excludeDocs.build.query) 97 | verify(docs, created) 98 | verify(docsWithExcludeDocs, created) 99 | } 100 | 101 | "Get multiple documents by IDs" >> { 102 | def verify( 103 | docs: CouchKeyVals[String, CouchDocRev], created: Seq[Res.DocOk], 104 | expected: Seq[Res.DocOk]): MatchResult[Any] = { 105 | docs.offset mustEqual 0 106 | docs.total_rows must beEqualTo(created.size) 107 | docs.rows must haveLength(expected.size) 108 | docs.rows.map(_.id) mustEqual expected.map(_.id) 109 | } 110 | clear() 111 | val created = Seq(fixAlice, fixCarl, fixBob).map(x => awaitRight(documents.create(x))) 112 | val expected = created.take(2) 113 | verify( 114 | awaitRight(documents.getMany.withIds(expected.map(_.id)).build.query), created, expected) 115 | verify( 116 | awaitRight( 117 | documents.getMany.disallowMissing.excludeDocs.withIds(expected.map(_.id)).build.query), 118 | created, expected) 119 | } 120 | 121 | "Get multiple documents by IDs with some missing" >> { 122 | def verify( 123 | docs: CouchKeyValsIncludesMissing[String, CouchDocRev], missing: Seq[String], 124 | existing: Seq[String]): MatchResult[Any] = { 125 | docs.offset mustEqual 0 126 | docs.rows must haveLength(missing.length + existing.length) 127 | docs.rows.flatMap(_.toOption).map(_.id).toList mustEqual existing 128 | docs.rows.flatMap(_.swap.toOption).map(_.key).toList mustEqual missing 129 | } 130 | clear() 131 | val fixPersons = Seq(fixAlice, fixBob, fixCarl) 132 | val createdPersons = fixPersons.map(person => awaitRight(documents.create(person))) 133 | val missingIds = Seq("non-existent-id-1", "non-existent-id-2") 134 | val existingIds = createdPersons.map(_.id) 135 | verify( 136 | awaitRight(documents.getMany.allowMissing.withIds(existingIds ++ missingIds).build.query), 137 | missingIds, existingIds) 138 | verify( 139 | awaitRight( 140 | documents.getMany.disallowMissing.allowMissing. 141 | withIds(existingIds ++ missingIds).build.query), missingIds, existingIds) 142 | } 143 | 144 | "Get all documents and include the doc data" >> { 145 | def verify( 146 | docs: CouchDocs[String, CouchDocRev, FixPerson], 147 | created: Seq[Res.DocOk], expected: Seq[FixPerson]): MatchResult[Any] = { 148 | docs.offset mustEqual 0 149 | docs.total_rows mustEqual created.size 150 | docs.rows must haveLength(expected.size) 151 | docs.rows.map(_.id) mustEqual created.map(_.id) 152 | docs.rows.map(_.doc.doc) mustEqual expected 153 | docs.getDocs.map(_.doc) mustEqual expected 154 | docs.getDocsData mustEqual expected 155 | } 156 | clear() 157 | val expected = Seq(fixAlice, fixBob) 158 | val created = expected.map(x => awaitRight(documents.create(x))) 159 | verify(awaitRight(documents.getMany.includeDocs[FixPerson].build.query), created, expected) 160 | verify( 161 | awaitRight(documents.getMany.excludeDocs.includeDocs[FixPerson].build.query), created, 162 | expected) 163 | } 164 | 165 | "Get all documents by type" >> { 166 | def verify( 167 | docs: CouchKeyVals[(String, String), String], created: Seq[DocOk], 168 | expected: Seq[FixXPerson]): MatchResult[Any] = { 169 | docs.total_rows must beGreaterThanOrEqualTo(created.size) 170 | docs.rows must haveLength(expected.size) 171 | docs.rows.map(_.value) mustEqual created.map(_.id) 172 | } 173 | clear() 174 | awaitRight(documents.createMany(Seq(fixAlice, fixBob))) 175 | val expected = Seq(fixProfessorX, fixMagneto) 176 | val createdXMenOnly = awaitRight(documents.createMany(expected)) 177 | verify( 178 | awaitRight( 179 | documents.getMany.byTypeUsingTemporaryView( 180 | typeMapping.get( 181 | classOf[FixXPerson]).get).build.query), createdXMenOnly, expected) 182 | } 183 | 184 | "Get all documents by type and include the doc data" >> { 185 | def verify( 186 | docs: CouchDocs[(String, String), String, FixXPerson], 187 | created: Seq[DocOk], expected: Seq[FixXPerson]): MatchResult[Any] = { 188 | docs.total_rows must beGreaterThanOrEqualTo(created.size) 189 | docs.rows must haveLength(expected.size) 190 | docs.rows.map(_.value) mustEqual created.map(_.id) 191 | docs.rows.map(_.doc.doc) mustEqual expected 192 | docs.getDocs.map(_.doc) mustEqual expected 193 | docs.getDocsData mustEqual expected 194 | } 195 | clear() 196 | awaitRight(documents.createMany(Seq(fixAlice, fixBob))) 197 | val expected = Seq(fixProfessorX, fixMagneto) 198 | val createdXMenOnly = awaitRight(documents.createMany(expected)) 199 | verify( 200 | awaitRight( 201 | documents.getMany.includeDocs[FixXPerson].byTypeUsingTemporaryView( 202 | typeMapping.get(classOf[FixXPerson]).get).build.query), createdXMenOnly, expected) 203 | } 204 | 205 | "Get all documents by type given a permanent type filter view" >> { 206 | def verify( 207 | docs: CouchKeyVals[_, String], created: Seq[DocOk], 208 | expected: Seq[FixXPerson]): MatchResult[Any] = { 209 | docs.total_rows must beGreaterThan(created.size) 210 | docs.rows must haveLength(expected.size) 211 | docs.rows.map(_.value) mustEqual created.map(_.id) 212 | } 213 | clear() 214 | val design = new Design(client, db) 215 | awaitRight(design.create(fixDesign)) 216 | awaitRight(documents.createMany(Seq(fixAlice, fixBob))) 217 | val expected = Seq(fixProfessorX, fixMagneto) 218 | val created = awaitRight(documents.createMany(expected)) 219 | val docs = awaitRight( 220 | documents.getMany.byType[String]( 221 | FixViews.typeFilter, fixDesign.name, 222 | typeMapping.get(classOf[FixXPerson]).get).build.query) 223 | verify(docs, created, expected) 224 | val docsCustom = awaitRight( 225 | documents.getMany.byType[(String, String, Int), String]( 226 | FixViews.typeFilterCustom, 227 | fixDesign.name, typeMapping.get(classOf[FixXPerson]).get).build.query) 228 | verify(docsCustom, created, expected) 229 | } 230 | 231 | "Get all documents by type and include the doc data, given a permanent type filter view" >> { 232 | def verify( 233 | docs: CouchDocs[_, String, FixXPerson], created: Seq[DocOk], 234 | expected: Seq[FixXPerson]): MatchResult[Any] = { 235 | docs.total_rows must beGreaterThan(created.size) 236 | docs.rows must haveLength(expected.size) 237 | docs.rows.map(_.value) mustEqual created.map(_.id) 238 | docs.rows.map(_.doc.doc) mustEqual expected 239 | docs.getDocs.map(_.doc) mustEqual expected 240 | docs.getDocsData mustEqual expected 241 | } 242 | clear() 243 | val design = new Design(client, db) 244 | awaitRight(design.create(fixDesign)) 245 | awaitRight(documents.createMany(Seq(fixAlice, fixBob))) 246 | val expected = Seq(fixProfessorX, fixMagneto) 247 | val created = awaitRight(documents.createMany(expected)) 248 | val docs = awaitRight( 249 | documents.getMany.includeDocs[FixXPerson].byType[String]( 250 | FixViews.typeFilter, fixDesign.name, 251 | typeMapping.get(classOf[FixXPerson]).get).build.query) 252 | verify(docs, created, expected) 253 | val docsCustom = awaitRight( 254 | documents.getMany.includeDocs[FixXPerson].byType[(String, String, Int), String]( 255 | FixViews 256 | .typeFilterCustom, fixDesign.name, typeMapping.get(classOf[FixXPerson]).get) 257 | .build.query) 258 | verify(docsCustom, created, expected) 259 | 260 | } 261 | 262 | "Get multiple documents by IDs and include the doc data" >> { 263 | def verify( 264 | docs: CouchDocs[String, CouchDocRev, FixPerson], created: Seq[Res.DocOk], 265 | expected: Seq[FixPerson]): MatchResult[Any] = { 266 | docs.offset mustEqual 0 267 | docs.total_rows must beGreaterThanOrEqualTo(3) 268 | docs.rows must haveLength(expected.size) 269 | docs.rows.map(_.id) mustEqual created.map(_.id) 270 | docs.rows.map(_.doc.doc) mustEqual expected 271 | docs.getDocs.map(_.doc) mustEqual expected 272 | docs.getDocsData mustEqual expected 273 | } 274 | clear() 275 | awaitRight(documents.create(fixBob)) 276 | val expected = Seq(fixAlice, fixCarl) 277 | val created: Seq[Res.DocOk] = expected.map(x => awaitRight(documents.create(x))) 278 | verify(awaitRight(documents.getMany[FixPerson](created.map(_.id))), created, expected) 279 | verify( 280 | awaitRight(documents.getMany.withIds(created.map(_.id)).includeDocs[FixPerson].build.query), 281 | created, expected) 282 | verify( 283 | awaitRight( 284 | documents.getMany.disallowMissing.withIds(created.map(_.id)).includeDocs[FixPerson]. 285 | build.query), created, expected) 286 | } 287 | 288 | "Get multiple documents by IDs with some missing and include the doc data" >> { 289 | def verify( 290 | docs: CouchDocsIncludesMissing[String, CouchDocRev, FixPerson], 291 | missing: Seq[String], existing: Seq[String], 292 | expected: Seq[FixPerson]): MatchResult[Any] = { 293 | docs.offset mustEqual 0 294 | docs.rows must haveLength(missing.length + existing.length) 295 | docs.rows.flatMap(_.toOption).map(_.id).toList mustEqual existing 296 | docs.rows.flatMap(_.toOption).map(_.doc.doc) mustEqual expected 297 | docs.getDocs.flatMap(_.toOption).map(_.doc) mustEqual expected 298 | docs.getDocsData mustEqual expected 299 | docs.rows.flatMap(_.swap.toOption).map(_.key).toList mustEqual missing 300 | } 301 | clear() 302 | val fixPersons = Seq(fixAlice, fixBob, fixCarl) 303 | val createdPersons = fixPersons.map(person => awaitRight(documents.create(person))) 304 | val missingIds = Seq("non-existent-id-1", "non-existent-id-2") 305 | val existingIds = createdPersons.map(_.id) 306 | val docs = awaitRight( 307 | documents.getMany.includeDocs[FixPerson].allowMissing.withIds(existingIds ++ missingIds). 308 | build.query) 309 | verify(docs, missingIds, existingIds, fixPersons) 310 | } 311 | 312 | "Get a document containing unicode values" >> { 313 | clear() 314 | val created = awaitRight(documents.create[FixPerson](fixHaile)) 315 | awaitRight(documents.get[FixPerson](created.id)).doc mustEqual fixHaile 316 | } 317 | 318 | "Update a document" >> { 319 | val created = awaitRight(documents.create(fixAlice)) 320 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 321 | val docOk = awaitRight(documents.update((_docPersonAge modify (_ + 1)) (aliceRes))) 322 | checkDocOk(docOk, aliceRes._id) 323 | val aliceRes2 = awaitRight(documents.get[FixPerson](aliceRes._id)) 324 | aliceRes2._id mustEqual aliceRes._id 325 | aliceRes2._rev mustEqual docOk.rev 326 | aliceRes2.doc.name mustEqual fixAlice.name 327 | aliceRes2.doc.age mustEqual fixAlice.age + 1 328 | } 329 | 330 | "Fail to update a document without ID" >> { 331 | val created = awaitRight(documents.create(fixAlice)) 332 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 333 | awaitError(documents.update(aliceRes.copy(_id = "")), "cannot_update") 334 | } 335 | 336 | "Fail to update a document with an outdated revision" >> { 337 | val created = awaitRight(documents.create(fixAlice)) 338 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 339 | await(documents.update((_docPersonAge set 26) (aliceRes))) 340 | val error = awaitLeft(documents.update((_docPersonAge set 27) (aliceRes))) 341 | error.status mustEqual Status.Conflict 342 | error.error mustEqual "conflict" 343 | } 344 | 345 | "Delete a document" >> { 346 | val created = awaitRight(documents.create(fixAlice)) 347 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 348 | awaitDocOk(documents.delete(aliceRes), aliceRes._id) 349 | awaitError(documents.get[FixPerson](aliceRes._id), "not_found") 350 | } 351 | 352 | "Attach a byte array to a document" >> { 353 | val created = awaitRight(documents.create(fixAlice)) 354 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 355 | awaitDocOk(documents.attach(aliceRes, fixAttachmentName, fixAttachmentData), aliceRes._id) 356 | } 357 | 358 | "Get a byte array attachment" >> { 359 | val created = awaitRight(documents.create(fixAlice)) 360 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 361 | awaitDocOk(documents.attach(aliceRes, fixAttachmentName, fixAttachmentData)) 362 | val url = s"http://${SpecConfig.couchDbHost}:${SpecConfig.couchDbPort}" + 363 | s"/$db/${aliceRes._id}/$fixAttachmentName" 364 | awaitRight(documents.getAttachmentUrl(aliceRes, fixAttachmentName)) mustEqual url 365 | awaitRight(documents.getAttachment(aliceRes, fixAttachmentName)) mustEqual fixAttachmentData 366 | } 367 | 368 | "Get a document with attachment stubs" >> { 369 | val created = awaitRight(documents.create(fixAlice)) 370 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 371 | awaitDocOk( 372 | documents.attach(aliceRes, fixAttachmentName, fixAttachmentData, fixAttachmentContentType), 373 | aliceRes._id) 374 | val doc = awaitRight(documents.get[FixPerson](aliceRes._id)) 375 | doc._id mustEqual aliceRes._id 376 | doc.doc.name mustEqual aliceRes.doc.name 377 | doc._attachments must haveLength(1) 378 | doc._attachments must haveKey(fixAttachmentName) 379 | val meta = doc._attachments(fixAttachmentName) 380 | meta.content_type mustEqual fixAttachmentContentType 381 | meta.length mustEqual fixAttachmentData.length 382 | meta.stub mustEqual true 383 | meta.digest must not be empty 384 | } 385 | 386 | "Get a document with attachments inline" >> { 387 | val created = awaitRight(documents.create(fixAlice)) 388 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 389 | awaitDocOk( 390 | documents.attach(aliceRes, fixAttachmentName, fixAttachmentData, fixAttachmentContentType), 391 | aliceRes._id) 392 | val doc = awaitRight(documents.get.attachments().query[FixPerson](aliceRes._id)) 393 | doc._id mustEqual aliceRes._id 394 | doc.doc.name mustEqual aliceRes.doc.name 395 | doc._attachments must haveLength(1) 396 | doc._attachments must haveKey(fixAttachmentName) 397 | val attachment = doc._attachments(fixAttachmentName) 398 | attachment.content_type mustEqual fixAttachmentContentType 399 | attachment.length mustEqual -1 400 | attachment.stub mustEqual false 401 | attachment.digest must not be empty 402 | attachment.toBytes mustEqual fixAttachmentData 403 | } 404 | 405 | "Create and get a document with attachments" >> { 406 | val attachments = Map[String, CouchAttachment]( 407 | fixAttachmentName -> CouchAttachment.fromBytes( 408 | fixAttachmentData, fixAttachmentContentType), 409 | fixAttachment2Name -> CouchAttachment.fromBytes( 410 | fixAttachment2Data, fixAttachment2ContentType)) 411 | val created = awaitRight(documents.create(fixAlice, attachments)) 412 | val doc = awaitRight(documents.get.attachments().query[FixPerson](created.id)) 413 | doc._id mustEqual created.id 414 | doc._attachments must haveLength(2) 415 | doc._attachments must haveKeys(fixAttachmentName, fixAttachment2Name) 416 | val attachment = doc._attachments(fixAttachmentName) 417 | attachment.content_type mustEqual fixAttachmentContentType 418 | attachment.length mustEqual -1 419 | attachment.stub mustEqual false 420 | attachment.digest must not be empty 421 | attachment.toBytes mustEqual fixAttachmentData 422 | val attachment2 = doc._attachments(fixAttachment2Name) 423 | attachment2.content_type mustEqual fixAttachment2ContentType.toString 424 | attachment2.length mustEqual -1 425 | attachment2.stub mustEqual false 426 | attachment2.digest must not be empty 427 | attachment2.toBytes mustEqual fixAttachment2Data 428 | } 429 | 430 | "Update a document with attachments" >> { 431 | val created = awaitRight(documents.create(fixAlice)) 432 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 433 | val aliceWithAttachments = aliceRes.copy( 434 | _attachments = Map( 435 | fixAttachmentName -> CouchAttachment.fromBytes( 436 | fixAttachmentData, fixAttachmentContentType), 437 | fixAttachment2Name -> CouchAttachment.fromBytes( 438 | fixAttachment2Data, fixAttachment2ContentType))) 439 | val docOk = awaitRight(documents.update(aliceWithAttachments)) 440 | checkDocOk(docOk, aliceRes._id) 441 | val doc = awaitRight(documents.get.attachments().query[FixPerson](created.id)) 442 | doc._id mustEqual created.id 443 | doc._attachments.keys must containTheSameElementsAs( 444 | Seq(fixAttachmentName, fixAttachment2Name)) 445 | val attachment = doc._attachments(fixAttachmentName) 446 | attachment.content_type mustEqual fixAttachmentContentType 447 | attachment.length mustEqual -1 448 | attachment.stub mustEqual false 449 | attachment.digest must not be empty 450 | attachment.toBytes mustEqual fixAttachmentData 451 | val attachment2 = doc._attachments(fixAttachment2Name) 452 | attachment2.content_type mustEqual fixAttachment2ContentType.toString 453 | attachment2.length mustEqual -1 454 | attachment2.stub mustEqual false 455 | attachment2.digest must not be empty 456 | attachment2.toBytes mustEqual fixAttachment2Data 457 | } 458 | 459 | "Delete an attachment to a document" >> { 460 | clear() 461 | val created = awaitRight(documents.create(fixAlice)) 462 | val aliceRes = awaitRight(documents.get[FixPerson](created.id)) 463 | val attachment = awaitRight(documents.attach(aliceRes, fixAttachmentName, fixAttachmentData)) 464 | val aliceWithAttachment = awaitRight(documents.get[FixPerson](created.id)) 465 | awaitDocOk(documents.deleteAttachment(aliceWithAttachment, fixAttachmentName), attachment.id) 466 | awaitError(documents.getAttachment(aliceRes, fixAttachmentName), "not_found") 467 | } 468 | 469 | "Bulk update should" >> { 470 | val fixes = Seq(fixAlice, fixBob, fixHaile) 471 | def change(v: String): String = s"$v-updated" 472 | def create(x: Seq[FixPerson]): Seq[CouchDoc[FixPerson]] = { 473 | clear() 474 | val newIds = awaitRight(documents.createMany(x)).map(_.id) 475 | awaitRight(documents.getMany[FixPerson](newIds)).getDocs 476 | } 477 | def modify(orig: Seq[CouchDoc[FixPerson]]): Seq[CouchDoc[FixPerson]] = 478 | orig.map(x => (_docPersonName modify change) (x)) 479 | def createAndModify: (Seq[FixPerson]) => Seq[CouchDoc[FixPerson]] = create _ andThen modify 480 | 481 | "update all documents when valid Ids and Rev" >> { 482 | val modified = createAndModify(fixes) 483 | val updatedDocs = awaitRight(documents.updateMany(modified)).map(_.id) 484 | updatedDocs.size mustEqual fixes.size 485 | val updateDocs = awaitRight(documents.getMany[FixPerson](modified.map(_._id))) 486 | updateDocs.getDocs.map(_.doc) mustEqual fixes.map( 487 | x => FixPerson(change(x.name), age = x.age)) 488 | } 489 | 490 | "fail if one or more elements is missing Id" >> { 491 | val modified = createAndModify(fixes) 492 | val withInvalidId = modified.updated(2, modified(2).copy(_id = "")) 493 | awaitError(documents.updateMany(withInvalidId), "cannot_update") 494 | awaitRight(documents.getMany[FixPerson](modified.map(_._id))) 495 | .getDocs.map(_.doc) mustEqual fixes 496 | } 497 | 498 | "fail if one or more elements is missing Rev" >> { 499 | val modified = createAndModify(fixes) 500 | val withInvalidRev = modified.updated(1, modified(1).copy(_rev = "")) 501 | awaitError(documents.updateMany(withInvalidRev), "cannot_update") 502 | awaitRight(documents.getMany[FixPerson](modified.map(_._id))) 503 | .getDocs.map(_.doc) mustEqual fixes 504 | } 505 | } 506 | 507 | "Bulk delete should" >> { 508 | val fixes = Seq(fixAlice, fixBob, fixHaile) 509 | def create(x: Seq[FixPerson]): Seq[CouchDoc[FixPerson]] = { 510 | clear() 511 | val newIds = awaitRight(documents.createMany(x)).map(_.id) 512 | awaitRight(documents.getMany[FixPerson](newIds)).getDocs 513 | } 514 | 515 | "delete all documents" >> { 516 | val created = create(fixes) 517 | val deleted = awaitRight(documents.deleteMany(created)).map(_.id) 518 | deleted.size mustEqual fixes.size 519 | val getDeleted = awaitRight(documents.getMany[FixPerson](created.map(_._id))) 520 | getDeleted.getDocs.size mustEqual fixes.size 521 | getDeleted.getDocs.count(Option(_).isDefined) mustEqual 0 522 | } 523 | 524 | "fail if one or more elements is missing an Id" >> { 525 | val created = create(fixes) 526 | val withInvalidId = created.updated(1, created(1).copy(_id = "")) 527 | awaitError(documents.deleteMany(withInvalidId), "cannot_update") 528 | awaitRight(documents.getMany[FixPerson](created.map(_._id))) 529 | .getDocs.map(_.doc) mustEqual fixes 530 | } 531 | 532 | "fail if one or more elements is missing a Rev" >> { 533 | val created = create(fixes) 534 | val withInvalidRev = created.updated(2, created(2).copy(_id = "")) 535 | awaitError(documents.deleteMany(withInvalidRev), "cannot_update") 536 | awaitRight(documents.getMany[FixPerson](created.map(_._id))) 537 | .getDocs.map(_.doc) mustEqual fixes 538 | } 539 | } 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/api/QueryListSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.spec.CouchDbSpecification 20 | 21 | class QueryListSpec extends CouchDbSpecification { 22 | 23 | val db = "couchdb-scala-query-list-spec" 24 | val databases = new Databases(client) 25 | val design = new Design(client, db) 26 | val documents = new Documents(client, db, typeMapping) 27 | val query = new Query(client, db) 28 | val list = query.list(fixDesign.name, FixLists.csvAll).get 29 | 30 | recreateDb(databases, db) 31 | 32 | val createdDesign = awaitRight(design.create(fixDesign)) 33 | val createdAlice = awaitRight(documents.create(fixAlice)) 34 | val createdBob = awaitRight(documents.create(fixBob)) 35 | val createdCarl = awaitRight(documents.create(fixCarl)) 36 | 37 | "Query List API" >> { 38 | 39 | "Query a list" >> { 40 | val expected = "Carl,20\nAlice,25\nBob,30\n" 41 | awaitRight(list.query(FixViews.compound)) mustEqual expected 42 | } 43 | 44 | "Query a list descending" >> { 45 | val expected = "Bob,30\nAlice,25\nCarl,20\n" 46 | awaitRight(list.descending().query(FixViews.compound)) mustEqual expected 47 | } 48 | 49 | "Query a list with a start key" >> { 50 | val expected = "Alice,25\nBob,30\n" 51 | awaitRight(list.startKey(Seq(21)).query(FixViews.compound)) mustEqual expected 52 | } 53 | 54 | "Query a list with custom params" >> { 55 | val expected = "name,age\nCarl,20\nAlice,25\nBob,30\n" 56 | awaitRight(list.addParam("header", "true").query(FixViews.compound)) mustEqual expected 57 | } 58 | 59 | "Query a list with a view from another design" >> { 60 | val expected = "Carl,20\nAlice,25\nBob,30\n" 61 | awaitRight(list.queryAnotherDesign(FixViews.compound, fixDesign.name)) mustEqual expected 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/api/QueryShowSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.spec.CouchDbSpecification 20 | 21 | class QueryShowSpec extends CouchDbSpecification { 22 | 23 | val db = "couchdb-scala-query-show-spec" 24 | val databases = new Databases(client) 25 | val design = new Design(client, db) 26 | val documents = new Documents(client, db, typeMapping) 27 | val query = new Query(client, db) 28 | val show = query.show(fixDesign.name, FixShows.csv).get 29 | 30 | recreateDb(databases, db) 31 | 32 | val createdDesign = awaitRight(design.create(fixDesign)) 33 | val createdAlice = awaitRight(documents.create(fixAlice)) 34 | val createdBob = awaitRight(documents.create(fixBob)) 35 | val createdCarl = awaitRight(documents.create(fixCarl)) 36 | 37 | "Query Show API" >> { 38 | 39 | "Query a show without ID" >> { 40 | awaitRight(show.query) mustEqual "empty show" 41 | } 42 | 43 | "Query a show by ID" >> { 44 | awaitRight(show.query(createdAlice.id)) mustEqual s"${fixAlice.name},${fixAlice.age}" 45 | awaitRight(show.query(createdBob.id)) mustEqual s"${fixBob.name},${fixBob.age}" 46 | } 47 | 48 | "Query a show by ID with params" >> { 49 | val res = awaitRight(show.addParam("extra", "test").query(createdAlice.id)) 50 | res mustEqual s"${fixAlice.name},${fixAlice.age},test" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/api/QueryTemporaryViewSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.spec.CouchDbSpecification 20 | 21 | class QueryTemporaryViewSpec extends CouchDbSpecification { 22 | 23 | val db = "couchdb-scala-query-temp-view-spec" 24 | val databases = new Databases(client) 25 | val documents = new Documents(client, db, typeMapping) 26 | val query = new Query(client, db) 27 | 28 | val namesView = query.temporaryView[String, String](FixViews.namesView).get 29 | val compoundView = query.temporaryView[(Int, String), FixPerson](FixViews.compoundView).get 30 | val aggregateView = query.temporaryView[String, String](FixViews.reducedView).get 31 | 32 | recreateDb(databases, db) 33 | 34 | val createdAlice = awaitRight(documents.create(fixAlice)) 35 | val createdBob = awaitRight(documents.create(fixBob)) 36 | val createdCarl = awaitRight(documents.create(fixCarl)) 37 | 38 | "Query Temporary View API" >> { 39 | 40 | "Query a temporary view" >> { 41 | val docs = awaitRight(namesView.build.query) 42 | docs.offset mustEqual 0 43 | docs.total_rows mustEqual 3 44 | docs.rows must haveLength(3) 45 | docs.rows.map(_.id) mustEqual Seq(createdAlice.id, createdBob.id, createdCarl.id) 46 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name) 47 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name) 48 | } 49 | 50 | "Query a temporary view with reducer" >> { 51 | val docs = awaitRight(aggregateView.reduce[Int].build.query) 52 | docs.rows must haveLength(1) 53 | docs.rows.head.value mustEqual Seq(fixCarl.age, fixBob.age, fixAlice.age).sum 54 | } 55 | 56 | "Query a temporary view with reducer given keys" >> { 57 | val docs = awaitRight( 58 | aggregateView.reduce[Int].withIds(Seq(createdCarl.id, createdAlice.id)).build.query) 59 | docs.rows must haveLength(2) 60 | docs.rows.map(_.value).sum mustEqual Seq(fixCarl.age, fixAlice.age).sum 61 | docs.rows.map(_.key) mustEqual Seq(createdCarl.id, createdAlice.id) 62 | } 63 | 64 | "Query a temporary view in the descending order" >> { 65 | val docs = awaitRight(namesView.descending().build.query) 66 | docs.offset mustEqual 0 67 | docs.total_rows mustEqual 3 68 | docs.rows must haveLength(3) 69 | docs.rows.map(_.id) mustEqual Seq(createdCarl.id, createdBob.id, createdAlice.id) 70 | docs.rows.map(_.key) mustEqual Seq(fixCarl.name, fixBob.name, fixAlice.name) 71 | docs.rows.map(_.value) mustEqual Seq(fixCarl.name, fixBob.name, fixAlice.name) 72 | } 73 | 74 | "Query a temporary view with compound keys and values" >> { 75 | val docs = awaitRight(compoundView.build.query) 76 | docs.offset mustEqual 0 77 | docs.total_rows mustEqual 3 78 | docs.rows must haveLength(3) 79 | docs.rows.map(_.key) mustEqual Seq((20, "Carl"), (25, "Alice"), (30, "Bob")) 80 | docs.rows.map(_.value) must contain(allOf(fixAlice, fixBob, fixCarl)) 81 | } 82 | 83 | "Query a temporary view and select by key" >> { 84 | val docs1 = awaitRight(namesView.key("Alice").build.query) 85 | docs1.offset mustEqual 0 86 | docs1.total_rows mustEqual 3 87 | docs1.rows must haveLength(1) 88 | docs1.rows.head.key mustEqual "Alice" 89 | docs1.rows.head.value mustEqual "Alice" 90 | val docs2 = awaitRight(compoundView.key((30, "Bob")).build.query) 91 | docs2.offset mustEqual 2 92 | docs2.total_rows mustEqual 3 93 | docs2.rows must haveLength(1) 94 | docs2.rows.head.key mustEqual ((30, "Bob")) 95 | } 96 | 97 | "Query a temporary view and include documents" >> { 98 | val docs = awaitRight(namesView.includeDocs[FixPerson].build.query) 99 | docs.offset mustEqual 0 100 | docs.total_rows mustEqual 3 101 | docs.rows must haveLength(3) 102 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name) 103 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name) 104 | docs.rows.map(_.doc.doc) mustEqual Seq(fixAlice, fixBob, fixCarl) 105 | } 106 | 107 | "Query a temporary view with a set of keys" >> { 108 | val docs = awaitRight(namesView.withIds(Seq(fixAlice.name, fixBob.name)).build.query) 109 | docs.offset mustEqual 0 110 | docs.total_rows mustEqual 3 111 | docs.rows must haveLength(2) 112 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name) 113 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name) 114 | } 115 | 116 | "Query a temporary view with a set of keys and include documents" >> { 117 | val docs = awaitRight( 118 | namesView.includeDocs[FixPerson].withIds( 119 | Seq(fixAlice.name, fixBob.name)).build.query) 120 | docs.offset mustEqual 0 121 | docs.total_rows mustEqual 3 122 | docs.rows must haveLength(2) 123 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name) 124 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name) 125 | docs.rows.map(_.doc.doc) mustEqual Seq(fixAlice, fixBob) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/api/QueryViewSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.spec.CouchDbSpecification 20 | import com.ibm.couchdb.{CouchDocs, CouchKeyVals, CouchReducedKeyVals} 21 | import org.specs2.matcher.MatchResult 22 | 23 | import scalaz.concurrent.Task 24 | 25 | class QueryViewSpec extends CouchDbSpecification { 26 | 27 | val db = "couchdb-scala-query-view-spec" 28 | val databases = new Databases(client) 29 | val design = new Design(client, db) 30 | val documents = new Documents(client, db, typeMapping) 31 | val query = new Query(client, db) 32 | val namesView = query.view[String, String](fixDesign.name, FixViews.names).get 33 | val compoundView = query.view[(Int, String), FixPerson](fixDesign.name, FixViews.compound).get 34 | val aggregateView = query.view[String, String](fixDesign.name, FixViews.reduced).get 35 | 36 | recreateDb(databases, db) 37 | 38 | val createdDesign = awaitRight(design.create(fixDesign)) 39 | val createdAlice = awaitRight(documents.create(fixAlice)) 40 | val createdBob = awaitRight(documents.create(fixBob)) 41 | val createdCarl = awaitRight(documents.create(fixCarl)) 42 | 43 | "Query View API" >> { 44 | 45 | "Query a view" >> { 46 | def verify(task: Task[CouchKeyVals[String, String]]): MatchResult[Any] = { 47 | val docs = awaitRight(task) 48 | docs.offset mustEqual 0 49 | docs.total_rows mustEqual 3 50 | docs.rows must haveLength(3) 51 | docs.rows.map(_.id) mustEqual Seq(createdAlice.id, createdBob.id, createdCarl.id) 52 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name) 53 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name) 54 | } 55 | verify(namesView.build.query) 56 | verify(namesView.noReduce.excludeDocs.build.query) 57 | } 58 | 59 | "Query a view with reducer" >> { 60 | def verify(task: Task[CouchReducedKeyVals[String, Int]]): MatchResult[Any] = { 61 | val docs = awaitRight(task) 62 | docs.rows must haveLength(1) 63 | docs.rows.head.value mustEqual Seq(fixCarl.age, fixBob.age, fixAlice.age).sum 64 | } 65 | verify(aggregateView.reduce[Int].build.query) 66 | verify(aggregateView.noReduce.reduce[Int].build.query) 67 | } 68 | 69 | "Query a view with reducer given keys" >> { 70 | def verify(task: Task[CouchReducedKeyVals[String, Int]]): MatchResult[Any] = { 71 | val docs = awaitRight(task) 72 | docs.rows must haveLength(2) 73 | docs.rows.map(_.value).sum mustEqual Seq(fixCarl.age, fixAlice.age).sum 74 | docs.rows.map(_.key) mustEqual Seq(createdCarl.id, createdAlice.id) 75 | } 76 | verify(aggregateView.reduce[Int].withIds(Seq(createdCarl.id, createdAlice.id)).build.query) 77 | } 78 | 79 | "Query a view in the descending order" >> { 80 | def verify(task: Task[CouchKeyVals[String, String]]): MatchResult[Any] = { 81 | val docs = awaitRight(task) 82 | docs.offset mustEqual 0 83 | docs.total_rows mustEqual 3 84 | docs.rows must haveLength(3) 85 | docs.rows.map(_.id) mustEqual Seq(createdCarl.id, createdBob.id, createdAlice.id) 86 | docs.rows.map(_.key) mustEqual Seq(fixCarl.name, fixBob.name, fixAlice.name) 87 | docs.rows.map(_.value) mustEqual Seq(fixCarl.name, fixBob.name, fixAlice.name) 88 | } 89 | verify(namesView.descending().build.query) 90 | } 91 | 92 | "Query a view with compound keys and values" >> { 93 | def verify( 94 | task: Task[CouchKeyVals[(Int, String), FixPerson]]): MatchResult[Seq[FixPerson]] = { 95 | val docs = awaitRight(task) 96 | docs.offset mustEqual 0 97 | docs.total_rows mustEqual 3 98 | docs.rows must haveLength(3) 99 | docs.rows.map(_.key) mustEqual Seq((20, "Carl"), (25, "Alice"), (30, "Bob")) 100 | docs.rows.map(_.value) must contain(allOf(fixAlice, fixBob, fixCarl)) 101 | } 102 | verify(compoundView.build.query) 103 | } 104 | 105 | "Query a view and select by key" >> { 106 | def verify[K, V, I]( 107 | task: Task[CouchKeyVals[K, V]], expected: Seq[I], total: Int): MatchResult[Any] = { 108 | val docs = awaitRight(task) 109 | docs.total_rows mustEqual total 110 | docs.rows must haveLength(expected.length) 111 | docs.rows.map(_.key) must containTheSameElementsAs(expected) 112 | } 113 | verify(namesView.key("Alice").build.query, Seq("Alice"), 3) 114 | verify(compoundView.key((30, "Bob")).build.query, Seq((30, "Bob")), 3) 115 | } 116 | 117 | "Query a view and include documents" >> { 118 | def verify(task: Task[CouchDocs[String, String, FixPerson]]): MatchResult[Any] = { 119 | val docs = awaitRight(task) 120 | docs.offset mustEqual 0 121 | docs.total_rows mustEqual 3 122 | docs.rows must haveLength(3) 123 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name) 124 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name) 125 | docs.rows.map(_.doc.doc) mustEqual Seq(fixAlice, fixBob, fixCarl) 126 | } 127 | verify(namesView.includeDocs[FixPerson].build.query) 128 | } 129 | 130 | "Query a view with a set of keys" >> { 131 | def verify(task: Task[CouchKeyVals[String, String]]): MatchResult[Any] = { 132 | val docs = awaitRight(task) 133 | docs.offset mustEqual 0 134 | docs.total_rows mustEqual 3 135 | docs.rows must haveLength(2) 136 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name) 137 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name) 138 | } 139 | verify(namesView.withIds(Seq(fixAlice.name, fixBob.name)).build.query) 140 | } 141 | 142 | "Query a view with a set of keys and include documents" >> { 143 | def verify(task: Task[CouchDocs[String, String, FixPerson]]): MatchResult[Any] = { 144 | val docs = awaitRight(task) 145 | docs.offset mustEqual 0 146 | docs.total_rows mustEqual 3 147 | docs.rows must haveLength(2) 148 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name) 149 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name) 150 | docs.rows.map(_.doc.doc) mustEqual Seq(fixAlice, fixBob) 151 | } 152 | verify(namesView.includeDocs[FixPerson].withIds(Seq(fixAlice.name, fixBob.name)).build.query) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/api/ServerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.api 18 | 19 | import com.ibm.couchdb.spec.CouchDbSpecification 20 | 21 | class ServerSpec extends CouchDbSpecification { 22 | 23 | val db = "couchdb-scala-server-spec" 24 | val server = new Server(client) 25 | 26 | "Server API" >> { 27 | 28 | "Get info about the DB instance" >> { 29 | val info = awaitRight(server.info) 30 | info.couchdb mustEqual "Welcome" 31 | info.uuid must beUuid 32 | info.version.length must beGreaterThanOrEqualTo(3) 33 | } 34 | 35 | "Create a UUID" >> { 36 | awaitRight(server.mkUuid) must beUuid 37 | } 38 | 39 | "Create 3 UUIDs" >> { 40 | val uuids = awaitRight(server.mkUuids(3)) 41 | uuids must have size 3 42 | uuids must contain(beUuid).forall 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/implicits/TaskImplicitsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.implicits 18 | 19 | import com.ibm.couchdb.api.Databases 20 | import com.ibm.couchdb.spec.CouchDbSpecification 21 | 22 | class TaskImplicitsSpec extends CouchDbSpecification { 23 | 24 | val db = "couchdb-scala-task-implicits-spec" 25 | val databases = new Databases(client) 26 | 27 | private def clear() = await(databases.delete(db)) 28 | 29 | "Task implicits" >> { 30 | 31 | "Ignore error" >> { 32 | clear() 33 | awaitOk(databases.create(db)) 34 | awaitError(databases.create(db), "file_exists") 35 | awaitOk(databases.create(db).ignoreError) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/implicits/UpickleImplicitsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.implicits 18 | 19 | import com.ibm.couchdb.spec.Fixtures 20 | import org.http4s.Status 21 | import org.specs2.mutable._ 22 | import org.specs2.specification.AllExpectations 23 | import upickle.default.Aliases.{R, W} 24 | import upickle.default.{read => pickleR, write => pickleW} 25 | 26 | class UpickleImplicitsSpec extends Specification 27 | with AllExpectations 28 | with Fixtures 29 | with UpickleImplicits { 30 | 31 | val map0 = Map[String, String]() 32 | val json0 = "{}" 33 | 34 | val map1 = Map[String, String]("key1" -> "val1", "key2" -> "val2") 35 | val json1 = "{\"key1\":\"val1\",\"key2\":\"val2\"}" 36 | 37 | val map2 = Map[String, Int]("key1" -> 1, "key2" -> 2) 38 | val json2 = "{\"key1\":1,\"key2\":2}" 39 | 40 | val map3 = Map[String, (String, Int)]("key1" -> (("key1", 1)), "key2" -> (("key2", 2))) 41 | val json3 = "{\"key1\":[\"key1\",1],\"key2\":[\"key2\",2]}" 42 | 43 | val map4 = Map[String, FixPerson]( 44 | "key1" -> FixPerson("Alice", 25), "key2" -> FixPerson("Bob", 30)) 45 | val json4 = "{\"key1\":{\"name\":\"Alice\",\"age\":25},\"key2\":{\"name\":\"Bob\",\"age\":30}}" 46 | 47 | private def testRoundtrip[D](obj: D)(implicit r: R[D], w: W[D]) = { 48 | pickleR[D](pickleW(obj)) mustEqual obj 49 | } 50 | 51 | "Custom upickle Reader and Writer instances" >> { 52 | 53 | "Write and read Map[String, D]" >> { 54 | pickleW(map0) mustEqual json0 55 | pickleW(map1) mustEqual json1 56 | pickleW(map2) mustEqual json2 57 | pickleW(map3) mustEqual json3 58 | pickleW(map4) mustEqual json4 59 | } 60 | 61 | "Read Map[String, D] from JSON" >> { 62 | pickleR[Map[String, String]](json0) mustEqual map0 63 | pickleR[Map[String, String]](json1) mustEqual map1 64 | pickleR[Map[String, Int]](json2) mustEqual map2 65 | pickleR[Map[String, (String, Int)]](json3) mustEqual map3 66 | pickleR[Map[String, FixPerson]](json4) mustEqual map4 67 | } 68 | 69 | "Write and read an Status" >> { 70 | testRoundtrip[Status](Status.Ok) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/spec/CouchDbSpecification.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.spec 18 | 19 | import com.ibm.couchdb._ 20 | import com.ibm.couchdb.api.Databases 21 | import com.ibm.couchdb.core.Client 22 | import com.ibm.couchdb.implicits.{TaskImplicits, UpickleImplicits} 23 | import org.specs2.matcher._ 24 | import org.specs2.mutable._ 25 | import org.specs2.scalaz.DisjunctionMatchers 26 | import org.specs2.specification.AllExpectations 27 | 28 | import scalaz._ 29 | import scalaz.concurrent.Task 30 | 31 | trait CouchDbSpecification extends Specification with 32 | Fixtures with 33 | AllExpectations with 34 | DisjunctionMatchers with 35 | MatcherMacros with 36 | TaskImplicits with 37 | UpickleImplicits { 38 | sequential 39 | 40 | val client = new Client( 41 | Config(SpecConfig.couchDbHost, SpecConfig.couchDbPort, https = false, None)) 42 | 43 | def await[T](future: Task[T]): Throwable \/ T = future.unsafePerformSyncAttempt 44 | 45 | def awaitRight[T](future: Task[T]): T = { 46 | val res = await(future) 47 | res must beRightDisjunction 48 | res.toOption.get 49 | } 50 | 51 | def awaitOk[T](future: Task[Res.Ok]): MatchResult[Any] = { 52 | await(future) must beRightDisjunction(Res.Ok(ok = true)) 53 | } 54 | 55 | def awaitDocOk[D](future: Task[Res.DocOk]): MatchResult[Any] = { 56 | checkDocOk(awaitRight(future)) 57 | } 58 | 59 | def awaitDocOk[D](future: Task[Res.DocOk], id: String): MatchResult[Any] = { 60 | checkDocOk(awaitRight(future), id) 61 | } 62 | 63 | def awaitLeft(future: Task[_]): Res.Error = { 64 | val res = await(future) 65 | res must beLeftDisjunction 66 | val -\/(e) = res 67 | e.asInstanceOf[CouchException[Res.Error]].content 68 | } 69 | 70 | def awaitError(future: Task[_], error: String): MatchResult[Any] = { 71 | val res = awaitLeft(future) 72 | res must beLike { 73 | case Res.Error(actualError, _, _, _, _) => actualError mustEqual error 74 | } 75 | } 76 | 77 | def beUuid: Matcher[String] = haveLength(32) 78 | 79 | def beRev: Matcher[String] = (_: String).length must beGreaterThan(32) 80 | 81 | def checkDocOk(doc: Res.DocOk): MatchResult[Any] = { 82 | (doc.ok mustEqual true) and (doc.id must not beEmpty) and (doc.rev must beRev) 83 | } 84 | 85 | def checkDocOk(doc: Res.DocOk, id: String): MatchResult[Any] = { 86 | checkDocOk(doc) and (doc.id mustEqual id) 87 | } 88 | 89 | def recreateDb(databases: Databases, name: String): \/[Throwable, Unit] = await { 90 | for { 91 | _ <- databases.delete(name).or(Task.now(Res.Ok())) 92 | _ <- databases.create(name) 93 | } yield () 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/spec/Fixtures.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation, Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.spec 18 | 19 | import com.ibm.couchdb.Lenses._ 20 | import com.ibm.couchdb._ 21 | import monocle.macros.GenLens 22 | import org.http4s.MediaType 23 | import org.http4s.headers.`Content-Type` 24 | 25 | trait Fixtures { 26 | 27 | case class FixPerson(name: String, age: Int) 28 | 29 | case class FixXPerson(name: String, aka: String, superPower: String) 30 | 31 | val typeMapping = TypeMapping(classOf[FixPerson] -> "Person", classOf[FixXPerson] -> "XPerson") 32 | 33 | val lenser = GenLens[FixPerson] 34 | val _personName = lenser(_.name) 35 | val _personAge = lenser(_.age) 36 | val _docPersonName = _couchDoc composeLens _personName 37 | val _docPersonAge = _couchDoc composeLens _personAge 38 | 39 | val fixProfessorX = FixXPerson( 40 | "Charles Xavier", "Professor X", "Telepathy, Astral projection, Mind control") 41 | val fixMagneto = FixXPerson("Max Eisenhardt", "Magneto", "Magnetism manipulation") 42 | 43 | val fixAlice = FixPerson("Alice", 25) 44 | val fixBob = FixPerson("Bob", 30) 45 | val fixCarl = FixPerson("Carl", 20) 46 | val fixHaile = FixPerson("\u1283\u12ED\u120C \u1308\u1265\u1228\u1225\u120B\u1234", 42) 47 | val fixMagritte = FixPerson("Ren\u00E9 Magritte", 68) 48 | 49 | object FixViews { 50 | val names = "names" 51 | val compound = "compound" 52 | val reduced = "reduced" 53 | val typeFilter = "typeFilter" 54 | val typeFilterCustom = "typeFilterCustom" 55 | 56 | val namesView = CouchView( 57 | map = 58 | """ 59 | |function(doc) { 60 | | emit(doc.doc.name, doc.doc.name); 61 | |} 62 | """.stripMargin) 63 | 64 | val reducedView = CouchView( 65 | map = 66 | """ 67 | |function(doc) { 68 | | emit(doc._id, doc.doc.age); 69 | |} 70 | """.stripMargin, 71 | reduce = 72 | """ 73 | |function(key, values, rereduce) { 74 | | return sum(values); 75 | |} 76 | """.stripMargin) 77 | 78 | val compoundView = CouchView( 79 | map = 80 | """ 81 | |function(doc) { 82 | | var d = doc.doc; 83 | | emit([d.age, d.name], d); 84 | |} 85 | """.stripMargin) 86 | 87 | val typeFilterView = CouchView( 88 | map = 89 | """ 90 | |function(doc) { 91 | | emit([doc.kind, doc._id], doc._id); 92 | |} 93 | """.stripMargin) 94 | 95 | val typeFilterViewCustom = CouchView( 96 | map = 97 | """ 98 | |function(doc) { 99 | | emit([doc.kind, doc._id, doc.doc.age], doc._id); 100 | |} 101 | """.stripMargin) 102 | } 103 | 104 | object FixShows { 105 | val csv = "csv" 106 | } 107 | 108 | object FixLists { 109 | val csvAll = "csv-all" 110 | } 111 | 112 | val fixDesign = CouchDesign( 113 | name = "test-design", 114 | 115 | views = Map( 116 | FixViews.names -> FixViews.namesView, 117 | FixViews.reduced -> FixViews.reducedView, 118 | FixViews.compound -> FixViews.compoundView, 119 | FixViews.typeFilter -> FixViews.typeFilterView, 120 | FixViews.typeFilterCustom -> FixViews.typeFilterViewCustom 121 | ), 122 | 123 | shows = Map( 124 | FixShows.csv -> 125 | """ 126 | |function(doc, req) { 127 | | if (doc !== null && doc.kind == "Person") { 128 | | var res = doc.doc.name + ',' + doc.doc.age; 129 | | if (typeof req.query.extra !== "undefined") { 130 | | res += ',' + req.query.extra; 131 | | } 132 | | return res; 133 | | } else { 134 | | return 'empty show'; 135 | | } 136 | |} 137 | """.stripMargin), 138 | 139 | lists = Map( 140 | FixLists.csvAll -> 141 | """ 142 | |function(head, req) { 143 | | var row = getRow(); 144 | | if (!row) { 145 | | return 'no rows'; 146 | | } 147 | | if (typeof req.query.header !== "undefined" && req.query.header) { 148 | | send('name,age\n'); 149 | | } 150 | | send(row.value.name + ',' + row.value.age + '\n'); 151 | | while (row = getRow()) { 152 | | send(row.value.name + ',' + row.value.age + '\n'); 153 | | } 154 | |} 155 | """.stripMargin) 156 | 157 | ) 158 | 159 | val fixAttachmentName = "attachment" 160 | val fixAttachmentData = Array[Byte](-1, 0, 1, 2, 3) 161 | val fixAttachmentContentType = "image/jpg" 162 | val fixAttachment2Name = "attachment2" 163 | val fixAttachment2Data = Array[Byte](-1, 0, 1, 2, 3, 4, 5) 164 | val fixAttachment2ContentType = `Content-Type`(MediaType.`image/png`) 165 | } 166 | -------------------------------------------------------------------------------- /src/test/scala/com/ibm/couchdb/spec/SpecConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 IBM Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.ibm.couchdb.spec 18 | 19 | object SpecConfig { 20 | 21 | val couchDbHost = System.getProperty("couchDbHost", "127.0.0.1") 22 | val couchDbPort = System.getProperty("couchDbPort", "5984").toInt 23 | val couchDbHttpsPort = System.getProperty("couchDbHttpsPort", "6984").toInt 24 | val couchDbUsername = System.getProperty("couchDbUsername", "admin") 25 | val couchDbPassword = System.getProperty("couchDbPassword", "admin") 26 | 27 | private val log = org.log4s.getLogger 28 | 29 | log.info("----------------------") 30 | log.info(s"couchDbHost: $couchDbHost") 31 | log.info(s"couchDbPort: $couchDbPort") 32 | log.info("----------------------") 33 | } 34 | --------------------------------------------------------------------------------