├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── NEWS.md ├── NOTICE ├── README.md ├── doc ├── README.md ├── couchbeam.md ├── couchbeam_app.md ├── couchbeam_attachments.md ├── couchbeam_changes.md ├── couchbeam_changes_stream.md ├── couchbeam_changes_sup.md ├── couchbeam_deps.md ├── couchbeam_doc.md ├── couchbeam_ejson.md ├── couchbeam_httpc.md ├── couchbeam_sup.md ├── couchbeam_util.md ├── couchbeam_uuids.md ├── couchbeam_view.md ├── couchbeam_view_stream.md ├── couchbeam_view_sup.md ├── gen_changes.md └── overview.edoc ├── examples ├── changes.ebin ├── changes_once.ebin └── test_gen_changes.erl ├── include └── couchbeam.hrl ├── rebar.config ├── rebar.lock ├── src ├── couchbeam.app.src ├── couchbeam.erl ├── couchbeam_app.erl ├── couchbeam_attachments.erl ├── couchbeam_changes.erl ├── couchbeam_changes_stream.erl ├── couchbeam_changes_sup.erl ├── couchbeam_doc.erl ├── couchbeam_ejson.erl ├── couchbeam_httpc.erl ├── couchbeam_sup.erl ├── couchbeam_util.erl ├── couchbeam_uuids.erl ├── couchbeam_view.erl ├── couchbeam_view_stream.erl ├── couchbeam_view_sup.erl └── gen_changes.erl └── support └── 1M /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | pull_request: 7 | branches: [ master, main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup CouchDB with single node configuration 17 | run: | 18 | # Create local.ini with single node configuration 19 | sudo mkdir -p /opt/couchdb/etc/local.d 20 | sudo tee /opt/couchdb/etc/local.d/10-single-node.ini > /dev/null < 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | couchbeam NEWS 2 | -------------- 3 | 4 | version 1.7.0 / 2025-05-28 5 | -------------------------- 6 | 7 | - fix resource leaks and race conditions in stream modules 8 | - fix unclosed hackney connections on error paths 9 | - add proper cleanup for monitor references using try-finally 10 | - fix ETS table race conditions by checking process liveness 11 | - improve stream initialization order to prevent races 12 | - add error handling for hackney operations to prevent leaks 13 | - fix changes stream registration race condition by registering before parent notification 14 | 15 | version 1.6.0 / 2025-01-26 16 | -------------------------- 17 | 18 | - add support for CouchDB _find endpoint 19 | - add ability to query _show functions 20 | - add option for disabling view_stream usage (enabled by default) 21 | - fix error handling in gen_changes callback handling 22 | - fix resource cleanup in stream modules to prevent connection leaks 23 | - improve replication test reliability and timeout handling 24 | - update hackney dependency to 1.23 25 | - update jsx dependency 26 | - add GitHub Actions for CouchDB testing 27 | - fix dialyzer complaints and pattern matching issues 28 | - OTP 27 compatibility improvements 29 | 30 | version 1.5.4 / 2025-02-20 31 | -------------------------- 32 | 33 | - bump hackney 1.22.0 34 | 35 | version 1.5.3 / 2024-08-20 36 | -------------------------- 37 | 38 | - fix packaging 39 | 40 | version 1.5.1 / 2024-11-07 41 | -------------------------- 42 | 43 | - fix pattern matching 44 | 45 | version 1.5.1 / 2024-11-07 46 | -------------------------- 47 | 48 | - fix: handle condition that may cause persistent CLOSE-WAIT sockets 49 | 50 | version 1.5.0 / 2023-10-11 51 | -------------------------- 52 | 53 | - use hackney 1.20.0 54 | - fix compatibility with couchdb 3 55 | - fix compatibility with Erlang >= 23 56 | 57 | version 1.4.1 / 2016-09-26 58 | -------------------------- 59 | 60 | - maintainance update 61 | 62 | version 1.4.0 / 2016-09-22 63 | -------------------------- 64 | 65 | - maintainance update. 66 | 67 | version 1.3.1 / 2016-07-01 68 | -------------------------- 69 | 70 | - fix: accept 202 status in `couchbeam:save_doc/4` function (#144) 71 | - fix: spec syntax to build with Erlang 19 (#145) 72 | 73 | version 1.3.0 / 2016-03-22 74 | -------------------------- 75 | 76 | - add `couchbeam:all_dbs/2` 77 | - add `couchbeam:view_cleanup/1` 78 | - add `couchbeam:design_info/2` 79 | - add `post_decode` function to view stream 80 | - add Elixir mix support 81 | - fix: handle http errors in view stream (#140) 82 | - fix: build with latest rebar3 83 | 84 | version 1.2.1 / 2015/11/04 85 | -------------------------- 86 | 87 | - also support hackney 1.4.4 for rebar2. 88 | - fix hex.pm release to really use 1.4.4 89 | 90 | version 1.2.0 / 2015/11/04 91 | -------------------------- 92 | 93 | - move to eunit for tests. 94 | - hex.pm support 95 | - mix & rebar3 build tools support 96 | - bump hackney to 1.4.4 97 | - bump jsx to 2.2.8 98 | 99 | ### Breaking change 100 | 101 | erlang-oauth is now optionnal and won't be installed by default. 102 | 103 | version 1.1.8 / 2015-08-27 104 | -------------------------- 105 | 106 | - use latest stable branch of [hackney](https://github.com/benoitc/hackney) 107 | 108 | version 1.1.7 / 2015-03-11 109 | -------------------------- 110 | 111 | - bump [hackney](https://github.com/benoitc/hackney) to 1.1.0 112 | - fix `Conten-Type` header ehen posting doc IDS in changes #126 113 | - fix documentation 114 | 115 | version 1.1.6 / 2015-01-02 116 | -------------------------- 117 | 118 | - fix `included_applications` (#122) 119 | 120 | version 1.1.5 / 2014-12-09 121 | -------------------------- 122 | 123 | - improvement: do not force connections options to nodelay 124 | - update to [Hackney](https://github.com/benoitc/hackney) 1.0.4 fix #120 125 | - fix: retry fecthing UUIDS on error (#121) 126 | 127 | version 1.1.4 / 2014-12-01 128 | -------------------------- 129 | 130 | - update to [Hackney](https://github.com/benoitc/hackney) 1.0.1: more SSL 131 | certificate authority handling. 132 | - fix: changes stream 133 | 134 | version 1.1.3 / 2014-11-30 135 | -------------------------- 136 | 137 | - update to [Hackney](https://github.com/benoitc/hackney) 1.0.0 138 | 139 | version 1.1.2 / 2014-11-15 140 | -------------------------- 141 | 142 | - remove spurious prints 143 | 144 | version 1.1.1 / 2014-11-11 145 | -------------------------- 146 | 147 | - update to [hackney 0.15.0](https://github.com/benoitc/hackney/releases ), 148 | improving performances and concurrency 149 | - fix `couchbeam:doc_exists/2`(#116) 150 | - fix `couchbeam:reply_att/1 (#114) 151 | 152 | 153 | version 1.1.0 / 2014-10-28 154 | -------------------------- 155 | 156 | - update to [hackney 0.14.3](https://github.com/benoitc/hackney/releases) 157 | - fix memory leaks 158 | - correctly close sockets 159 | - fix streaming issue: don't wait the stream timeout to report the initial 160 | error. 161 | - update JSX dependency to version 2.1.1 162 | 163 | version 1.0.7 / 2014-07-08 164 | -------------------------- 165 | 166 | - bump to [hackney 0.13.0](https://github.com/benoitc/hackney/releases/tag/0.13.0) 167 | 168 | version 1.0.6 / 2014-04-18 169 | -------------------------- 170 | 171 | - bump to [hackney 0.12.1](https://github.com/benoitc/hackney/releases/tag/0.12.2) 172 | 173 | 174 | version 1.0.5 / 2014-04-18 175 | -------------------------- 176 | 177 | - improve connections with HTTP proxies 178 | - improve content-types detection of attachments 179 | - improve URL encoding normalzation, useful when connecting to an 180 | international domain/URI 181 | - URL resolving is faster 182 | - bump to [hackney 0.12.0](https://github.com/benoitc/hackney/releases/tag/0.12.0) 183 | 184 | version 1.0.4 / 2014-04-15 185 | -------------------------- 186 | 187 | - remove spurious print 188 | 189 | version 1.0.3 / 2014-04-15 190 | -------------------------- 191 | 192 | - add support for the `new_edits` option in bulk doc API. 193 | - improvement: send a doc as multipart that already contains attachments 194 | - bump [hackney](http://github.com/benoitc/hackney) to 0.11.2 195 | - fix path encoding 196 | 197 | version 1.0.2 / 2014-01-03 198 | -------------------------- 199 | 200 | - fix: send a doc as multipart that already contains attachments 201 | 202 | 203 | version 1.0.1 / 2013-12-30 204 | -------------------------- 205 | 206 | - fix connection reusing in changes and view streams 207 | - bump hackney version to 0.10.1 208 | 209 | 210 | version 1.0.0 / 2013-12-21 211 | -------------------------- 212 | 213 | **First stable release**. This is a supported release. 214 | 215 | - send a doc and its attachments efficiently using the [multipart 216 | API](http://docs.couchdb.org/en/latest/api/document/common.html#creating-multiple-attachments). 217 | - add `couchbeam:get_config/{1,2,3}`, `couchbeam:set_config/{4,5}` and 218 | `couchbeam:delete_config/{3,4}` to use the [config 219 | API](http://docs.couchdb.org/en/latest/api/server/configuration.html). 220 | - add `couchbeam_uuids:random/0` and `couchbeam_uuids:utc_random/0` to 221 | generate UUIDS in your app instead of reusing the UUID generated on 222 | the node. By default couchbeam is fetching from the node, which is 223 | - add `{error, forbidden}` and `{error, unauthenticated}` as possible 224 | results of a reply. 225 | better if you want to use UUID based on the node time. 226 | - fix accept header handling 227 | 228 | 229 | version 0.10.0 / 2013-12-21 230 | --------------------------- 231 | 232 | - add `couchbeam:copy_doc/{2,3}` to support the COPY API 233 | - add `couchbeam:get_missing_revs/2` to get the list of missing 234 | revisions 235 | - add support of the [multipart 236 | API](http://docs.couchdb.org/en/latest/api/document/common.html#efficient-multiple-attachments-retrieving) when fetching a doc: This change make 237 | `couchbeam:open_doc/3` return a multipart response `{ok, {multipart, 238 | Stream}}` when using the setting `attachments=true` option. A new option 239 | {`accept. <<"multipart/mixed">>}" can also be used with the options 240 | `open_revs` or `revs` to fetch the response as a multipart. 241 | - bump the [hackney](http://github.com/benoitc/hackney) version to 242 | **0.9.1** . 243 | 244 | 245 | With this change you can now efficiently retrieve a doc with all of its 246 | attachments or a doc wit all its revisions. 247 | master 248 | 249 | version 0.9.3 / 2013-12-07 250 | -------------------------- 251 | 252 | - fix: `couchbeam:open_or_create_db/2' 253 | 254 | version 0.9.2 / 2013-12-07 255 | -------------------------- 256 | 257 | - bump hackney version to 0.8.3 258 | 259 | version 0.9.1 / 2013-12-05 260 | -------------------------- 261 | 262 | - fix design docid encoding 263 | 264 | version 0.9.0 / 2013-12-05 265 | -------------------------- 266 | 267 | This is a major release pre-1.0. API is now frozen and won't change much 268 | until the version 1.0. 269 | 270 | - replaced the use of `ibrowse` by `hackney` to handle HTTP connections 271 | - new [streaming 272 | API](https://github.com/benoitc/couchbeam#stream-view-results) in view 273 | - breaking change: remobe 274 | - breaking change: remove deprecated view API. Everything is now managed in the 275 | [couch_view](https://github.com/benoitc/couchbeam/blob/master/doc/couchbeam_view.md) module. 276 | - replace `couchbeam_changes:stream` and `couchbeam_changes:fetch` 277 | functions by `couchbeam_changes/follow` and `couchbeam_changes:follow_once`. 278 | - breaking change: new attachment API 279 | - new: JSX a pure erlang JSON encoder/decoder is now the default. Jiffy 280 | can be set at the compilation by defining `WITH_JIFFY` in the Erlang 281 | options. 282 | - removed mochiweb dependency. 283 | 284 | 285 | version 0.7.0 / 2011-07-05 286 | -------------------------- 287 | 288 | This release contains backwards incompatible changes. 289 | 290 | - New and more efficient couchbeam_changes API, we now parse json stream 291 | instead of the try catch steps we used before. 292 | - New and more efficient couchbeam_view API. we now parse json stream 293 | instead of getting all results. New couchbeam_view:stream and 294 | couchbeam_view fetch functions have been added. We also don't use any 295 | more a view record in other functions 296 | - HTTP functions have been moved to couchbeam_httpc modules 297 | - gen_changes behaviour has been updated to use the couchbeam_changes 298 | API. It's also abble to restart a lost connection for longpoll and 299 | continuous feeds. 300 | 301 | Breaking Changes: 302 | 303 | - couchbeam:view and couchbeam:all_docs have been deprecated. Old views 304 | functions using the #view{} record from these functions have been 305 | moved in couchbeam_oldview module. 306 | - couchbeam:wait_changes, couchbeam:wait_changes_once, couchbeam:changes 307 | functions have been deprecated and are now replaced by 308 | couchbeam_changes:stream and couchbeam_changes:fetch functions. 309 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Couchdbeam 2 | ---------- 3 | 4 | 2009-2016 (c) Benoitît Chesneau 5 | 6 | couchbeam is released under the MIT license. See the LICENSE file for the 7 | complete license. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Couchbeam - simple Apache CouchDB client library for Erlang applications # 4 | 5 | Copyright (c) 2009-2025 Benoît Chesneau. 6 | 7 | __Version:__ 1.7.0 8 | 9 | # couchbeam 10 | 11 | Couchbeam is a simple erlang library for [Barrel](https://barrel-db.org) or [Apache CouchDB](http://couchdb.apache.org). Couchbeam provides you a full featured and easy client to access and manage multiple nodes. 12 | 13 | #### Main features: 14 | 15 | - Complete support of the BarrelDB and Apache CouchDB API 16 | - Stream view results to your app 17 | - Stream changes feeds 18 | - reduced memory usage 19 | - fetch and send attachments in a streaming fashion 20 | - by default use the JSX module to encode/decode JSON 21 | - support [Jiffy](http://github.com/davisp/jiffy) a JSON encoder/decoder 22 | in C. 23 | 24 | #### Useful modules are: 25 | 26 | - [`couchbeam`](http://github.com/benoitc/couchbeam/blob/master/doc/couchbeam.md): The `couchbeam` module is the main interface for interaction with this application. It includes functions for managing connections to Apache CouchDB or RCOUCH servers and databases and for performing document creations, updates, deletes, views... 27 | - [`couchbeam_doc`](http://github.com/benoitc/couchbeam/blob/master/doc/couchbeam_doc.md) Module to manipulate Documents structures. You can set values, 28 | updates keys, ... 29 | - [`couchbeam_attachments`](http://github.com/benoitc/couchbeam/blob/master/doc/couchbeam_attachments.md): Module to manipulate attachments. You can add, remove 30 | attachments in a Document structure (inline attachments). 31 | - [`couchbeam_view`](http://github.com/benoitc/couchbeam/blob/master/doc/couchbeam_view.md): Module to manage view results. 32 | - [`couchbeam_changes`](http://github.com/benoitc/couchbeam/blob/master/doc/couchbeam_changes.md): Module to manage changes feeds. Follow continuously 33 | the changes in a db or get all changes at once. 34 | 35 | The goal of Couchbeam is to ease the access to the Apache CouchDB and RCOUCH HTTP API in erlang. 36 | 37 | Read the [NEWS](https://raw.github.com/benoitc/couchbeam/master/NEWS) file 38 | to get last changelog. 39 | 40 | ## Installation 41 | 42 | Download the sources from our [Github repository](http://github.com/benoitc/couchbeam) 43 | 44 | To build the application simply run 'make'. This should build .beam, .app 45 | files and documentation. 46 | 47 | To run tests run 'make test'. 48 | To generate doc, run 'make doc'. 49 | 50 | Or add it to your rebar config 51 | 52 | ``` 53 | erlang 54 | {deps, [ 55 | .... 56 | {couchbeam, ".*", {git, "git://github.com/benoitc/couchbeam.git", {branch, "master"}}} 57 | ]}. 58 | ``` 59 | 60 | Note to compile with jiffy you need to define in the erlang options the 61 | variable `WITH_JIFFY`. 62 | 63 | if you use rebar, add to your `rebar.config`: 64 | 65 | ``` 66 | erlang 67 | {erl_opts, [{d, 'WITH_JIFFY'}]}. 68 | ``` 69 | 70 | or use the `rebar` command with the `-D` options: 71 | 72 | ``` 73 | sh 74 | rebar compile -DWITH_JIFFY 75 | ``` 76 | 77 | ## Basic Usage 78 | 79 | ### Start couchbeam 80 | 81 | Couchbeam is an [OTP](http://www.erlang.org/doc/design_principles/users_guide.html) 82 | application. You have to start it first before using any of the 83 | functions. The couchbeam application will start the default socket pool 84 | for you. 85 | 86 | To start in the console run: 87 | 88 | ``` 89 | sh 90 | $ erl -pa ebin 91 | 1> couchbeam:start(). 92 | ok 93 | ``` 94 | 95 | It will start hackney and all of the application it depends on: 96 | 97 | ``` 98 | erlang 99 | application:start(crypto), 100 | application:start(asn1), 101 | application:start(public_key), 102 | application:start(ssl), 103 | application:start(hackney), 104 | application:start(couchbeam). 105 | ``` 106 | 107 | Or add couchbeam to the applications property of your .app in a release 108 | 109 | ### Create a connection to the server 110 | 111 | To create a connection to a server machine: 112 | 113 | ``` 114 | erlang 115 | Url = "http://localhost:5984", 116 | Options = [], 117 | S = couchbeam:server_connection(Url, Options). 118 | ``` 119 | 120 | Test the connection with `couchbeam:server_info/1` : 121 | 122 | ``` 123 | erlang 124 | {ok, _Version} = couchbeam:server_info(S). 125 | ``` 126 | 127 | ### Open or Create a database 128 | 129 | All document operations are done in databases. To open a database simply do: 130 | 131 | ``` 132 | erlang 133 | Options = [], 134 | {ok, Db} = couchbeam:open_db(Server, "testdb", Options). 135 | ``` 136 | 137 | To create a new one: 138 | 139 | ``` 140 | erlang 141 | Options = [], 142 | {ok, Db} = couchbeam:create_db(Server, "testdb", Options). 143 | ``` 144 | 145 | You can also use the shorcut `couchbeam:open_or_create_db/3`. that 146 | will create a database if it does not exist. 147 | 148 | ### Make a new document 149 | 150 | Make a new document: 151 | 152 | ``` 153 | erlang 154 | Doc = {[ 155 | {<<"_id">>, <<"test">>}, 156 | {<<"content">>, <<"some text">>} 157 | ]}. 158 | ``` 159 | 160 | And save it to the database: 161 | 162 | ``` 163 | erlang 164 | {ok, Doc1} = couchbeam:save_doc(Db, Doc). 165 | ``` 166 | 167 | The `couchbeam:save_doc/2` return a new document with updated 168 | revision and if you do not specify the _id, a unique document id. 169 | 170 | To change an document property use functions from `couchbeam_doc`. 171 | 172 | ### Retrieve a document 173 | 174 | To retrieve a document do: 175 | 176 | ``` 177 | erlang 178 | {ok, Doc2} = couchbeam:open_doc(Db, "test"). 179 | ``` 180 | 181 | If you want a specific revision: 182 | 183 | ``` 184 | erlang 185 | Rev = couchbeam_doc:get_rev(Doc1), 186 | Options = [{rev, Rev}], 187 | {ok, Doc3} = couchbeam:open_doc(Db, "test", Options). 188 | ``` 189 | 190 | Here we get the revision from the document we previously stored. Any 191 | options from the Apache CouchDB and RCOUCH API can be used. 192 | 193 | ### Get all documents 194 | 195 | To get all documents you have first to create an object 196 | that will keep all informations. 197 | 198 | ``` 199 | erlang 200 | Options = [include_docs], 201 | {ok, AllDocs} = couchbeam_view:all(Db, Options). 202 | ``` 203 | 204 | Ex of results: 205 | 206 | ``` 207 | erlang 208 | {ok,[{[{<<"id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>}, 209 | {<<"key">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>}, 210 | {<<"value">>, 211 | {[{<<"rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18bdb">>}]}}, 212 | {<<"doc">>, 213 | {[{<<"_id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>}, 214 | {<<"_rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18"...>>}]}}]}, 215 | ]}. 216 | ``` 217 | 218 | All functions to manipulate these results are in the `couchbeam_view` module. 219 | 220 | ### Couch DB views 221 | 222 | Views are workin like all_docs. You have to create a View object before 223 | doing anything. 224 | 225 | ``` 226 | erlang 227 | Options = [], 228 | DesignName = "designname", 229 | ViewName = "viewname", 230 | {ok, ViewResults} = couchbeam_view:fetch(Db, {DesignName, ViewName}, Options). 231 | ``` 232 | 233 | Like the `all_docs` function, use the functions 234 | from `couchbeam_view` module to manipulate results. You can pass 235 | any querying options from the [view API](http://docs.rcouch.org/en/latest/api/ddoc/views.html). 236 | 237 | Design doc are created like any documents: 238 | 239 | ``` 240 | erlang 241 | DesignDoc = {[ 242 | {<<"_id">>, <<"_design/couchbeam">>}, 243 | {<<"language">>,<<"javascript">>}, 244 | {<<"views">>, 245 | {[{<<"test">>, 246 | {[{<<"map">>, 247 | <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">> 248 | }]} 249 | },{<<"test2">>, 250 | {[{<<"map">>, 251 | <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">> 252 | }]} 253 | }]} 254 | } 255 | ]}, 256 | {ok, DesignDoc1} = couchbeam:save_doc(Db, DesignDoc). 257 | ``` 258 | 259 | You can also use [couchapp](http://github.com/couchapp/couchapp) to manage them 260 | more easily. 261 | 262 | ### Stream View results 263 | 264 | While you can get results using `couchbeam_views:fetch/2`, you can also retrieve 265 | all rows in a streaming fashion: 266 | 267 | ``` 268 | erlang 269 | ViewFun = fun(Ref, F) -> 270 | receive 271 | {Ref, done} -> 272 | io:format("done", []), 273 | done; 274 | {Ref, {row, Row}} -> 275 | io:format("got ~p~n", [Row]), 276 | F(Ref, F); 277 | {error, Ref, Error} -> 278 | io:format("error: ~p~n", [Error]) 279 | end 280 | end, 281 | 282 | {ok, StreamRef} = couchbeam_view:stream(Db, 'all_docs'), 283 | ViewFun(StreamRef, ViewFun), 284 | {ok, StreamRef2} = couchbeam_view:stream(Db, 'all_docs', [include_docs]), 285 | ViewFun(StreamRef2, ViewFun). 286 | ``` 287 | 288 | You can of course do the same with a view: 289 | 290 | ``` 291 | erlang 292 | DesignNam = "designname", 293 | ViewName = "viewname", 294 | {ok, StreamRef3} = couchbeam_view:stream(Db, {DesignNam, ViewName}, [include_docs]), 295 | ViewFun(StreamRef3, ViewFun). 296 | ``` 297 | 298 | ### Put, Fetch and Delete documents attachments 299 | 300 | You can add attachments to any documents. Attachments could be anything. 301 | 302 | To send an attachment: 303 | 304 | ``` 305 | erlang 306 | DocID = "test", 307 | AttName = "test.txt", 308 | Att = "some content I want to attach", 309 | Options = [] 310 | {ok, _Result} = couchbeam:put_attachment(Db, DocId, AttName, Att, Options). 311 | ``` 312 | 313 | All attachments are streamed to servers. `Att` could be also be an iolist 314 | or functions, see `couchbeam:put_attachment/5` for more information. 315 | 316 | To fetch an attachment: 317 | 318 | ``` 319 | erlang 320 | {ok Att1} = couchbeam:fetch_attachment(Db, DocId, AttName). 321 | ``` 322 | 323 | You can use `couchbeam:stream_fetch_attachment/6` for the stream 324 | fetch. 325 | 326 | To delete an attachment: 327 | 328 | ``` 329 | erlang 330 | {ok, Doc4} = couchbeam:open_doc(Db, DocID), 331 | ok = couchbeam:delete_attachment(Db, Doc4, AttName). 332 | ``` 333 | 334 | ### Changes 335 | 336 | Apache CouchDB and RCOUCH provide a means to get a list of changes made to documents in 337 | the database. With couchbeam you can get changes using `couchbeam_changes:follow_once/2`. 338 | This function returns all changes immediately. But you can also retrieve 339 | all changes rows using longpolling : 340 | 341 | ``` 342 | erlang 343 | Options = [], 344 | {ok, LastSeq, Rows} = couchbeam_changes:follow_once(Db, Options). 345 | ``` 346 | 347 | Options can be any Changes query parameters. See the [change API](http://docs.rcouch.org/en/latest/api/database/changes.html) for more informations. 348 | 349 | You can also get [continuous](http://docs.rcouch.org/en/latest/api/database/changes.html#continuous): 350 | 351 | ``` 352 | erlang 353 | ChangesFun = fun(StreamRef, F) -> 354 | receive 355 | {StreamRef, {done, LastSeq}} -> 356 | io:format("stopped, last seq is ~p~n", [LastSeq]), 357 | ok; 358 | {StreamRef, {change, Change}} -> 359 | io:format("change row ~p ~n", [Change]), 360 | F(StreamRef, F); 361 | {StreamRef, Error}-> 362 | io:format("error ? ~p ~n,", [Error]) 363 | end 364 | end, 365 | Options = [continuous, heartbeat], 366 | {ok, StreamRef} = couchbeam_changes:follow(Db, Options), 367 | ChangesFun(StreamRef, ChangesFun). 368 | ``` 369 | 370 | > **Note**: a `gen_changes` behaviour exists in couchbeam that you can 371 | use to create your own specific gen_server receiving changes. Have a 372 | look in the 373 | [example](https://github.com/benoitc/couchbeam/blob/master/examples/test_gen_changes.erl) 374 | for more info. 375 | 376 | ### Authentication/ Connections options 377 | 378 | You can authenticate to the database or Apache CouchDB or RCOUCH server by filling 379 | options to the Option list in `couchbeam:server_connection/4` for the 380 | server or in `couchbeam:create_db/3`, `couchbeam:open_db/3`, 381 | `couchbeam:wopen_or_create_db/3` functions. 382 | 383 | To set basic_auth on a server: 384 | 385 | ``` 386 | erlang 387 | UserName = "guest", 388 | Password = "test", 389 | Url = "http://localhost:5984", 390 | Options = [{basic_auth, {UserName, Password}}], 391 | S1 = couchbeam:server_connection(Url, Options). 392 | ``` 393 | 394 | Couchbeam support SSL, OAuth, Basic Authentication, and Proxy. You can 395 | also set a cookie. For more informations about the options have a look 396 | in the `couchbeam:server_connection/2` documentation. 397 | 398 | ## Contribute 399 | 400 | For issues, comments or feedback please [create an 401 | issue](http://github.com/benoitc/couchbeam/issues). 402 | 403 | 404 | ## Modules ## 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 |
couchbeam
couchbeam_app
couchbeam_attachments
couchbeam_changes
couchbeam_changes_stream
couchbeam_changes_sup
couchbeam_doc
couchbeam_ejson
couchbeam_httpc
couchbeam_sup
couchbeam_util
couchbeam_uuids
couchbeam_view
couchbeam_view_stream
couchbeam_view_sup
gen_changes
424 | 425 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Couchbeam - simple Apache CouchDB client library for Erlang applications # 4 | 5 | Copyright (c) 2009-2025 Benoît Chesneau. 6 | 7 | __Version:__ 1.5.0 8 | 9 | # couchbeam 10 | 11 | Couchbeam is a simple erlang library for [Barrel](https://barrel-db.org) or [Apache CouchDB](http://couchdb.apache.org). Couchbeam provides you a full featured and easy client to access and manage multiple nodes. 12 | 13 | #### Main features: 14 | 15 | - Complete support of the BarrelDB and Apache CouchDB API 16 | - Stream view results to your app 17 | - Stream changes feeds 18 | - reduced memory usage 19 | - fetch and send attachments in a streaming fashion 20 | - by default use the JSX module to encode/decode JSON 21 | - support [Jiffy](http://github.com/davisp/jiffy) a JSON encoder/decoder 22 | in C. 23 | 24 | #### Useful modules are: 25 | 26 | - [`couchbeam`](couchbeam.md): The `couchbeam` module is the main interface for interaction with this application. It includes functions for managing connections to Apache CouchDB or RCOUCH servers and databases and for performing document creations, updates, deletes, views... 27 | - [`couchbeam_doc`](couchbeam_doc.md) Module to manipulate Documents structures. You can set values, 28 | updates keys, ... 29 | - [`couchbeam_attachments`](couchbeam_attachments.md): Module to manipulate attachments. You can add, remove 30 | attachments in a Document structure (inline attachments). 31 | - [`couchbeam_view`](couchbeam_view.md): Module to manage view results. 32 | - [`couchbeam_changes`](couchbeam_changes.md): Module to manage changes feeds. Follow continuously 33 | the changes in a db or get all changes at once. 34 | 35 | The goal of Couchbeam is to ease the access to the Apache CouchDB and RCOUCH HTTP API in erlang. 36 | 37 | Read the [NEWS](https://raw.github.com/benoitc/couchbeam/master/NEWS) file 38 | to get last changelog. 39 | 40 | ## Installation 41 | 42 | Download the sources from our [Github repository](http://github.com/benoitc/couchbeam) 43 | 44 | To build the application simply run 'make'. This should build .beam, .app 45 | files and documentation. 46 | 47 | To run tests run 'make test'. 48 | To generate doc, run 'make doc'. 49 | 50 | Or add it to your rebar config 51 | 52 | ``` 53 | erlang 54 | {deps, [ 55 | .... 56 | {couchbeam, ".*", {git, "git://github.com/benoitc/couchbeam.git", {branch, "master"}}} 57 | ]}. 58 | ``` 59 | 60 | Note to compile with jiffy you need to define in the erlang options the 61 | variable `WITH_JIFFY`. 62 | 63 | if you use rebar, add to your `rebar.config`: 64 | 65 | ``` 66 | erlang 67 | {erl_opts, [{d, 'WITH_JIFFY'}]}. 68 | ``` 69 | 70 | or use the `rebar` command with the `-D` options: 71 | 72 | ``` 73 | sh 74 | rebar compile -DWITH_JIFFY 75 | ``` 76 | 77 | ## Basic Usage 78 | 79 | ### Start couchbeam 80 | 81 | Couchbeam is an [OTP](http://www.erlang.org/doc/design_principles/users_guide.html) 82 | application. You have to start it first before using any of the 83 | functions. The couchbeam application will start the default socket pool 84 | for you. 85 | 86 | To start in the console run: 87 | 88 | ``` 89 | sh 90 | $ erl -pa ebin 91 | 1> couchbeam:start(). 92 | ok 93 | ``` 94 | 95 | It will start hackney and all of the application it depends on: 96 | 97 | ``` 98 | erlang 99 | application:start(crypto), 100 | application:start(asn1), 101 | application:start(public_key), 102 | application:start(ssl), 103 | application:start(hackney), 104 | application:start(couchbeam). 105 | ``` 106 | 107 | Or add couchbeam to the applications property of your .app in a release 108 | 109 | ### Create a connection to the server 110 | 111 | To create a connection to a server machine: 112 | 113 | ``` 114 | erlang 115 | Url = "http://localhost:5984", 116 | Options = [], 117 | S = couchbeam:server_connection(Url, Options). 118 | ``` 119 | 120 | Test the connection with `couchbeam:server_info/1` : 121 | 122 | ``` 123 | erlang 124 | {ok, _Version} = couchbeam:server_info(S). 125 | ``` 126 | 127 | ### Open or Create a database 128 | 129 | All document operations are done in databases. To open a database simply do: 130 | 131 | ``` 132 | erlang 133 | Options = [], 134 | {ok, Db} = couchbeam:open_db(Server, "testdb", Options). 135 | ``` 136 | 137 | To create a new one: 138 | 139 | ``` 140 | erlang 141 | Options = [], 142 | {ok, Db} = couchbeam:create_db(Server, "testdb", Options). 143 | ``` 144 | 145 | You can also use the shorcut `couchbeam:open_or_create_db/3`. that 146 | will create a database if it does not exist. 147 | 148 | ### Make a new document 149 | 150 | Make a new document: 151 | 152 | ``` 153 | erlang 154 | Doc = {[ 155 | {<<"_id">>, <<"test">>}, 156 | {<<"content">>, <<"some text">>} 157 | ]}. 158 | ``` 159 | 160 | And save it to the database: 161 | 162 | ``` 163 | erlang 164 | {ok, Doc1} = couchbeam:save_doc(Db, Doc). 165 | ``` 166 | 167 | The `couchbeam:save_doc/2` return a new document with updated 168 | revision and if you do not specify the _id, a unique document id. 169 | 170 | To change an document property use functions from `couchbeam_doc`. 171 | 172 | ### Retrieve a document 173 | 174 | To retrieve a document do: 175 | 176 | ``` 177 | erlang 178 | {ok, Doc2} = couchbeam:open_doc(Db, "test"). 179 | ``` 180 | 181 | If you want a specific revision: 182 | 183 | ``` 184 | erlang 185 | Rev = couchbeam_doc:get_rev(Doc1), 186 | Options = [{rev, Rev}], 187 | {ok, Doc3} = couchbeam:open_doc(Db, "test", Options). 188 | ``` 189 | 190 | Here we get the revision from the document we previously stored. Any 191 | options from the Apache CouchDB and RCOUCH API can be used. 192 | 193 | ### Get all documents 194 | 195 | To get all documents you have first to create an object 196 | that will keep all informations. 197 | 198 | ``` 199 | erlang 200 | Options = [include_docs], 201 | {ok, AllDocs} = couchbeam_view:all(Db, Options). 202 | ``` 203 | 204 | Ex of results: 205 | 206 | ``` 207 | erlang 208 | {ok,[{[{<<"id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>}, 209 | {<<"key">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>}, 210 | {<<"value">>, 211 | {[{<<"rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18bdb">>}]}}, 212 | {<<"doc">>, 213 | {[{<<"_id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>}, 214 | {<<"_rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18"...>>}]}}]}, 215 | ]}. 216 | ``` 217 | 218 | All functions to manipulate these results are in the `couchbeam_view` module. 219 | 220 | ### Couch DB views 221 | 222 | Views are workin like all_docs. You have to create a View object before 223 | doing anything. 224 | 225 | ``` 226 | erlang 227 | Options = [], 228 | DesignName = "designname", 229 | ViewName = "viewname", 230 | {ok, ViewResults} = couchbeam_view:fetch(Db, {DesignName, ViewName}, Options). 231 | ``` 232 | 233 | Like the `all_docs` function, use the functions 234 | from `couchbeam_view` module to manipulate results. You can pass 235 | any querying options from the [view API](http://docs.rcouch.org/en/latest/api/ddoc/views.html). 236 | 237 | Design doc are created like any documents: 238 | 239 | ``` 240 | erlang 241 | DesignDoc = {[ 242 | {<<"_id">>, <<"_design/couchbeam">>}, 243 | {<<"language">>,<<"javascript">>}, 244 | {<<"views">>, 245 | {[{<<"test">>, 246 | {[{<<"map">>, 247 | <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">> 248 | }]} 249 | },{<<"test2">>, 250 | {[{<<"map">>, 251 | <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">> 252 | }]} 253 | }]} 254 | } 255 | ]}, 256 | {ok, DesignDoc1} = couchbeam:save_doc(Db, DesignDoc). 257 | ``` 258 | 259 | You can also use [couchapp](http://github.com/couchapp/couchapp) to manage them 260 | more easily. 261 | 262 | ### Stream View results 263 | 264 | While you can get results using `couchbeam_views:fetch/2`, you can also retrieve 265 | all rows in a streaming fashion: 266 | 267 | ``` 268 | erlang 269 | ViewFun = fun(Ref, F) -> 270 | receive 271 | {Ref, done} -> 272 | io:format("done", []), 273 | done; 274 | {Ref, {row, Row}} -> 275 | io:format("got ~p~n", [Row]), 276 | F(Ref, F); 277 | {error, Ref, Error} -> 278 | io:format("error: ~p~n", [Error]) 279 | end 280 | end, 281 | 282 | {ok, StreamRef} = couchbeam_view:stream(Db, 'all_docs'), 283 | ViewFun(StreamRef, ViewFun), 284 | {ok, StreamRef2} = couchbeam_view:stream(Db, 'all_docs', [include_docs]), 285 | ViewFun(StreamRef2, ViewFun). 286 | ``` 287 | 288 | You can of course do the same with a view: 289 | 290 | ``` 291 | erlang 292 | DesignNam = "designname", 293 | ViewName = "viewname", 294 | {ok, StreamRef3} = couchbeam_view:stream(Db, {DesignNam, ViewName}, [include_docs]), 295 | ViewFun(StreamRef3, ViewFun). 296 | ``` 297 | 298 | ### Put, Fetch and Delete documents attachments 299 | 300 | You can add attachments to any documents. Attachments could be anything. 301 | 302 | To send an attachment: 303 | 304 | ``` 305 | erlang 306 | DocID = "test", 307 | AttName = "test.txt", 308 | Att = "some content I want to attach", 309 | Options = [] 310 | {ok, _Result} = couchbeam:put_attachment(Db, DocId, AttName, Att, Options). 311 | ``` 312 | 313 | All attachments are streamed to servers. `Att` could be also be an iolist 314 | or functions, see `couchbeam:put_attachment/5` for more information. 315 | 316 | To fetch an attachment: 317 | 318 | ``` 319 | erlang 320 | {ok Att1} = couchbeam:fetch_attachment(Db, DocId, AttName). 321 | ``` 322 | 323 | You can use `couchbeam:stream_fetch_attachment/6` for the stream 324 | fetch. 325 | 326 | To delete an attachment: 327 | 328 | ``` 329 | erlang 330 | {ok, Doc4} = couchbeam:open_doc(Db, DocID), 331 | ok = couchbeam:delete_attachment(Db, Doc4, AttName). 332 | ``` 333 | 334 | ### Changes 335 | 336 | Apache CouchDB and RCOUCH provide a means to get a list of changes made to documents in 337 | the database. With couchbeam you can get changes using `couchbeam_changes:follow_once/2`. 338 | This function returns all changes immediately. But you can also retrieve 339 | all changes rows using longpolling : 340 | 341 | ``` 342 | erlang 343 | Options = [], 344 | {ok, LastSeq, Rows} = couchbeam_changes:follow_once(Db, Options). 345 | ``` 346 | 347 | Options can be any Changes query parameters. See the [change API](http://docs.rcouch.org/en/latest/api/database/changes.html) for more informations. 348 | 349 | You can also get [continuous](http://docs.rcouch.org/en/latest/api/database/changes.html#continuous): 350 | 351 | ``` 352 | erlang 353 | ChangesFun = fun(StreamRef, F) -> 354 | receive 355 | {StreamRef, {done, LastSeq}} -> 356 | io:format("stopped, last seq is ~p~n", [LastSeq]), 357 | ok; 358 | {StreamRef, {change, Change}} -> 359 | io:format("change row ~p ~n", [Change]), 360 | F(StreamRef, F); 361 | {StreamRef, Error}-> 362 | io:format("error ? ~p ~n,", [Error]) 363 | end 364 | end, 365 | Options = [continuous, heartbeat], 366 | {ok, StreamRef} = couchbeam_changes:follow(Db, Options), 367 | ChangesFun(StreamRef, ChangesFun). 368 | ``` 369 | 370 | > **Note**: a `gen_changes` behaviour exists in couchbeam that you can 371 | use to create your own specific gen_server receiving changes. Have a 372 | look in the 373 | [example](https://github.com/benoitc/couchbeam/blob/master/examples/test_gen_changes.erl) 374 | for more info. 375 | 376 | ### Authentication/ Connections options 377 | 378 | You can authenticate to the database or Apache CouchDB or RCOUCH server by filling 379 | options to the Option list in `couchbeam:server_connection/4` for the 380 | server or in `couchbeam:create_db/3`, `couchbeam:open_db/3`, 381 | `couchbeam:wopen_or_create_db/3` functions. 382 | 383 | To set basic_auth on a server: 384 | 385 | ``` 386 | erlang 387 | UserName = "guest", 388 | Password = "test", 389 | Url = "http://localhost:5984", 390 | Options = [{basic_auth, {UserName, Password}}], 391 | S1 = couchbeam:server_connection(Url, Options). 392 | ``` 393 | 394 | Couchbeam support SSL, OAuth, Basic Authentication, and Proxy. You can 395 | also set a cookie. For more informations about the options have a look 396 | in the `couchbeam:server_connection/2` documentation. 397 | 398 | ## Contribute 399 | 400 | For issues, comments or feedback please [create an 401 | issue](http://github.com/benoitc/couchbeam/issues). 402 | 403 | 404 | ## Modules ## 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 |
couchbeam
couchbeam_app
couchbeam_attachments
couchbeam_changes
couchbeam_changes_stream
couchbeam_changes_sup
couchbeam_doc
couchbeam_ejson
couchbeam_httpc
couchbeam_sup
couchbeam_util
couchbeam_uuids
couchbeam_view
couchbeam_view_stream
couchbeam_view_sup
gen_changes
424 | 425 | -------------------------------------------------------------------------------- /doc/couchbeam_app.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_app # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | __Behaviours:__ [`application`](application.md). 8 | 9 | 10 | 11 | ## Function Index ## 12 | 13 | 14 |
start/2
stop/1
15 | 16 | 17 | 18 | 19 | ## Function Details ## 20 | 21 | 22 | 23 | ### start/2 ### 24 | 25 | `start(Type, StartArgs) -> any()` 26 | 27 | 28 | 29 | ### stop/1 ### 30 | 31 | `stop(State) -> any()` 32 | 33 | -------------------------------------------------------------------------------- /doc/couchbeam_attachments.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_attachments # 4 | * [Description](#description) 5 | * [Function Index](#index) 6 | * [Function Details](#functions) 7 | 8 | This module contains utilities to manage attachments. 9 | 10 | 11 | 12 | ## Function Index ## 13 | 14 | 15 |
add_inline/3add attachment to a doc and encode it.
add_inline/4add attachment to a doc and encode it with ContentType fixed.
add_stub/3
delete_inline/2delete an attachment record in doc.
16 | 17 | 18 | 19 | 20 | ## Function Details ## 21 | 22 | 23 | 24 | ### add_inline/3 ### 25 | 26 |

27 | add_inline(Doc::json_obj(), Content::attachment_content(), AName::string()) -> json_obj()
28 | 
29 |
30 | 31 | add attachment to a doc and encode it. Give possibility to send attachments inline. 32 | 33 | 34 | 35 | ### add_inline/4 ### 36 | 37 |

38 | add_inline(Doc::json_obj(), Content::attachment_content(), AName::string(), ContentType::string()) -> json_obj()
39 | 
40 |
41 | 42 | add attachment to a doc and encode it with ContentType fixed. 43 | 44 | 45 | 46 | ### add_stub/3 ### 47 | 48 | `add_stub(Doc, Name, ContentType) -> any()` 49 | 50 | 51 | 52 | ### delete_inline/2 ### 53 | 54 |

55 | delete_inline(Doc::json_obj(), AName::string()) -> json_obj()
56 | 
57 |
58 | 59 | delete an attachment record in doc. This is different from delete_attachment 60 | change is only applied in Doc object. Save_doc should be save to save changes. 61 | 62 | -------------------------------------------------------------------------------- /doc/couchbeam_changes.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_changes # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | ## Function Index ## 10 | 11 | 12 |
cancel_stream/1
follow/1
follow/2Stream changes to a pid.
follow_once/1
follow_once/2fetch all changes at once using a normal or longpoll 13 | connections.
stream_next/1
14 | 15 | 16 | 17 | 18 | ## Function Details ## 19 | 20 | 21 | 22 | ### cancel_stream/1 ### 23 | 24 | `cancel_stream(Ref) -> any()` 25 | 26 | 27 | 28 | ### follow/1 ### 29 | 30 |

 31 | follow(Db::db()) -> {ok, StreamRef::atom()} | {error, term()}
 32 | 
33 |
34 | 35 | 36 | 37 | ### follow/2 ### 38 | 39 |

 40 | follow(Db::db(), Options::changes_options()) -> {ok, StreamRef::atom()} | {error, term()}
 41 | 
42 |
43 | 44 | Stream changes to a pid 45 | 46 | Db : a db record 47 | 48 | Client : pid or callback where to send changes events where events are 49 | The pid receive these events: 50 | 51 | 52 | 53 |
{change, StartRef, {done, Lastseq::integer()}
54 | 55 | 56 | 57 | 58 |
Connection terminated or you got all changes
59 | 60 | 61 | 62 | 63 |
{change, StartRef, Row :: ejson_object()}
64 | 65 | 66 | 67 | 68 |
Line of change
69 | 70 | 71 | 72 | 73 |
{error, LastSeq::integer(), Msg::term()}
74 | 75 | 76 | 77 | 78 |
Got an error, connection is closed when an error 79 | happend.
80 | 81 | 82 | 83 | LastSeq is the last sequence of changes. 84 | 85 | While the callbac could be like: 86 | 87 | ``` 88 | 89 | fun({done, LastSeq}) -> 90 | ok; 91 | fun({done, LastSeq}) -> 92 | ok; 93 | fun({done, LastSeq}) -> 94 | ok. 95 | ``` 96 | 97 | 98 | ``` 99 | >Options :: changes_stream_options() [continuous 100 | | longpoll 101 | | normal 102 | | include_docs 103 | | {since, integer() | now} 104 | | {timeout, integer()} 105 | | heartbeat | {heartbeat, integer()} 106 | | {filter, string()} | {filter, string(), list({string(), string() | integer()})} 107 | | {view, string()}, 108 | | {docids, list))}, 109 | | {stream_to, pid()}, 110 | | {async, once | normal}] 111 | ``` 112 | 113 | * `continuous | longpoll | normal`: set the type of changes 114 | feed to get 115 | 116 | * `include_doc`: if you want to include the doc in the line of 117 | change 118 | 119 | * `{timeout, Timeout::integer()}`: timeout 120 | 121 | * `heartbeat | {heartbeat, Heartbeat::integer()}`: set couchdb 122 | to send a heartbeat to maintain connection open 123 | 124 | * `{filter, FilterName} | {filter, FilterName, Args::list({key, 125 | value})}`: set the filter to use with optional arguments 126 | 127 | * `{view, ViewName}`: use a view function as filter. Note 128 | that it requires to set filter special value `"_view"` 129 | to enable this feature. 130 | 131 | * >`{stream_to, Pid}`: the pid where the changes will be sent, 132 | by default the current pid. Used for continuous and longpoll 133 | connections 134 | 135 | 136 | Return {ok, StartRef, ChangesPid} or {error, Error}. Ref can be 137 | used to disctint all changes from this pid. ChangesPid is the pid of 138 | the changes loop process. Can be used to monitor it or kill it 139 | when needed. 140 | 141 | 142 | 143 | ### follow_once/1 ### 144 | 145 |

146 | follow_once(Db::db()) -> {ok, LastSeq::integer(), Changes::list()} | {error, term()}
147 | 
148 |
149 | 150 | 151 | 152 | ### follow_once/2 ### 153 | 154 |

155 | follow_once(Db::db(), Options::changes_options()) -> {ok, LastSeq::integer(), Changes::list()} | {error, term()}
156 | 
157 |
158 | 159 | fetch all changes at once using a normal or longpoll 160 | connections. 161 | 162 | Db : a db record 163 | 164 | ``` 165 | Options :: changes_options() [ 166 | | longpoll 167 | | normal 168 | | include_docs 169 | | {since, integer() | now} 170 | | {timeout, integer()} 171 | | heartbeat | {heartbeat, integer()} 172 | | {filter, string()} 173 | | {filter, string(), list({string(), string() | integer()})} 174 | | {docids, list()))}, 175 | | {stream_to, pid()} 176 | ] 177 | ``` 178 | 179 | * `longpoll | normal`: set the type of changes 180 | feed to get 181 | 182 | * `include_docs`: if you want to include the doc in the line of 183 | change 184 | 185 | * `{timeout, Timeout::integer()}`: timeout 186 | 187 | * `heartbeat | {heartbeat, Heartbeat::integer()}`: set couchdb 188 | to send a heartbeat to maintain connection open 189 | 190 | * `{filter, FilterName} | {filter, FilterName, Args::list({key, 191 | value})`: set the filter to use with optional arguments 192 | 193 | * `{view, ViewName}`: use a view function as filter. Note 194 | that it requires to set filter special value `"_view"` 195 | to enable this feature. 196 | 197 | 198 | Result: `{ok, LastSeq::integer(), Rows::list()}` or 199 | `{error, LastSeq, Error}`. LastSeq is the last sequence of changes. 200 | 201 | 202 | 203 | ### stream_next/1 ### 204 | 205 | `stream_next(Ref) -> any()` 206 | 207 | -------------------------------------------------------------------------------- /doc/couchbeam_changes_stream.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_changes_stream # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | ## Function Index ## 10 | 11 | 12 |
collect_object/2
handle_event/2
init/1
init_stream/5
maybe_continue/1
maybe_continue_decoding/1
start_link/4
system_code_change/4
system_continue/3
system_terminate/4
wait_reconnect/1
wait_results/2
wait_results1/2
13 | 14 | 15 | 16 | 17 | ## Function Details ## 18 | 19 | 20 | 21 | ### collect_object/2 ### 22 | 23 | `collect_object(X1, X2) -> any()` 24 | 25 | 26 | 27 | ### handle_event/2 ### 28 | 29 | `handle_event(Event, St) -> any()` 30 | 31 | 32 | 33 | ### init/1 ### 34 | 35 | `init(X1) -> any()` 36 | 37 | 38 | 39 | ### init_stream/5 ### 40 | 41 | `init_stream(Parent, Owner, StreamRef, Db, Options) -> any()` 42 | 43 | 44 | 45 | ### maybe_continue/1 ### 46 | 47 | `maybe_continue(State) -> any()` 48 | 49 | 50 | 51 | ### maybe_continue_decoding/1 ### 52 | 53 | `maybe_continue_decoding(State) -> any()` 54 | 55 | 56 | 57 | ### start_link/4 ### 58 | 59 | `start_link(Owner, StreamRef, Db, Options) -> any()` 60 | 61 | 62 | 63 | ### system_code_change/4 ### 64 | 65 | `system_code_change(Misc, X2, X3, X4) -> any()` 66 | 67 | 68 | 69 | ### system_continue/3 ### 70 | 71 | `system_continue(X1, X2, X3) -> any()` 72 | 73 | 74 | 75 | ### system_terminate/4 ### 76 | 77 |

 78 | system_terminate(Reason::any(), X2::term(), X3::term(), State::term()) -> no_return()
 79 | 
80 |
81 | 82 | 83 | 84 | ### wait_reconnect/1 ### 85 | 86 | `wait_reconnect(State) -> any()` 87 | 88 | 89 | 90 | ### wait_results/2 ### 91 | 92 | `wait_results(X1, St) -> any()` 93 | 94 | 95 | 96 | ### wait_results1/2 ### 97 | 98 | `wait_results1(X1, X2) -> any()` 99 | 100 | -------------------------------------------------------------------------------- /doc/couchbeam_changes_sup.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_changes_sup # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | __Behaviours:__ [`supervisor`](supervisor.md). 8 | 9 | 10 | 11 | ## Function Index ## 12 | 13 | 14 |
init/1
start_link/0
15 | 16 | 17 | 18 | 19 | ## Function Details ## 20 | 21 | 22 | 23 | ### init/1 ### 24 | 25 | `init(X1) -> any()` 26 | 27 | 28 | 29 | ### start_link/0 ### 30 | 31 |

32 | start_link() -> {ok, pid()}
33 | 
34 |
35 | 36 | -------------------------------------------------------------------------------- /doc/couchbeam_deps.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_deps # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | 10 | ## Function Index ## 11 | 12 | 13 |
deps_on_path/0List of project dependencies on the path.
ensure/0Ensure that the ebin and include paths for dependencies of 14 | this application are on the code path.
ensure/1Ensure that all ebin and include paths for dependencies 15 | of the application for Module are on the code path.
get_base_dir/0Return the application directory for this application.
get_base_dir/1Return the application directory for Module.
local_path/1Return an application-relative directory for this application.
local_path/2Return an application-relative directory from Module's application.
new_siblings/1Find new siblings paths relative to Module that aren't already on the 16 | code path.
17 | 18 | 19 | 20 | 21 | ## Function Details ## 22 | 23 | 24 | 25 | ### deps_on_path/0 ### 26 | 27 | 28 |

 29 | deps_on_path() -> [ProjNameAndVers]
 30 | 
31 |
32 | 33 | List of project dependencies on the path. 34 | 35 | 36 | ### ensure/0 ### 37 | 38 | 39 |

 40 | ensure() -> ok
 41 | 
42 |
43 | 44 | Ensure that the ebin and include paths for dependencies of 45 | this application are on the code path. Equivalent to 46 | ensure(?Module). 47 | 48 | 49 | ### ensure/1 ### 50 | 51 | 52 |

 53 | ensure(Module) -> ok
 54 | 
55 |
56 | 57 | Ensure that all ebin and include paths for dependencies 58 | of the application for Module are on the code path. 59 | 60 | 61 | ### get_base_dir/0 ### 62 | 63 | 64 |

 65 | get_base_dir() -> string()
 66 | 
67 |
68 | 69 | Return the application directory for this application. Equivalent to 70 | get_base_dir(?MODULE). 71 | 72 | 73 | ### get_base_dir/1 ### 74 | 75 | 76 |

 77 | get_base_dir(Module) -> string()
 78 | 
79 |
80 | 81 | Return the application directory for Module. It assumes Module is in 82 | a standard OTP layout application in the ebin or src directory. 83 | 84 | 85 | ### local_path/1 ### 86 | 87 | 88 |

 89 | local_path(Components) -> string()
 90 | 
91 |
92 | 93 | Return an application-relative directory for this application. 94 | Equivalent to local_path(Components, ?MODULE). 95 | 96 | 97 | ### local_path/2 ### 98 | 99 | 100 |

101 | local_path(Components::[string()], Module) -> string()
102 | 
103 |
104 | 105 | Return an application-relative directory from Module's application. 106 | 107 | 108 | ### new_siblings/1 ### 109 | 110 | 111 |

112 | new_siblings(Module) -> [Dir]
113 | 
114 |
115 | 116 | Find new siblings paths relative to Module that aren't already on the 117 | code path. 118 | -------------------------------------------------------------------------------- /doc/couchbeam_doc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_doc # 4 | * [Data Types](#types) 5 | * [Function Index](#index) 6 | * [Function Details](#functions) 7 | 8 | 9 | 10 | ## Data Types ## 11 | 12 | 13 | 14 | 15 | ### key_val() ### 16 | 17 | 18 |

 19 | key_val() = lis() | binary()
 20 | 
21 | 22 | 23 | 24 | 25 | ### property() ### 26 | 27 | 28 |

 29 | property() = json_obj() | tuple()
 30 | 
31 | 32 | 33 | 34 | ## Function Index ## 35 | 36 | 37 |
delete_value/2Deletes all entries associated with Key in json object.
extend/2extend a jsonobject by a property, list of property or another jsonobject.
extend/3extend a jsonobject by key, value.
get_id/1get document id.
get_idrev/1get a tuple containing docucment id and revision.
get_rev/1get document revision.
get_value/2Returns the value of a simple key/value property in json object 38 | Equivalent to get_value(Key, JsonObj, undefined).
get_value/3Returns the value of a simple key/value property in json object 39 | function from erlang_couchdb.
is_saved/1If document have been saved (revision is defined) return true, 40 | else, return false.
set_value/3set a value for a key in jsonobj.
take_value/2Returns the value of a simple key/value property in json object and deletes 41 | it form json object 42 | Equivalent to take_value(Key, JsonObj, undefined).
take_value/3Returns the value of a simple key/value property in json object and deletes 43 | it from json object.
44 | 45 | 46 | 47 | 48 | ## Function Details ## 49 | 50 | 51 | 52 | ### delete_value/2 ### 53 | 54 |

 55 | delete_value(Key::key_val(), JsonObj::json_obj()) -> json_obj()
 56 | 
57 |
58 | 59 | Deletes all entries associated with Key in json object. 60 | 61 | 62 | 63 | ### extend/2 ### 64 | 65 |

 66 | extend(Prop::property(), JsonObj::json_obj()) -> json_obj()
 67 | 
68 |
69 | 70 | extend a jsonobject by a property, list of property or another jsonobject 71 | 72 | 73 | 74 | ### extend/3 ### 75 | 76 |

 77 | extend(Key::binary(), Value::json_term(), JsonObj::json_obj()) -> json_obj()
 78 | 
79 |
80 | 81 | extend a jsonobject by key, value 82 | 83 | 84 | 85 | ### get_id/1 ### 86 | 87 |

 88 | get_id(Doc::json_obj()) -> binary()
 89 | 
90 |
91 | 92 | get document id. 93 | 94 | 95 | 96 | ### get_idrev/1 ### 97 | 98 |

 99 | get_idrev(Doc::json_obj()) -> {DocId, DocRev}
100 | 
101 |
102 | 103 | get a tuple containing docucment id and revision. 104 | 105 | 106 | 107 | ### get_rev/1 ### 108 | 109 |

110 | get_rev(Doc::json_obj()) -> binary()
111 | 
112 |
113 | 114 | get document revision. 115 | 116 | 117 | 118 | ### get_value/2 ### 119 | 120 |

121 | get_value(Key::key_val(), JsonObj::json_obj()) -> term()
122 | 
123 |
124 | 125 | Returns the value of a simple key/value property in json object 126 | Equivalent to get_value(Key, JsonObj, undefined). 127 | 128 | 129 | 130 | ### get_value/3 ### 131 | 132 |

133 | get_value(Key::lis() | binary(), JsonObj::json_obj(), Default::term()) -> term()
134 | 
135 |
136 | 137 | Returns the value of a simple key/value property in json object 138 | function from erlang_couchdb 139 | 140 | 141 | 142 | ### is_saved/1 ### 143 | 144 |

145 | is_saved(Doc::json_obj()) -> boolean()
146 | 
147 |
148 | 149 | If document have been saved (revision is defined) return true, 150 | else, return false. 151 | 152 | 153 | 154 | ### set_value/3 ### 155 | 156 |

157 | set_value(Key::key_val(), Value::term(), JsonObj::json_obj()) -> term()
158 | 
159 |
160 | 161 | set a value for a key in jsonobj. If key exists it will be updated. 162 | 163 | 164 | 165 | ### take_value/2 ### 166 | 167 |

168 | take_value(Key::key_val(), JsonObj::json_obj()) -> {term(), json_obj()}
169 | 
170 |
171 | 172 | Returns the value of a simple key/value property in json object and deletes 173 | it form json object 174 | Equivalent to take_value(Key, JsonObj, undefined). 175 | 176 | 177 | 178 | ### take_value/3 ### 179 | 180 |

181 | take_value(Key::key_val() | binary(), JsonObj::json_obj(), Default::term()) -> {term(), json_obj()}
182 | 
183 |
184 | 185 | Returns the value of a simple key/value property in json object and deletes 186 | it from json object 187 | 188 | -------------------------------------------------------------------------------- /doc/couchbeam_ejson.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_ejson # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | ## Function Index ## 10 | 11 | 12 |
decode/1decode a binary to an EJSON term.
encode/1encode an erlang term to JSON.
post_decode/1
13 | 14 | 15 | 16 | 17 | ## Function Details ## 18 | 19 | 20 | 21 | ### decode/1 ### 22 | 23 |

24 | decode(D::binary()) -> ejson()
25 | 
26 |
27 | 28 | decode a binary to an EJSON term. Throw an exception if there is 29 | any error. 30 | 31 | 32 | 33 | ### encode/1 ### 34 | 35 |

36 | encode(D::ejson()) -> binary()
37 | 
38 |
39 | 40 | encode an erlang term to JSON. Throw an exception if there is 41 | any error. 42 | 43 | 44 | 45 | ### post_decode/1 ### 46 | 47 | `post_decode(Rest) -> any()` 48 | 49 | -------------------------------------------------------------------------------- /doc/couchbeam_httpc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_httpc # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | ## Function Index ## 10 | 11 | 12 |
db_request/5
db_request/6
db_resp/2
db_url/1
doc_url/2
json_body/1
make_headers/4
maybe_oauth_header/4
request/5
server_url/1Asemble the server URL for the given client.
13 | 14 | 15 | 16 | 17 | ## Function Details ## 18 | 19 | 20 | 21 | ### db_request/5 ### 22 | 23 | `db_request(Method, Url, Headers, Body, Options) -> any()` 24 | 25 | 26 | 27 | ### db_request/6 ### 28 | 29 | `db_request(Method, Url, Headers, Body, Options, Expect) -> any()` 30 | 31 | 32 | 33 | ### db_resp/2 ### 34 | 35 | `db_resp(Resp, Expect) -> any()` 36 | 37 | 38 | 39 | ### db_url/1 ### 40 | 41 | `db_url(Db) -> any()` 42 | 43 | 44 | 45 | ### doc_url/2 ### 46 | 47 | `doc_url(Db, DocId) -> any()` 48 | 49 | 50 | 51 | ### json_body/1 ### 52 | 53 | `json_body(Ref) -> any()` 54 | 55 | 56 | 57 | ### make_headers/4 ### 58 | 59 | `make_headers(Method, Url, Headers, Options) -> any()` 60 | 61 | 62 | 63 | ### maybe_oauth_header/4 ### 64 | 65 | `maybe_oauth_header(Method, Url, Headers, Options) -> any()` 66 | 67 | 68 | 69 | ### request/5 ### 70 | 71 | `request(Method, Url, Headers, Body, Options) -> any()` 72 | 73 | 74 | 75 | ### server_url/1 ### 76 | 77 |

78 | server_url(Server::{Host, Port}) -> iolist()
79 | 
80 |
81 | 82 | Asemble the server URL for the given client 83 | 84 | -------------------------------------------------------------------------------- /doc/couchbeam_sup.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_sup # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | __Behaviours:__ [`supervisor`](supervisor.md). 8 | 9 | 10 | 11 | ## Function Index ## 12 | 13 | 14 |
init/1
start_link/0
15 | 16 | 17 | 18 | 19 | ## Function Details ## 20 | 21 | 22 | 23 | ### init/1 ### 24 | 25 | `init(X1) -> any()` 26 | 27 | 28 | 29 | ### start_link/0 ### 30 | 31 | `start_link() -> any()` 32 | 33 | -------------------------------------------------------------------------------- /doc/couchbeam_util.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_util # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | ## Function Index ## 10 | 11 | 12 |
binary_env/2
dbname/1
deprecated/3
encode_att_name/1
encode_docid/1
encode_docid1/1
encode_docid_noop/1
encode_query/1Encode needed value of Query proplists in json.
encode_query_value/2Encode value in JSON if needed depending on the key.
force_param/3replace a value in a proplist.
get_app_env/2
get_value/2emulate proplists:get_value/2,3 but use faster lists:keyfind/3.
get_value/3
oauth_header/3
parse_options/1make view options a list.
parse_options/2
propmerge/3merge 2 proplists.
propmerge1/2Update a proplist with values of the second.
proxy_header/3
proxy_token/2
shutdown_sync/1
start_app_deps/1Start depedent applications of App.
to_atom/1
to_binary/1
to_integer/1
to_list/1
13 | 14 | 15 | 16 | 17 | ## Function Details ## 18 | 19 | 20 | 21 | ### binary_env/2 ### 22 | 23 | `binary_env(Key, Default) -> any()` 24 | 25 | 26 | 27 | ### dbname/1 ### 28 | 29 | `dbname(DbName) -> any()` 30 | 31 | 32 | 33 | ### deprecated/3 ### 34 | 35 | `deprecated(Old, New, When) -> any()` 36 | 37 | 38 | 39 | ### encode_att_name/1 ### 40 | 41 | `encode_att_name(Name) -> any()` 42 | 43 | 44 | 45 | ### encode_docid/1 ### 46 | 47 | `encode_docid(DocId) -> any()` 48 | 49 | 50 | 51 | ### encode_docid1/1 ### 52 | 53 | `encode_docid1(DocId) -> any()` 54 | 55 | 56 | 57 | ### encode_docid_noop/1 ### 58 | 59 | `encode_docid_noop(DocId) -> any()` 60 | 61 | 62 | 63 | ### encode_query/1 ### 64 | 65 | `encode_query(QSL) -> any()` 66 | 67 | Encode needed value of Query proplists in json 68 | 69 | 70 | 71 | ### encode_query_value/2 ### 72 | 73 | `encode_query_value(K, V) -> any()` 74 | 75 | Encode value in JSON if needed depending on the key 76 | 77 | 78 | 79 | ### force_param/3 ### 80 | 81 | `force_param(Key, Value, Options) -> any()` 82 | 83 | replace a value in a proplist 84 | 85 | 86 | 87 | ### get_app_env/2 ### 88 | 89 | `get_app_env(Env, Default) -> any()` 90 | 91 | 92 | 93 | ### get_value/2 ### 94 | 95 |

 96 | get_value(Key::term(), Prop::[term()]) -> term()
 97 | 
98 |
99 | 100 | emulate proplists:get_value/2,3 but use faster lists:keyfind/3 101 | 102 | 103 | 104 | ### get_value/3 ### 105 | 106 |

107 | get_value(Key::term(), Prop::[term()], Default::term()) -> term()
108 | 
109 |
110 | 111 | 112 | 113 | ### oauth_header/3 ### 114 | 115 | `oauth_header(Url, Action, OauthProps) -> any()` 116 | 117 | 118 | 119 | ### parse_options/1 ### 120 | 121 | `parse_options(Options) -> any()` 122 | 123 | make view options a list 124 | 125 | 126 | 127 | ### parse_options/2 ### 128 | 129 | `parse_options(Rest, Acc) -> any()` 130 | 131 | 132 | 133 | ### propmerge/3 ### 134 | 135 | `propmerge(F, L1, L2) -> any()` 136 | 137 | merge 2 proplists. All the Key - Value pairs from both proplists 138 | are included in the new proplists. If a key occurs in both dictionaries 139 | then Fun is called with the key and both values to return a new 140 | value. This a wreapper around dict:merge 141 | 142 | 143 | 144 | ### propmerge1/2 ### 145 | 146 | `propmerge1(L1, L2) -> any()` 147 | 148 | Update a proplist with values of the second. In case the same 149 | key is in 2 proplists, the value from the first are kept. 150 | 151 | 152 | 153 | ### proxy_header/3 ### 154 | 155 | `proxy_header(UserName, Roles, Secret) -> any()` 156 | 157 | 158 | 159 | ### proxy_token/2 ### 160 | 161 | `proxy_token(Secret, UserName) -> any()` 162 | 163 | 164 | 165 | ### shutdown_sync/1 ### 166 | 167 | `shutdown_sync(Pid) -> any()` 168 | 169 | 170 | 171 | ### start_app_deps/1 ### 172 | 173 |

174 | start_app_deps(App::atom()) -> ok
175 | 
176 |
177 | 178 | Start depedent applications of App. 179 | 180 | 181 | 182 | ### to_atom/1 ### 183 | 184 | `to_atom(V) -> any()` 185 | 186 | 187 | 188 | ### to_binary/1 ### 189 | 190 | `to_binary(V) -> any()` 191 | 192 | 193 | 194 | ### to_integer/1 ### 195 | 196 | `to_integer(V) -> any()` 197 | 198 | 199 | 200 | ### to_list/1 ### 201 | 202 | `to_list(V) -> any()` 203 | 204 | -------------------------------------------------------------------------------- /doc/couchbeam_uuids.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_uuids # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | __Behaviours:__ [`gen_server`](gen_server.md). 8 | 9 | 10 | 11 | ## Function Index ## 12 | 13 | 14 |
code_change/3
get_uuids/2Get a list of uuids from the server.
handle_call/3
handle_cast/2
handle_info/2
random/0return a random uuid.
start_link/0Starts the couchbeam process linked to the calling process.
terminate/2
utc_random/0return a random uuid based on time.
15 | 16 | 17 | 18 | 19 | ## Function Details ## 20 | 21 | 22 | 23 | ### code_change/3 ### 24 | 25 | `code_change(OldVsn, State, Extra) -> any()` 26 | 27 | 28 | 29 | ### get_uuids/2 ### 30 | 31 |

32 | get_uuids(Server::server(), Count::integer()) -> lists()
33 | 
34 |
35 | 36 | Get a list of uuids from the server 37 | 38 | 39 | 40 | ### handle_call/3 ### 41 | 42 | `handle_call(X1, From, State) -> any()` 43 | 44 | 45 | 46 | ### handle_cast/2 ### 47 | 48 | `handle_cast(Msg, State) -> any()` 49 | 50 | 51 | 52 | ### handle_info/2 ### 53 | 54 | `handle_info(Info, State) -> any()` 55 | 56 | 57 | 58 | ### random/0 ### 59 | 60 |

61 | random() -> binary()
62 | 
63 |
64 | 65 | return a random uuid 66 | 67 | 68 | 69 | ### start_link/0 ### 70 | 71 |

72 | start_link() -> {ok, pid()}
73 | 
74 |
75 | 76 | Starts the couchbeam process linked to the calling process. Usually 77 | invoked by the supervisor couchbeam_sup 78 | 79 | 80 | 81 | ### terminate/2 ### 82 | 83 | `terminate(Reason, State) -> any()` 84 | 85 | 86 | 87 | ### utc_random/0 ### 88 | 89 |

90 | utc_random() -> binary()
91 | 
92 |
93 | 94 | return a random uuid based on time 95 | 96 | -------------------------------------------------------------------------------- /doc/couchbeam_view.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_view # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | ## Function Index ## 10 | 11 | 12 |
all/1fetch all docs.
all/2fetch all docs.
cancel_stream/1
count/1Equivalent to count(Db, all_docs, []).
count/2Equivalent to count(Db, ViewName, []).
count/3count number of doc in a view (or all docs).
fetch/1Equivalent to fetch(Db, all_docs, []).
fetch/2Equivalent to fetch(Db, ViewName, []).
fetch/3Collect view results.
first/1Equivalent to first(Db, all_docs, []).
first/2Equivalent to first(Db, ViewName, []).
first/3get first result of a view.
fold/4Equivalent to fold(Function, Acc, Db, ViewName, []).
fold/5call Function(Row, AccIn) on succesive row, starting with 13 | AccIn == Acc.
foreach/3Equivalent to foreach(Function, Db, ViewName, []).
foreach/4call Function(Row) on succesive row.
parse_view_options/1parse view options.
stream/2Equivalent to stream(Db, ViewName, Client, []).
stream/3stream view results to a pid.
stream_next/1
14 | 15 | 16 | 17 | 18 | ## Function Details ## 19 | 20 | 21 | 22 | ### all/1 ### 23 | 24 |

 25 | all(Db::db()) -> {ok, Rows::[ejson_object()]} | {error, term()}
 26 | 
27 |
28 | 29 | Equivalent to [`fetch(Db, all_docs, [])`](#fetch-3). 30 | 31 | fetch all docs 32 | 33 | 34 | 35 | ### all/2 ### 36 | 37 |

 38 | all(Db::db(), Options::view_options()) -> {ok, Rows::[ejson_object()]} | {error, term()}
 39 | 
40 |
41 | 42 | Equivalent to [`fetch(Db, all_docs, Options)`](#fetch-3). 43 | 44 | fetch all docs 45 | 46 | 47 | 48 | ### cancel_stream/1 ### 49 | 50 | `cancel_stream(Ref) -> any()` 51 | 52 | 53 | 54 | ### count/1 ### 55 | 56 |

 57 | count(Db::db()) -> integer() | {error, term()}
 58 | 
59 |
60 | 61 | Equivalent to [`count(Db, all_docs, [])`](#count-3). 62 | 63 | 64 | 65 | ### count/2 ### 66 | 67 |

 68 | count(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> integer() | {error, term()}
 69 | 
70 |
71 | 72 | Equivalent to [`count(Db, ViewName, [])`](#count-3). 73 | 74 | 75 | 76 | ### count/3 ### 77 | 78 |

 79 | count(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> integer() | {error, term()}
 80 | 
81 |
82 | 83 | count number of doc in a view (or all docs) 84 | 85 | 86 | 87 | ### fetch/1 ### 88 | 89 |

 90 | fetch(Db::db()) -> {ok, Rows::[ejson_object()]} | {error, term()}
 91 | 
92 |
93 | 94 | Equivalent to [`fetch(Db, all_docs, [])`](#fetch-3). 95 | 96 | 97 | 98 | ### fetch/2 ### 99 | 100 |

101 | fetch(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> {ok, Rows::[ejson_object()]} | {error, term()}
102 | 
103 |
104 | 105 | Equivalent to [`fetch(Db, ViewName, [])`](#fetch-3). 106 | 107 | 108 | 109 | ### fetch/3 ### 110 | 111 |

112 | fetch(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> {ok, Rows::[ejson_object()]} | {error, term()}
113 | 
114 |
115 | 116 | Collect view results 117 | 118 | Db: a db record 119 | 120 | ViewName: `'all_docs'` to get all docs or `{DesignName, 121 | ViewName}` 122 | 123 | 124 | ``` 125 | Options :: view_options() [{key, binary()} 126 | | {start_docid, binary()} | {startkey_docid, binary()} 127 | | {end_docid, binary()} | {endkey_docid, binary()} 128 | | {start_key, binary()} | {end_key, binary()} 129 | | {limit, integer()} 130 | | {stale, stale()} 131 | | descending 132 | | {skip, integer()} 133 | | group | {group_level, integer()} 134 | | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts 135 | | {keys, list(binary())} 136 | ``` 137 | 138 | 139 | See [`couchbeam_view:stream/4`](couchbeam_view.md#stream-4) for more information about 140 | options. 141 | 142 | Return: {ok, Rows} or {error, Error} 143 | 144 | 145 | 146 | ### first/1 ### 147 | 148 |

149 | first(Db::db()) -> {ok, Row::ejson_object()} | {error, term()}
150 | 
151 |
152 | 153 | Equivalent to [`first(Db, all_docs, [])`](#first-3). 154 | 155 | 156 | 157 | ### first/2 ### 158 | 159 |

160 | first(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> {ok, Row::ejson_object()} | {error, term()}
161 | 
162 |
163 | 164 | Equivalent to [`first(Db, ViewName, [])`](#first-3). 165 | 166 | 167 | 168 | ### first/3 ### 169 | 170 |

171 | first(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> {ok, Rows::ejson_object()} | {error, term()}
172 | 
173 |
174 | 175 | get first result of a view 176 | 177 | Db: a db record 178 | 179 | ViewName: 'all_docs' to get all docs or {DesignName, 180 | ViewName} 181 | 182 | 183 | ``` 184 | Options :: view_options() [{key, binary()} 185 | | {start_docid, binary()} | {startkey_docid, binary()} 186 | | {end_docid, binary()} | {endkey_docid, binary()} 187 | | {start_key, binary()} | {end_key, binary()} 188 | | {limit, integer()} 189 | | {stale, stale()} 190 | | descending 191 | | {skip, integer()} 192 | | group | {group_level, integer()} 193 | | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts 194 | | {keys, list(binary())} 195 | ``` 196 | 197 | 198 | See [`couchbeam_view:stream/4`](couchbeam_view.md#stream-4) for more information about 199 | options. 200 | 201 | Return: {ok, Row} or {error, Error} 202 | 203 | 204 | 205 | ### fold/4 ### 206 | 207 |

208 | fold(Function::function(), Acc::any(), Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> [term()] | {error, term()}
209 | 
210 |
211 | 212 | Equivalent to [`fold(Function, Acc, Db, ViewName, [])`](#fold-5). 213 | 214 | 215 | 216 | ### fold/5 ### 217 | 218 |

219 | fold(Function::function(), Acc::any(), Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> [term()] | {error, term()}
220 | 
221 |
222 | 223 | call Function(Row, AccIn) on succesive row, starting with 224 | AccIn == Acc. Function/2 must return a new list accumultator or the 225 | atom _done_ to stop fetching results. Acc0 is returned if the 226 | list is empty. For example: 227 | 228 | ``` 229 | couchbeam_view:fold(fun(Row, Acc) -> [Row|Acc] end, [], Db, 'all_docs'). 230 | ``` 231 | 232 | 233 | 234 | ### foreach/3 ### 235 | 236 |

237 | foreach(Function::function(), Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> [term()] | {error, term()}
238 | 
239 |
240 | 241 | Equivalent to [`foreach(Function, Db, ViewName, [])`](#foreach-4). 242 | 243 | 244 | 245 | ### foreach/4 ### 246 | 247 |

248 | foreach(Function::function(), Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> [term()] | {error, term()}
249 | 
250 |
251 | 252 | call Function(Row) on succesive row. Example: 253 | 254 | ``` 255 | couchbeam_view:foreach(fun(Row) -> io:format("got row ~p~n", [Row]) end, Db, 'all_docs'). 256 | ``` 257 | 258 | 259 | 260 | ### parse_view_options/1 ### 261 | 262 |

263 | parse_view_options(Options::list()) -> view_query_args()
264 | 
265 |
266 | 267 | parse view options 268 | 269 | 270 | 271 | ### stream/2 ### 272 | 273 |

274 | stream(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}) -> {ok, StartRef::term(), ViewPid::pid()} | {error, term()}
275 | 
276 |
277 | 278 | Equivalent to [`stream(Db, ViewName, Client, [])`](#stream-4). 279 | 280 | 281 | 282 | ### stream/3 ### 283 | 284 |

285 | stream(Db::db(), ViewName::all_docs | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> {ok, StartRef::term()} | {error, term()}
286 | 
287 |
288 | 289 | stream view results to a pid 290 | 291 | Db: a db record 292 | 293 | ViewName: 'all_docs' to get all docs or {DesignName, 294 | ViewName} 295 | 296 | Client: pid where to send view events where events are: 297 | 298 | 299 | 300 |
{row, StartRef, done}
301 | 302 | 303 | 304 | 305 |
All view results have been fetched
306 | 307 | 308 | 309 | 310 |
{row, StartRef, Row :: ejson_object()}
311 | 312 | 313 | 314 | 315 |
A row in the view
316 | 317 | 318 | 319 | 320 |
{error, StartRef, Error}
321 | 322 | 323 | 324 | 325 |
Got an error, connection is closed when an error 326 | happend.
327 | 328 | 329 | 330 | ``` 331 | Options :: view_options() [{key, binary()} 332 | | {start_docid, binary()} | {startkey_docid, binary()} 333 | | {end_docid, binary()} | {endkey_docid, binary()} 334 | | {start_key, binary()} | {end_key, binary()} 335 | | {limit, integer()} 336 | | {stale, stale()} 337 | | descending 338 | | {skip, integer()} 339 | | group | {group_level, integer()} 340 | | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts 341 | | {keys, list(binary())} 342 | | {stream_to, Pid}: the pid where the changes will be sent, 343 | by default the current pid. Used for continuous and longpoll 344 | connections 345 | ``` 346 | 347 | * `{key, Key}`: key value 348 | 349 | * `{start_docid, DocId}` | `{startkey_docid, DocId}`: document id to start with (to allow pagination 350 | for duplicate start keys 351 | 352 | * `{end_docid, DocId}` | `{endkey_docid, DocId}`: last document id to include in the result (to 353 | allow pagination for duplicate endkeys) 354 | 355 | * `{start_key, Key}`: start result from key value 356 | 357 | * `{end_key, Key}`: end result from key value 358 | 359 | * `{limit, Limit}`: Limit the number of documents in the result 360 | 361 | * `{stale, Stale}`: If stale=ok is set, CouchDB will not refresh the view 362 | even if it is stale, the benefit is a an improved query latency. If 363 | stale=update_after is set, CouchDB will update the view after the stale 364 | result is returned. If stale=false is set, CouchDB will update the view before 365 | the query. The default value of this parameter is update_after. 366 | 367 | * `descending`: reverse the result 368 | 369 | * `{skip, N}`: skip n number of documents 370 | 371 | * `group`: the reduce function reduces to a single result 372 | row. 373 | 374 | * `{group_level, Level}`: the reduce function reduces to a set 375 | of distinct keys. 376 | 377 | * `{reduce, boolean()}`: whether to use the reduce function of the view. It defaults to 378 | true, if a reduce function is defined and to false otherwise. 379 | 380 | * `include_docs`: automatically fetch and include the document 381 | which emitted each view entry 382 | 383 | * `{inclusive_end, boolean()}`: Controls whether the endkey is included in 384 | the result. It defaults to true. 385 | 386 | * `conflicts`: include conflicts 387 | 388 | * `{keys, [Keys]}`: to pass multiple keys to the view query 389 | 390 | 391 | Return `{ok, StartRef, ViewPid}` or `{error, 392 | Error}`. Ref can be 393 | used to disctint all changes from this pid. ViewPid is the pid of 394 | the view loop process. Can be used to monitor it or kill it 395 | when needed. 396 | 397 | 398 | 399 | ### stream_next/1 ### 400 | 401 | `stream_next(Ref) -> any()` 402 | 403 | -------------------------------------------------------------------------------- /doc/couchbeam_view_stream.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_view_stream # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | ## Function Index ## 10 | 11 | 12 |
collect_object/2
handle_event/2
init/1
init_stream/5
maybe_continue/1
maybe_continue_decoding/1
start_link/4
system_code_change/4
system_continue/3
system_terminate/4
wait_rows/2
wait_rows1/2
wait_val/2
13 | 14 | 15 | 16 | 17 | ## Function Details ## 18 | 19 | 20 | 21 | ### collect_object/2 ### 22 | 23 | `collect_object(X1, X2) -> any()` 24 | 25 | 26 | 27 | ### handle_event/2 ### 28 | 29 | `handle_event(Event, St) -> any()` 30 | 31 | 32 | 33 | ### init/1 ### 34 | 35 | `init(X1) -> any()` 36 | 37 | 38 | 39 | ### init_stream/5 ### 40 | 41 | `init_stream(Parent, Owner, StreamRef, Req, StreamOptions) -> any()` 42 | 43 | 44 | 45 | ### maybe_continue/1 ### 46 | 47 | `maybe_continue(State) -> any()` 48 | 49 | 50 | 51 | ### maybe_continue_decoding/1 ### 52 | 53 | `maybe_continue_decoding(Viewst) -> any()` 54 | 55 | 56 | 57 | ### start_link/4 ### 58 | 59 | `start_link(Owner, StreamRef, X3, StreamOptions) -> any()` 60 | 61 | 62 | 63 | ### system_code_change/4 ### 64 | 65 | `system_code_change(Misc, X2, X3, X4) -> any()` 66 | 67 | 68 | 69 | ### system_continue/3 ### 70 | 71 | `system_continue(X1, X2, X3) -> any()` 72 | 73 | 74 | 75 | ### system_terminate/4 ### 76 | 77 |

 78 | system_terminate(Reason::any(), X2::term(), X3::term(), State::term()) -> no_return()
 79 | 
80 |
81 | 82 | 83 | 84 | ### wait_rows/2 ### 85 | 86 | `wait_rows(X1, St) -> any()` 87 | 88 | 89 | 90 | ### wait_rows1/2 ### 91 | 92 | `wait_rows1(X1, X2) -> any()` 93 | 94 | 95 | 96 | ### wait_val/2 ### 97 | 98 | `wait_val(X1, X2) -> any()` 99 | 100 | -------------------------------------------------------------------------------- /doc/couchbeam_view_sup.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module couchbeam_view_sup # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | __Behaviours:__ [`supervisor`](supervisor.md). 8 | 9 | 10 | 11 | ## Function Index ## 12 | 13 | 14 |
init/1
start_link/0
15 | 16 | 17 | 18 | 19 | ## Function Details ## 20 | 21 | 22 | 23 | ### init/1 ### 24 | 25 | `init(X1) -> any()` 26 | 27 | 28 | 29 | ### start_link/0 ### 30 | 31 |

32 | start_link() -> {ok, pid()}
33 | 
34 |
35 | 36 | -------------------------------------------------------------------------------- /doc/gen_changes.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module gen_changes # 4 | * [Description](#description) 5 | * [Function Index](#index) 6 | * [Function Details](#functions) 7 | 8 | gen_changes CouchDB continuous changes consumer behavior 9 | This behaviour allows you to create easily a server that consume 10 | Couchdb continuous changes. 11 | 12 | __This module defines the `gen_changes` behaviour.__
Required callback functions: `init/1`, `handle_change/2`, `handle_call/3`, `handle_cast/2`, `handle_info/2`, `terminate/2`. 13 | 14 | 15 | 16 | ## Function Index ## 17 | 18 | 19 |
behaviour_info/1
call/2
call/3
cast/2
code_change/3
get_seq/1
handle_call/3
handle_cast/2
handle_info/2
init/1
start_link/4create a gen_changes process as part of a supervision tree.
stop/1
terminate/2
20 | 21 | 22 | 23 | 24 | ## Function Details ## 25 | 26 | 27 | 28 | ### behaviour_info/1 ### 29 | 30 | `behaviour_info(X1) -> any()` 31 | 32 | 33 | 34 | ### call/2 ### 35 | 36 | `call(Name, Request) -> any()` 37 | 38 | 39 | 40 | ### call/3 ### 41 | 42 | `call(Name, Request, Timeout) -> any()` 43 | 44 | 45 | 46 | ### cast/2 ### 47 | 48 | `cast(Dest, Request) -> any()` 49 | 50 | 51 | 52 | ### code_change/3 ### 53 | 54 | `code_change(OldVersion, State, Extra) -> any()` 55 | 56 | 57 | 58 | ### get_seq/1 ### 59 | 60 | `get_seq(Pid) -> any()` 61 | 62 | 63 | 64 | ### handle_call/3 ### 65 | 66 | `handle_call(Request, From, State) -> any()` 67 | 68 | 69 | 70 | ### handle_cast/2 ### 71 | 72 | `handle_cast(Msg, State) -> any()` 73 | 74 | 75 | 76 | ### handle_info/2 ### 77 | 78 | `handle_info(Info, State) -> any()` 79 | 80 | 81 | 82 | ### init/1 ### 83 | 84 | `init(X1) -> any()` 85 | 86 | 87 | 88 | ### start_link/4 ### 89 | 90 |

 91 | start_link(Module, Db::db(), Options::changesoptions(), InitArgs::list()) -> term()
 92 | 
93 | 94 | 95 | 96 | create a gen_changes process as part of a supervision tree. 97 | The function should be called, directly or indirectly, by the supervisor. 98 | 99 | 100 | 101 | ### stop/1 ### 102 | 103 | `stop(Pid) -> any()` 104 | 105 | 106 | 107 | ### terminate/2 ### 108 | 109 | `terminate(Reason, Gen_changes_state) -> any()` 110 | 111 | -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | %% 3 | %% This file is part of couchbeam released under the MIT license. 4 | %% See the NOTICE for more information. 5 | 6 | 7 | 8 | @copyright 2009-2025 Benoît Chesneau. 9 | @version 1.5.4 10 | @title Couchbeam - simple Apache CouchDB client library for Erlang applications 11 | 12 | @doc 13 | 14 | # couchbeam 15 | 16 | Couchbeam is a simple erlang library for [Barrel](https://barrel-db.org) or [Apache CouchDB](http://couchdb.apache.org). Couchbeam provides you a full featured and easy client to access and manage multiple nodes. 17 | 18 | #### Main features: 19 | 20 | - Complete support of the BarrelDB and Apache CouchDB API 21 | - Stream view results to your app 22 | - Stream changes feeds 23 | - reduced memory usage 24 | - fetch and send attachments in a streaming fashion 25 | - by default use the JSX module to encode/decode JSON 26 | - support [Jiffy](http://github.com/davisp/jiffy) a JSON encoder/decoder 27 | in C. 28 | 29 | 30 | #### Useful modules are: 31 | 32 | - {@link couchbeam}: The `couchbeam' module is the main interface for interaction with this application. It includes functions for managing connections to Apache CouchDB or RCOUCH servers and databases and for performing document creations, updates, deletes, views... 33 | - {@link couchbeam_doc} Module to manipulate Documents structures. You can set values, 34 | updates keys, ... 35 | - {@link couchbeam_attachments}: Module to manipulate attachments. You can add, remove 36 | attachments in a Document structure (inline attachments). 37 | - {@link couchbeam_view}: Module to manage view results. 38 | - {@link couchbeam_changes}: Module to manage changes feeds. Follow continuously 39 | the changes in a db or get all changes at once. 40 | 41 | 42 | The goal of Couchbeam is to ease the access to the Apache CouchDB and RCOUCH HTTP API in erlang. 43 | 44 | Read the [NEWS](https://raw.github.com/benoitc/couchbeam/master/NEWS) file 45 | to get last changelog. 46 | 47 | ## Installation 48 | 49 | Download the sources from our [Github repository](http://github.com/benoitc/couchbeam) 50 | 51 | To build the application simply run 'make'. This should build .beam, .app 52 | files and documentation. 53 | 54 | To run tests run 'make test'. 55 | To generate doc, run 'make doc'. 56 | 57 | 58 | Or add it to your rebar config 59 | 60 | ```erlang 61 | {deps, [ 62 | .... 63 | {couchbeam, ".*", {git, "git://github.com/benoitc/couchbeam.git", {branch, "master"}}} 64 | ]}. 65 | ''' 66 | 67 | Note to compile with jiffy you need to define in the erlang options the 68 | variable `WITH_JIFFY'. 69 | 70 | if you use rebar, add to your `rebar.config': 71 | 72 | ```erlang 73 | {erl_opts, [{d, 'WITH_JIFFY'}]}. 74 | ''' 75 | 76 | or use the `rebar' command with the `-D' options: 77 | 78 | ```sh 79 | rebar compile -DWITH_JIFFY 80 | ''' 81 | 82 | ## Basic Usage 83 | 84 | ### Start couchbeam 85 | 86 | Couchbeam is an [OTP](http://www.erlang.org/doc/design_principles/users_guide.html) 87 | application. You have to start it first before using any of the 88 | functions. The couchbeam application will start the default socket pool 89 | for you. 90 | 91 | To start in the console run: 92 | 93 | 94 | ```sh 95 | $ erl -pa ebin 96 | 1> couchbeam:start(). 97 | ok 98 | ''' 99 | 100 | 101 | It will start hackney and all of the application it depends on: 102 | 103 | ```erlang 104 | application:start(crypto), 105 | application:start(asn1), 106 | application:start(public_key), 107 | application:start(ssl), 108 | application:start(hackney), 109 | application:start(couchbeam). 110 | ''' 111 | 112 | Or add couchbeam to the applications property of your .app in a release 113 | 114 | ### Create a connection to the server 115 | 116 | To create a connection to a server machine: 117 | 118 | ```erlang 119 | Url = "http://localhost:5984", 120 | Options = [], 121 | S = couchbeam:server_connection(Url, Options). 122 | ''' 123 | 124 | Test the connection with `couchbeam:server_info/1' : 125 | 126 | ```erlang 127 | {ok, _Version} = couchbeam:server_info(S). 128 | ''' 129 | 130 | ### Open or Create a database 131 | 132 | All document operations are done in databases. To open a database simply do: 133 | 134 | ```erlang 135 | Options = [], 136 | {ok, Db} = couchbeam:open_db(Server, "testdb", Options). 137 | ''' 138 | 139 | To create a new one: 140 | 141 | ```erlang 142 | Options = [], 143 | {ok, Db} = couchbeam:create_db(Server, "testdb", Options). 144 | ''' 145 | 146 | You can also use the shorcut `couchbeam:open_or_create_db/3'. that 147 | will create a database if it does not exist. 148 | 149 | ### Make a new document 150 | 151 | Make a new document: 152 | 153 | ```erlang 154 | Doc = {[ 155 | {<<"_id">>, <<"test">>}, 156 | {<<"content">>, <<"some text">>} 157 | ]}. 158 | ''' 159 | 160 | And save it to the database: 161 | 162 | ```erlang 163 | {ok, Doc1} = couchbeam:save_doc(Db, Doc). 164 | ''' 165 | 166 | The `couchbeam:save_doc/2' return a new document with updated 167 | revision and if you do not specify the _id, a unique document id. 168 | 169 | To change an document property use functions from `couchbeam_doc'. 170 | 171 | ### Retrieve a document 172 | 173 | To retrieve a document do: 174 | 175 | ```erlang 176 | {ok, Doc2} = couchbeam:open_doc(Db, "test"). 177 | ''' 178 | 179 | If you want a specific revision: 180 | 181 | ```erlang 182 | Rev = couchbeam_doc:get_rev(Doc1), 183 | Options = [{rev, Rev}], 184 | {ok, Doc3} = couchbeam:open_doc(Db, "test", Options). 185 | ''' 186 | 187 | Here we get the revision from the document we previously stored. Any 188 | options from the Apache CouchDB and RCOUCH API can be used. 189 | 190 | ### Get all documents 191 | 192 | To get all documents you have first to create an object 193 | that will keep all informations. 194 | 195 | ```erlang 196 | Options = [include_docs], 197 | {ok, AllDocs} = couchbeam_view:all(Db, Options). 198 | ''' 199 | 200 | 201 | Ex of results: 202 | 203 | ```erlang 204 | {ok,[{[{<<"id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>}, 205 | {<<"key">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>}, 206 | {<<"value">>, 207 | {[{<<"rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18bdb">>}]}}, 208 | {<<"doc">>, 209 | {[{<<"_id">>,<<"7a0ce91d0d0c5e5b51e904d1ee3266a3">>}, 210 | {<<"_rev">>,<<"15-15c0b3c4efa74f9a80d28ac040f18"...>>}]}}]}, 211 | ]}. 212 | ''' 213 | 214 | All functions to manipulate these results are in the `couchbeam_view' module. 215 | 216 | ### Couch DB views 217 | 218 | Views are workin like all_docs. You have to create a View object before 219 | doing anything. 220 | 221 | ```erlang 222 | Options = [], 223 | DesignName = "designname", 224 | ViewName = "viewname", 225 | {ok, ViewResults} = couchbeam_view:fetch(Db, {DesignName, ViewName}, Options). 226 | ''' 227 | 228 | Like the `all_docs' function, use the functions 229 | from `couchbeam_view' module to manipulate results. You can pass 230 | any querying options from the [view API](http://docs.rcouch.org/en/latest/api/ddoc/views.html). 231 | 232 | Design doc are created like any documents: 233 | 234 | ```erlang 235 | DesignDoc = {[ 236 | {<<"_id">>, <<"_design/couchbeam">>}, 237 | {<<"language">>,<<"javascript">>}, 238 | {<<"views">>, 239 | {[{<<"test">>, 240 | {[{<<"map">>, 241 | <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">> 242 | }]} 243 | },{<<"test2">>, 244 | {[{<<"map">>, 245 | <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">> 246 | }]} 247 | }]} 248 | } 249 | ]}, 250 | {ok, DesignDoc1} = couchbeam:save_doc(Db, DesignDoc). 251 | ''' 252 | 253 | You can also use couchapp to manage them 254 | more easily. 255 | 256 | ### Stream View results 257 | 258 | While you can get results using `couchbeam_views:fetch/2', you can also retrieve 259 | all rows in a streaming fashion: 260 | 261 | ```erlang 262 | ViewFun = fun(Ref, F) -> 263 | receive 264 | {Ref, done} -> 265 | io:format("done", []), 266 | done; 267 | {Ref, {row, Row}} -> 268 | io:format("got ~p~n", [Row]), 269 | F(Ref, F); 270 | {error, Ref, Error} -> 271 | io:format("error: ~p~n", [Error]) 272 | end 273 | end, 274 | 275 | {ok, StreamRef} = couchbeam_view:stream(Db, 'all_docs'), 276 | ViewFun(StreamRef, ViewFun), 277 | {ok, StreamRef2} = couchbeam_view:stream(Db, 'all_docs', [include_docs]), 278 | ViewFun(StreamRef2, ViewFun). 279 | ''' 280 | 281 | You can of course do the same with a view: 282 | 283 | ```erlang 284 | DesignNam = "designname", 285 | ViewName = "viewname", 286 | {ok, StreamRef3} = couchbeam_view:stream(Db, {DesignNam, ViewName}, [include_docs]), 287 | ViewFun(StreamRef3, ViewFun). 288 | ''' 289 | 290 | ### Put, Fetch and Delete documents attachments 291 | 292 | You can add attachments to any documents. Attachments could be anything. 293 | 294 | To send an attachment: 295 | 296 | ```erlang 297 | DocID = "test", 298 | AttName = "test.txt", 299 | Att = "some content I want to attach", 300 | Options = [] 301 | {ok, _Result} = couchbeam:put_attachment(Db, DocId, AttName, Att, Options). 302 | ''' 303 | 304 | All attachments are streamed to servers. `Att' could be also be an iolist 305 | or functions, see `couchbeam:put_attachment/5' for more information. 306 | 307 | To fetch an attachment: 308 | 309 | ```erlang 310 | {ok Att1} = couchbeam:fetch_attachment(Db, DocId, AttName). 311 | ''' 312 | 313 | You can use `couchbeam:stream_fetch_attachment/6' for the stream 314 | fetch. 315 | 316 | To delete an attachment: 317 | 318 | ```erlang 319 | {ok, Doc4} = couchbeam:open_doc(Db, DocID), 320 | ok = couchbeam:delete_attachment(Db, Doc4, AttName). 321 | ''' 322 | 323 | ### Changes 324 | 325 | Apache CouchDB and RCOUCH provide a means to get a list of changes made to documents in 326 | the database. With couchbeam you can get changes using `couchbeam_changes:follow_once/2'. 327 | This function returns all changes immediately. But you can also retrieve 328 | all changes rows using longpolling : 329 | 330 | ```erlang 331 | Options = [], 332 | {ok, LastSeq, Rows} = couchbeam_changes:follow_once(Db, Options). 333 | ''' 334 | 335 | Options can be any Changes query parameters. See the [change API](http://docs.rcouch.org/en/latest/api/database/changes.html) for more informations. 336 | 337 | You can also get [continuous](http://docs.rcouch.org/en/latest/api/database/changes.html#continuous): 338 | 339 | ```erlang 340 | ChangesFun = fun(StreamRef, F) -> 341 | receive 342 | {StreamRef, {done, LastSeq}} -> 343 | io:format("stopped, last seq is ~p~n", [LastSeq]), 344 | ok; 345 | {StreamRef, {change, Change}} -> 346 | io:format("change row ~p ~n", [Change]), 347 | F(StreamRef, F); 348 | {StreamRef, Error}-> 349 | io:format("error ? ~p ~n,", [Error]) 350 | end 351 | end, 352 | Options = [continuous, heartbeat], 353 | {ok, StreamRef} = couchbeam_changes:follow(Db, Options), 354 | ChangesFun(StreamRef, ChangesFun). 355 | ''' 356 | 357 | > **Note**: a `gen_changes' behaviour exists in couchbeam that you can 358 | use to create your own specific gen_server receiving changes. Have a 359 | look in the 360 | [example](https://github.com/benoitc/couchbeam/blob/master/examples/test_gen_changes.erl) 361 | for more info. 362 | 363 | ### Authentication/ Connections options 364 | 365 | You can authenticate to the database or Apache CouchDB or RCOUCH server by filling 366 | options to the Option list in `couchbeam:server_connection/4' for the 367 | server or in `couchbeam:create_db/3', `couchbeam:open_db/3', 368 | `couchbeam:wopen_or_create_db/3' functions. 369 | 370 | To set basic_auth on a server: 371 | 372 | ```erlang 373 | UserName = "guest", 374 | Password = "test", 375 | Url = "http://localhost:5984", 376 | Options = [{basic_auth, {UserName, Password}}], 377 | S1 = couchbeam:server_connection(Url, Options). 378 | ''' 379 | 380 | Couchbeam support SSL, OAuth, Basic Authentication, and Proxy. You can 381 | also set a cookie. For more informations about the options have a look 382 | in the `couchbeam:server_connection/2' documentation. 383 | 384 | 385 | ## Contribute 386 | 387 | For issues, comments or feedback please [create an 388 | issue](http://github.com/benoitc/couchbeam/issues). 389 | 390 | @end 391 | -------------------------------------------------------------------------------- /examples/changes.ebin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin 4 | 5 | -module(changes). 6 | 7 | main(_) -> 8 | couchbeam:start(), 9 | Server = couchbeam:server_connection(), 10 | {ok, Db} = couchbeam:open_or_create_db(Server, "testdb"), 11 | {ok, Ref} = couchbeam_changes:follow(Db, [continuous, heartbeat]), 12 | io:format("stream ref ~p~n", [Ref]), 13 | io:format("i am ~p~n", [self()]), 14 | get_changes(Ref). 15 | 16 | 17 | get_changes(Ref) -> 18 | receive 19 | {Ref, {done, LastSeq}} -> 20 | io:format("stopped, last seq is ~p~n", [LastSeq]), 21 | ok; 22 | {Ref, {change, Change}} -> 23 | io:format("got ~p ~n", [Change]), 24 | get_changes(Ref); 25 | {Ref, Else}-> 26 | io:format("else ? ~p ~n", [Else]) 27 | end. 28 | -------------------------------------------------------------------------------- /examples/changes_once.ebin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin 4 | 5 | -module(changes). 6 | 7 | main(_) -> 8 | couchbeam:start(), 9 | Server = couchbeam:server_connection(), 10 | {ok, Db} = couchbeam:open_or_create_db(Server, "testdb"), 11 | {ok, LastSeq, Changes} = couchbeam_changes:follow_once(Db, [longpoll]), 12 | io:format("longpoll changes ~p~n", [Changes]), 13 | io:format("now wait until the next change...~n", []), 14 | {ok, _, Changes2} = couchbeam_changes:follow_once(Db, 15 | [longpoll, 16 | {since, LastSeq}]), 17 | io:format("longpoll changes 2 ~p~n", [Changes2]), 18 | 19 | {ok, _, Changes3} = couchbeam_changes:follow_once(Db), 20 | io:format("normal changes ~p~n", [Changes3]). 21 | -------------------------------------------------------------------------------- /examples/test_gen_changes.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(test_gen_changes). 7 | 8 | -behaviour(gen_changes). 9 | -export([start_link/2, 10 | init/1, 11 | handle_change/2, 12 | handle_call/3, 13 | handle_cast/2, 14 | handle_info/2, 15 | terminate/2]). 16 | 17 | -export([get_changes/1]). 18 | 19 | 20 | -record(state, {changes=[]}). 21 | 22 | start_link(Db, Opts) -> 23 | gen_changes:start_link(?MODULE, Db, Opts, []). 24 | 25 | 26 | init([]) -> 27 | {ok, #state{}}. 28 | 29 | get_changes(Pid) -> 30 | gen_changes:call(Pid, get_changes). 31 | 32 | handle_change({done, _LastSeq}, State) -> 33 | {noreply, State}; 34 | 35 | 36 | handle_change(Change, State=#state{changes=Changes}) -> 37 | NewChanges = [Change|Changes], 38 | {noreply, State#state{changes=NewChanges}}. 39 | 40 | handle_call(get_changes, _From, State=#state{changes=Changes}) -> 41 | {reply, lists:reverse(Changes), State}. 42 | 43 | handle_cast(_Msg, State) -> {noreply, State}. 44 | 45 | handle_info(Info, State) -> 46 | io:format("Unknown message: ~p~n", [Info]), 47 | {noreply, State}. 48 | 49 | terminate(Reason, _State) -> 50 | io:format("~p terminating with reason ~p~n", [?MODULE, Reason]), 51 | ok. 52 | 53 | -------------------------------------------------------------------------------- /include/couchbeam.hrl: -------------------------------------------------------------------------------- 1 | %% @author Benoît Chesneau 2 | %% @copyright 2009 Benoît Chesneau. 3 | %% 4 | %% 5 | %% Licensed under the Apache License, Version 2.0 (the "License"); 6 | %% you may not use this file except in compliance with the License. 7 | %% You may obtain a copy of the License at 8 | %% 9 | %% http://www.apache.org/licenses/LICENSE-2.0 10 | %% 11 | %% Unless required by applicable law or agreed to in writing, software 12 | %% distributed under the License is distributed on an "AS IS" BASIS, 13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | %% See the License for the specific language governing permissions and 15 | %% limitations under the License. 16 | 17 | 18 | -define(DEFAULT_TIMEOUT, 60000). 19 | 20 | -type header() :: {string() | atom(), string()}. 21 | -type headers() :: [header()]. 22 | %% In R13B bool() is now called boolean() 23 | %% Uncomment if it's not compiling. 24 | %% -type boolean() :: bool() 25 | 26 | 27 | 28 | -type db_name() :: binary() | string(). 29 | -type docid() :: binary() | string(). 30 | 31 | -type design_name() :: binary() | string(). 32 | -type view_name() :: binary() | string(). 33 | 34 | -type ejson() :: ejson_object() | ejson_array(). 35 | 36 | -type ejson_array() :: [ejson_term()]. 37 | -type ejson_object() :: {[{ejson_key(), ejson_term()}]}. 38 | 39 | -type ejson_key() :: binary() | atom(). 40 | 41 | -type ejson_term() :: ejson_array() 42 | | ejson_object() 43 | | ejson_string() 44 | | ejson_number() 45 | | true | false | null. 46 | 47 | -type ejson_string() :: binary(). 48 | 49 | -type ejson_number() :: float() | integer(). 50 | 51 | -type doc() :: ejson_object(). 52 | 53 | -type changes_option() :: continuous | longpoll | normal 54 | | include_docs | {since, integer()} 55 | | {timeout, integer()} 56 | | heartbeat | {heartbeat, integer()} 57 | | {filter, string()} | {filter, string(), list({string(), string() | integer()} 58 | )} 59 | | conflicts | {style, string()} | descending. 60 | -type changes_options() :: list(changes_option()). 61 | 62 | -type changes_option1() :: longpoll | normal 63 | | include_docs | {since, integer()} 64 | | {timeout, integer()} 65 | | heartbeat | {heartbeat, integer()} 66 | | {filter, string()} | {filter, string(), list({string(), string() | integer()} 67 | )} 68 | | conflicts | {style, string()} | descending. 69 | -type changes_options1() :: list(changes_option1()). 70 | 71 | -type changes_stream_option() :: continuous | longpoll | normal 72 | | include_docs 73 | | {since, integer()} 74 | | {timeout, integer()} 75 | | heartbeat | {heartbeat, integer()} 76 | | {filter, string() | binary()} 77 | | {filter, string() | binary(), list({string(), string() | integer()})} 78 | | conflicts 79 | | {style, string() | binary()} 80 | | descending 81 | | {view, string() | binary()} 82 | | {doc_ids, list()}. 83 | -type changes_stream_options() :: list(changes_stream_option()). 84 | 85 | -type stale() :: ok | update_after | false. 86 | 87 | -type view_option() :: {key, binary()} | {startkey_docid, binary()} 88 | | {start_docid, binary()} | {startkey_docid, binary()} 89 | | {end_docid, binary()} | {endkey_docid, binary()} 90 | | {start_key, binary()} | {end_key, binary()} 91 | | {limit, integer()} 92 | | {stale, stale()} 93 | | descending 94 | | {skip, integer()} 95 | | group | {group_level, exact | integer()} 96 | | reduce | {reduce, boolean()} 97 | | inclusive_end | include_docs | conflicts 98 | | {list, binary()} 99 | | {keys, list(binary())} 100 | | async_query. 101 | 102 | -type view_options() :: list(view_option()). 103 | 104 | -record(view_query_args, { 105 | method = get :: atom(), 106 | options = [] :: view_options(), 107 | keys = [] :: list(binary())}). 108 | 109 | -type view_query_args() :: #view_query_args{}. 110 | 111 | -record(server, { 112 | url, 113 | options = [] :: list() 114 | }). 115 | 116 | -type server() :: #server{}. 117 | 118 | % record to keep database information 119 | -record(db, { 120 | server :: server(), 121 | name :: binary(), 122 | options = [] :: list() 123 | }). 124 | 125 | -type db() :: #db{}. 126 | 127 | -record(server_uuids, { 128 | server_url, 129 | uuids 130 | }). 131 | 132 | -record(view, { 133 | db :: db(), 134 | name :: string(), 135 | options :: iolist(), 136 | method :: atom(), 137 | body :: iolist(), 138 | headers :: list(), 139 | url_parts :: list(), 140 | url :: string() 141 | }). 142 | 143 | -type view() :: #view{}. 144 | 145 | -record(changes_args, { 146 | type = normal, 147 | since = 0, 148 | http_options = []}). 149 | -type changes_args() :: #changes_args{}. 150 | 151 | -record(gen_changes_state, { 152 | stream_ref, 153 | last_seq=0, 154 | mod, 155 | modstate, 156 | db, 157 | options}). 158 | 159 | -define(USER_AGENT, "couchbeam/0.9.0"). 160 | 161 | -define(DEPRECATED(Old, New, When), 162 | couchbeam_util:deprecated(Old, New, When)). 163 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | 3 | 4 | {erl_opts, [debug_info, 5 | {platform_define, "^(2[3-9])", 'USE_CRYPTO_MAC'}]}. 6 | 7 | {deps, [ 8 | {jsx, "3.1.0"}, 9 | {hackney, "1.24.1"} 10 | ]}. 11 | 12 | 13 | {profiles, [{docs, [{deps, [{edown,"0.9.1"}]}, 14 | {edoc_opts, [{doclet, edown_doclet}, 15 | {packages, false}, 16 | {subpackages, true}, 17 | {top_level_readme, 18 | {"./README.md", "http://github.com/benoitc/couchbeam"}} 19 | ]}]}, 20 | {test, [ 21 | {cover_enabled, true}, 22 | {eunit_opts, [verbose]}, 23 | {deps, [{oauth, "2.1.0"}]} 24 | ]} 25 | ]}. 26 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"certifi">>,{pkg,<<"certifi">>,<<"2.15.0">>},1}, 3 | {<<"hackney">>,{pkg,<<"hackney">>,<<"1.24.1">>},0}, 4 | {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},1}, 5 | {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0}, 6 | {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1}, 7 | {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.4.0">>},1}, 8 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.4.1">>},1}, 9 | {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},1}, 10 | {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},1}]}. 11 | [ 12 | {pkg_hash,[ 13 | {<<"certifi">>, <<"0E6E882FCDAAA0A5A9F2B3DB55B1394DBA07E8D6D9BCAD08318FB604C6839712">>}, 14 | {<<"hackney">>, <<"F5205A125BBA6ED4587F9DB3CC7C729D11316FA8F215D3E57ED1C067A9703FA9">>}, 15 | {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, 16 | {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, 17 | {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, 18 | {<<"mimerl">>, <<"3882A5CA67FBBE7117BA8947F27643557ADEC38FA2307490C4C4207624CB213B">>}, 19 | {<<"parse_trans">>, <<"6E6AA8167CB44CC8F39441D05193BE6E6F4E7C2946CB2759F015F8C56B76E5FF">>}, 20 | {<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>}, 21 | {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]}, 22 | {pkg_hash_ext,[ 23 | {<<"certifi">>, <<"B147ED22CE71D72EAFDAD94F055165C1C182F61A2FF49DF28BCC71D1D5B94A60">>}, 24 | {<<"hackney">>, <<"F4A7392A0B53D8BBC3EB855BDCC919CD677358E65B2AFD3840B5B3690C4C8A39">>}, 25 | {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, 26 | {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, 27 | {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>}, 28 | {<<"mimerl">>, <<"13AF15F9F68C65884ECCA3A3891D50A7B57D82152792F3E19D88650AA126B144">>}, 29 | {<<"parse_trans">>, <<"620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A">>}, 30 | {<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>}, 31 | {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]} 32 | ]. 33 | -------------------------------------------------------------------------------- /src/couchbeam.app.src: -------------------------------------------------------------------------------- 1 | %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ft=erlang ts=4 sw=4 et 3 | %%% 4 | %%% This file is part of couchbeam released under the MIT license. 5 | %%% See the NOTICE for more information. 6 | 7 | {application, couchbeam, 8 | [{description, "Erlang CouchDB client"}, 9 | {vsn, "1.7.0"}, 10 | {modules, []}, 11 | {registered, [ 12 | couchbeam_sup 13 | ]}, 14 | {applications, [kernel, 15 | stdlib, 16 | asn1, 17 | crypto, 18 | public_key, 19 | ssl, 20 | idna, 21 | hackney]}, 22 | {included_applications, []}, 23 | {env, []}, 24 | {mod, { couchbeam_app, []}}, 25 | 26 | {maintainers, ["Benoit Chesneau"]}, 27 | {licenses, ["MIT"]}, 28 | {links, [{"Github", "https://github.com/benoitc/couchbeam"}]}, 29 | {files, [ 30 | "src", 31 | "include", 32 | "rebar.config", 33 | "rebar.lock", 34 | "README.md", 35 | "NEWS.md", 36 | "LICENSE", 37 | "NOTICE"]} 38 | ] 39 | }. 40 | -------------------------------------------------------------------------------- /src/couchbeam_app.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_app). 7 | 8 | -behaviour(application). 9 | 10 | -export([start/2, stop/1]). 11 | 12 | %%-------------------------------------------------------------------- 13 | %% Func: start/2 14 | %% Returns: {ok, Pid} | 15 | %% {ok, Pid, State} | 16 | %% {error, Reason} 17 | %%-------------------------------------------------------------------- 18 | start(_Type, _StartArgs) -> 19 | couchbeam_util:start_app_deps(couchbeam), 20 | couchbeam_sup:start_link(). 21 | 22 | %%-------------------------------------------------------------------- 23 | %% Func: stop/1 24 | %% Returns: any 25 | %%-------------------------------------------------------------------- 26 | stop(_State) -> 27 | ok. 28 | 29 | -------------------------------------------------------------------------------- /src/couchbeam_attachments.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | 7 | %% @doc This module contains utilities to manage attachments 8 | 9 | -module(couchbeam_attachments). 10 | 11 | -include("couchbeam.hrl"). 12 | 13 | -export([add_inline/3, add_inline/4, 14 | add_stub/3, 15 | delete_inline/2]). 16 | 17 | %% @spec add_inline(Doc::json_obj(),Content::attachment_content(), 18 | %% AName::string()) -> json_obj() 19 | %% @doc add attachment to a doc and encode it. Give possibility to send attachments inline. 20 | add_inline(Doc, Content, AName) -> 21 | AName1 = hackney_bstr:to_binary(AName), 22 | ContentType = mimerl:filename(AName1), 23 | add_inline(Doc, Content, AName1, ContentType). 24 | 25 | %% @spec add_inline(Doc::json_obj(), Content::attachment_content(), 26 | %% AName::string(), ContentType::string()) -> json_obj() 27 | %% @doc add attachment to a doc and encode it with ContentType fixed. 28 | add_inline(Doc, Content, AName, ContentType) -> 29 | {Props} = Doc, 30 | Data = base64:encode(Content), 31 | Attachment = {AName, {[{<<"content_type">>, ContentType}, 32 | {<<"data">>, Data}]}}, 33 | 34 | Attachments1 = case proplists:get_value(<<"_attachments">>, Props) of 35 | undefined -> 36 | [Attachment]; 37 | {Attachments} -> 38 | case set_attachment(Attachments, [], Attachment) of 39 | notfound -> 40 | [Attachment|Attachments]; 41 | A -> 42 | A 43 | end 44 | end, 45 | couchbeam_doc:set_value(<<"_attachments">>, {Attachments1}, Doc). 46 | 47 | 48 | add_stub({Props} = Doc, Name, ContentType) -> 49 | Att = {couchbeam_util:to_binary(Name), {[ 50 | {<<"content_type">>, couchbeam_util:to_binary(ContentType)} 51 | ]}}, 52 | 53 | Attachments1 = case proplists:get_value(<<"_attachments">>, Props) of 54 | undefined -> 55 | [Att]; 56 | {Attachments} -> 57 | case set_attachment(Attachments, [], Att) of 58 | notfound -> 59 | [Att|Attachments]; 60 | A -> 61 | A 62 | end 63 | end, 64 | couchbeam_doc:set_value(<<"_attachments">>, {Attachments1}, Doc). 65 | 66 | 67 | %% @spec delete_inline(Doc::json_obj(), AName::string()) -> json_obj() 68 | %% @doc delete an attachment record in doc. This is different from delete_attachment 69 | %% change is only applied in Doc object. Save_doc should be save to save changes. 70 | delete_inline(Doc, AName) when is_list(AName) -> 71 | delete_inline(Doc, list_to_binary(AName)); 72 | delete_inline(Doc, AName) when is_binary(AName) -> 73 | {Props} = Doc, 74 | case proplists:get_value(<<"_attachments">>, Props) of 75 | undefined -> 76 | Doc; 77 | {Attachments} -> 78 | case proplists:get_value(AName, Attachments) of 79 | undefined -> 80 | Doc; 81 | _ -> 82 | Attachments1 = proplists:delete(AName, Attachments), 83 | couchbeam_doc:set_value(<<"_attachments">>, {Attachments1}, Doc) 84 | end 85 | end. 86 | 87 | % @private 88 | set_attachment(Attachments, NewAttachments, Attachment) -> 89 | set_attachment(Attachments, NewAttachments, Attachment, false). 90 | set_attachment([], Attachments, _Attachment, Found) -> 91 | case Found of 92 | true -> 93 | Attachments; 94 | false -> 95 | notfound 96 | end; 97 | set_attachment([{Name, V}|T], Attachments, Attachment, Found) -> 98 | {AName, _} = Attachment, 99 | {Attachment1, Found1} = if 100 | Name =:= AName, Found =:= false -> 101 | {Attachment, true}; 102 | true -> 103 | {{Name, V}, Found} 104 | end, 105 | set_attachment(T, [Attachment1|Attachments], Attachment, Found1). 106 | -------------------------------------------------------------------------------- /src/couchbeam_changes.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_changes). 7 | 8 | -include("couchbeam.hrl"). 9 | 10 | -export([follow/1, follow/2, 11 | cancel_stream/1, 12 | stream_next/1, 13 | follow_once/1, follow_once/2]). 14 | 15 | -spec follow(Db::db()) -> {ok, StreamRef::atom()} | {error, term()}. 16 | follow(Db) -> 17 | follow(Db, []). 18 | 19 | %% @doc Stream changes to a pid 20 | %%

Db : a db record

21 | %%

Client : pid or callback where to send changes events where events are 22 | %% The pid receive these events: 23 | %%

24 | %%
{change, StartRef, {done, Lastseq::integer()}
25 | %%
Connection terminated or you got all changes
26 | %%
{change, StartRef, Row :: ejson_object()}
27 | %%
Line of change
28 | %%
{error, LastSeq::integer(), Msg::term()}
29 | %%
Got an error, connection is closed when an error 30 | %% happend.
31 | %%
32 | %% LastSeq is the last sequence of changes.

33 | %% While the callbac could be like: 34 | %%
 35 | %%      fun({done, LastSeq}) ->
 36 | %%          ok;
 37 | %%      fun({done, LastSeq}) ->
 38 | %%          ok;
 39 | %%      fun({done, LastSeq}) ->
 40 | %%          ok.
41 | %%

>Options :: changes_stream_options() [continuous
 42 | %%    | longpoll
 43 | %%    | normal
 44 | %%    | include_docs
 45 | %%    | {since, integer() | now}
 46 | %%    | {timeout, integer()}
 47 | %%    | heartbeat | {heartbeat, integer()}
 48 | %%    | {filter, string()} | {filter, string(), list({string(), string() | integer()})}
 49 | %%    | {view, string()},
 50 | %%    | {docids, list))},
 51 | %%    | {stream_to, pid()},
 52 | %%    | {async, once | normal}]
53 | %% 54 | %%
    55 | %%
  • continuous | longpoll | normal: set the type of changes 56 | %% feed to get
  • 57 | %%
  • include_doc: if you want to include the doc in the line of 58 | %% change
  • 59 | %%
  • {timeout, Timeout::integer()}: timeout
  • 60 | %%
  • heartbeat | {heartbeat, Heartbeat::integer()}: set couchdb 61 | %% to send a heartbeat to maintain connection open
  • 62 | %%
  • {filter, FilterName} | {filter, FilterName, Args::list({key, 63 | %% value})}: set the filter to use with optional arguments
  • 64 | %%
  • {view, ViewName}: use a view function as filter. Note 65 | %% that it requires to set filter special value "_view" 66 | %% to enable this feature.
  • 67 | %%
  • >`{stream_to, Pid}': the pid where the changes will be sent, 68 | %% by default the current pid. Used for continuous and longpoll 69 | %% connections
  • 70 | %%

71 | %% 72 | %%

Return {ok, StartRef, ChangesPid} or {error, Error}. Ref can be 73 | %% used to disctint all changes from this pid. ChangesPid is the pid of 74 | %% the changes loop process. Can be used to monitor it or kill it 75 | %% when needed.

76 | -spec follow(Db::db(), Options::changes_options()) -> 77 | {ok, StreamRef::atom()} | {error, term()}. 78 | follow(Db, Options) -> 79 | {To, Options1} = case proplists:get_value(stream_to, Options) of 80 | undefined -> 81 | {self(), Options}; 82 | Pid -> 83 | {Pid, proplists:delete(stream_to, Options)} 84 | end, 85 | 86 | Ref = make_ref(), 87 | case supervisor:start_child(couchbeam_changes_sup, [To, Ref, Db, 88 | Options1]) of 89 | {ok, _Pid} -> 90 | {ok, Ref}; 91 | {error, _} = Error -> 92 | Error 93 | end. 94 | 95 | -spec follow_once(Db::db()) 96 | -> {ok, LastSeq::integer(), Changes::list()} | {error,term()}. 97 | follow_once(Db) -> 98 | follow_once(Db, []). 99 | 100 | 101 | %% @doc fetch all changes at once using a normal or longpoll 102 | %% connections. 103 | %% 104 | %%

Db : a db record

105 | %%

Options :: changes_options() [
106 | %%    | longpoll
107 | %%    | normal
108 | %%    | include_docs
109 | %%    | {since, integer() | now}
110 | %%    | {timeout, integer()}
111 | %%    | heartbeat | {heartbeat, integer()}
112 | %%    | {filter, string()}
113 | %%    | {filter, string(), list({string(), string() | integer()})}
114 | %%    | {docids, list()))},
115 | %%    | {stream_to, pid()}
116 | %%    ]
117 | %% 118 | %%
    119 | %%
  • longpoll | normal: set the type of changes 120 | %% feed to get
  • 121 | %%
  • include_docs: if you want to include the doc in the line of 122 | %% change
  • 123 | %%
  • {timeout, Timeout::integer()}: timeout
  • 124 | %%
  • heartbeat | {heartbeat, Heartbeat::integer()}: set couchdb 125 | %% to send a heartbeat to maintain connection open
  • 126 | %%
  • {filter, FilterName} | {filter, FilterName, Args::list({key, 127 | %% value}): set the filter to use with optional arguments
  • 128 | %%
  • {view, ViewName}: use a view function as filter. Note 129 | %% that it requires to set filter special value "_view" 130 | %% to enable this feature.
  • 131 | %%

132 | %% 133 | %%

Result: {ok, LastSeq::integer(), Rows::list()} or 134 | %% {error, LastSeq, Error}. LastSeq is the last sequence of changes.

135 | -spec follow_once(Db::db(), Options::changes_options()) 136 | -> {ok, LastSeq::integer(), Changes::list()} | {error,term()}. 137 | follow_once(Db, Options) -> 138 | case parse_options_once(Options, []) of 139 | {error, _}=Error -> 140 | Error; 141 | Options1 -> 142 | FinalOptions = couchbeam_util:force_param(reconnect_after, 143 | false, Options1), 144 | case proplists:get_value(feed, FinalOptions) of 145 | longpoll -> 146 | case follow(Db, FinalOptions) of 147 | {ok, Ref} -> 148 | collect_changes(Ref); 149 | {error, _} = Error -> 150 | Error 151 | end; 152 | _ -> 153 | changes_request(Db, FinalOptions) 154 | end 155 | end. 156 | 157 | 158 | 159 | cancel_stream(Ref) -> 160 | with_changes_stream(Ref, fun(Pid) -> 161 | case supervisor:terminate_child(couchbeam_view_sup, Pid) of 162 | ok -> 163 | case supervisor:delete_child(couchbeam_view_sup, Pid) of 164 | ok ->ok; 165 | {error, not_found} -> ok; 166 | Error -> Error 167 | end; 168 | Error -> 169 | Error 170 | end 171 | end). 172 | 173 | stream_next(Ref) -> 174 | with_changes_stream(Ref, fun(Pid) -> 175 | Pid ! {Ref, stream_next} 176 | end). 177 | 178 | %% @private 179 | collect_changes(Ref) -> 180 | collect_changes(Ref, []). 181 | 182 | collect_changes(Ref, Acc) -> 183 | receive 184 | {Ref, {done, LastSeq}} -> 185 | Changes = lists:reverse(Acc), 186 | {ok, LastSeq, Changes}; 187 | {Ref, {change, Change}} -> 188 | collect_changes(Ref, [Change|Acc]); 189 | {Ref, Error} -> 190 | Error; 191 | Error -> 192 | Error 193 | end. 194 | 195 | 196 | with_changes_stream(Ref, Fun) -> 197 | case ets:lookup(couchbeam_changes_streams, Ref) of 198 | [] -> 199 | {error, stream_undefined}; 200 | [{Ref, Pid}] -> 201 | %% Check if the process is still alive to avoid race conditions 202 | case is_process_alive(Pid) of 203 | true -> 204 | Fun(Pid); 205 | false -> 206 | %% Clean up the stale entry 207 | ets:delete(couchbeam_changes_streams, Ref), 208 | {error, stream_undefined} 209 | end 210 | end. 211 | 212 | 213 | changes_request(#db{server=Server, options=ConnOptions}=Db, Options) -> 214 | %% if we are filtering the changes using docids, send a POST request 215 | %% instead of a GET to make sure it will be accepted whatever the 216 | %% number of doc ids given. 217 | {DocIds, Options1} = case proplists:get_value(doc_ids, Options) of 218 | undefined -> 219 | {[], Options}; 220 | [] -> 221 | {[], Options}; 222 | Ids -> 223 | {Ids, proplists:delete(doc_ids, Options)} 224 | end, 225 | 226 | %% make url 227 | Url = hackney_url:make_url(couchbeam_httpc:server_url(Server), 228 | [couchbeam_httpc:db_url(Db), <<"_changes">>], 229 | Options1), 230 | 231 | %% do the request 232 | Resp = case DocIds of 233 | [] -> 234 | couchbeam_httpc:db_request(get, Url, [], <<>>, ConnOptions, 235 | [200, 202]); 236 | _ -> 237 | Body = couchbeam_ejson:encode({[{<<"doc_ids">>, DocIds}]}), 238 | Headers = [{<<"Content-Type">>, <<"application/json">>}], 239 | couchbeam_httpc:db_request(post, Url, Headers, Body, ConnOptions, 240 | [200, 202]) 241 | end, 242 | 243 | case Resp of 244 | {ok, _, _, Ref} -> 245 | {Props} = couchbeam_httpc:json_body(Ref), 246 | LastSeq = couchbeam_util:get_value(<<"last_seq">>, Props), 247 | Changes = couchbeam_util:get_value(<<"results">>, Props), 248 | {ok, LastSeq, Changes}; 249 | Error -> 250 | Error 251 | end. 252 | 253 | parse_options_once([], Acc) -> 254 | lists:reverse(Acc); 255 | parse_options_once([normal | Rest], Acc) -> 256 | parse_options_once(Rest, couchbeam_util:force_param(feed, normal, 257 | Acc)); 258 | parse_options_once([continuous | _Rest], _Acc) -> 259 | {error, {badarg, continuous}}; 260 | parse_options_once([longpoll | Rest], Acc) -> 261 | parse_options_once(Rest, couchbeam_util:force_param(feed, longpoll, 262 | Acc)); 263 | parse_options_once([heartbeat | Rest], Acc) -> 264 | parse_options_once(Rest, couchbeam_util:force_param(heartbeat, true, 265 | Acc)); 266 | parse_options_once([descending | Rest], Acc) -> 267 | parse_options_once(Rest, couchbeam_util:force_param(descending, true, 268 | Acc)); 269 | parse_options_once([conflicts | Rest], Acc) -> 270 | parse_options_once(Rest, couchbeam_util:force_param(conflicts, true, 271 | Acc)); 272 | parse_options_once([include_docs | Rest], Acc) -> 273 | parse_options_once(Rest, couchbeam_util:force_param(include_docs, true, 274 | Acc)); 275 | parse_options_once([{K, V} | Rest], Acc) -> 276 | parse_options_once(Rest, [{K, V} | Acc]). 277 | -------------------------------------------------------------------------------- /src/couchbeam_changes_sup.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_changes_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API. 11 | -export([start_link/0]). 12 | 13 | %% supervisor. 14 | -export([init/1]). 15 | 16 | -define(SUPERVISOR, ?MODULE). 17 | 18 | %% API. 19 | 20 | -spec start_link() -> {ok, pid()}. 21 | start_link() -> 22 | supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). 23 | 24 | %% supervisor. 25 | 26 | init([]) -> 27 | 28 | %% start table to keep async streams ref 29 | ets:new(couchbeam_changes_streams, [set, public, named_table]), 30 | 31 | %% define a stream spec 32 | Stream = {couchbeam_changes_stream, 33 | {couchbeam_changes_stream, start_link, []}, 34 | temporary, infinity, worker, [couchbeam_changes_stream]}, 35 | 36 | {ok, {{simple_one_for_one, 10, 3600}, [Stream]}}. 37 | -------------------------------------------------------------------------------- /src/couchbeam_doc.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_doc). 7 | -author('Benoît Chesneau '). 8 | -include("couchbeam.hrl"). 9 | 10 | -export([set_value/3, get_value/2, get_value/3, 11 | take_value/2, take_value/3, 12 | delete_value/2, extend/2, extend/3]). 13 | -export([get_id/1, get_rev/1, get_idrev/1, is_saved/1]). 14 | 15 | %% @spec get_id(Doc::json_obj()) -> binary() 16 | %% @doc get document id. 17 | get_id(Doc) -> 18 | get_value(<<"_id">>, Doc). 19 | 20 | %% @spec get_rev(Doc::json_obj()) -> binary() 21 | %% @doc get document revision. 22 | get_rev(Doc) -> 23 | get_value(<<"_rev">>, Doc). 24 | 25 | %% @spec get_idrev(Doc::json_obj()) -> {DocId, DocRev} 26 | %% @doc get a tuple containing docucment id and revision. 27 | get_idrev(Doc) -> 28 | DocId = get_value(<<"_id">>, Doc), 29 | DocRev = get_value(<<"_rev">>, Doc), 30 | {DocId, DocRev}. 31 | 32 | %% @spec is_saved(Doc::json_obj()) -> boolean() 33 | %% @doc If document have been saved (revision is defined) return true, 34 | %% else, return false. 35 | is_saved(Doc) -> 36 | case get_value(<<"_rev">>, Doc) of 37 | undefined -> false; 38 | _ -> true 39 | end. 40 | 41 | %% @spec set_value(Key::key_val(), Value::term(), JsonObj::json_obj()) -> term() 42 | %% @doc set a value for a key in jsonobj. If key exists it will be updated. 43 | set_value(Key, Value, JsonObj) when is_list(Key)-> 44 | set_value(list_to_binary(Key), Value, JsonObj); 45 | set_value(Key, Value, JsonObj) when is_binary(Key) -> 46 | {Props} = JsonObj, 47 | case proplists:is_defined(Key, Props) of 48 | true -> set_value1(Props, Key, Value, []); 49 | false-> {lists:reverse([{Key, Value}|lists:reverse(Props)])} 50 | end. 51 | 52 | %% @spec get_value(Key::key_val(), JsonObj::json_obj()) -> term() 53 | %% @type key_val() = lis() | binary() 54 | %% @doc Returns the value of a simple key/value property in json object 55 | %% Equivalent to get_value(Key, JsonObj, undefined). 56 | get_value(Key, JsonObj) -> 57 | get_value(Key, JsonObj, undefined). 58 | 59 | 60 | %% @spec get_value(Key::lis() | binary(), JsonObj::json_obj(), Default::term()) -> term() 61 | %% @doc Returns the value of a simple key/value property in json object 62 | %% function from erlang_couchdb 63 | get_value(Key, JsonObj, Default) when is_list(Key) -> 64 | get_value(list_to_binary(Key), JsonObj, Default); 65 | get_value(Key, JsonObj, Default) when is_binary(Key) -> 66 | {Props} = JsonObj, 67 | couchbeam_util:get_value(Key, Props, Default). 68 | 69 | 70 | %% @spec take_value(Key::key_val(), JsonObj::json_obj()) -> {term(), json_obj()} 71 | %% @doc Returns the value of a simple key/value property in json object and deletes 72 | %% it form json object 73 | %% Equivalent to take_value(Key, JsonObj, undefined). 74 | take_value(Key, JsonObj) -> 75 | take_value(Key, JsonObj, undefined). 76 | 77 | 78 | %% @spec take_value(Key::key_val() | binary(), JsonObj::json_obj(), 79 | %% Default::term()) -> {term(), json_obj()} 80 | %% @doc Returns the value of a simple key/value property in json object and deletes 81 | %% it from json object 82 | take_value(Key, JsonObj, Default) when is_list(Key) -> 83 | get_value(list_to_binary(Key), JsonObj, Default); 84 | take_value(Key, JsonObj, Default) when is_binary(Key) -> 85 | {Props} = JsonObj, 86 | case lists:keytake(Key, 1, Props) of 87 | {value, {Key, Value}, Rest} -> 88 | {Value, {Rest}}; 89 | false -> 90 | {Default, JsonObj} 91 | end. 92 | 93 | %% @spec delete_value(Key::key_val(), JsonObj::json_obj()) -> json_obj() 94 | %% @doc Deletes all entries associated with Key in json object. 95 | delete_value(Key, JsonObj) when is_list(Key) -> 96 | delete_value(list_to_binary(Key), JsonObj); 97 | delete_value(Key, JsonObj) when is_binary(Key) -> 98 | {Props} = JsonObj, 99 | Props1 = proplists:delete(Key, Props), 100 | {Props1}. 101 | 102 | %% @spec extend(Key::binary(), Value::json_term(), JsonObj::json_obj()) -> json_obj() 103 | %% @doc extend a jsonobject by key, value 104 | extend(Key, Value, JsonObj) -> 105 | extend({Key, Value}, JsonObj). 106 | 107 | %% @spec extend(Prop::property(), JsonObj::json_obj()) -> json_obj() 108 | %% @type property() = json_obj() | tuple() 109 | %% @doc extend a jsonobject by a property, list of property or another jsonobject 110 | extend([], JsonObj) -> 111 | JsonObj; 112 | extend({List}, JsonObj) when is_list(List) -> 113 | extend(List, JsonObj); 114 | extend([Prop|R], JsonObj)-> 115 | NewObj = extend(Prop, JsonObj), 116 | extend(R, NewObj); 117 | extend({Key, Value}, JsonObj) -> 118 | set_value(Key, Value, JsonObj). 119 | 120 | %% @private 121 | set_value1([], _Key, _Value, Acc) -> 122 | {lists:reverse(Acc)}; 123 | set_value1([{K, V}|T], Key, Value, Acc) -> 124 | Acc1 = if 125 | K =:= Key -> 126 | [{Key, Value}|Acc]; 127 | true -> 128 | [{K, V}|Acc] 129 | end, 130 | set_value1(T, Key, Value, Acc1). 131 | 132 | 133 | -ifdef(TEST). 134 | 135 | -include_lib("eunit/include/eunit.hrl"). 136 | 137 | get_value_test() -> 138 | Doc = {[{<<"a">>, 1}]}, 139 | ?assertEqual(1, couchbeam_doc:get_value(<<"a">>, Doc)), 140 | ?assertEqual(1, couchbeam_doc:get_value("a", Doc)), 141 | ?assertEqual(undefined, couchbeam_doc:get_value("b", Doc)), 142 | ?assertEqual(nil, couchbeam_doc:get_value("b", Doc, nil)), 143 | ok. 144 | 145 | set_value_test() -> 146 | Doc = {[{<<"a">>, 1}]}, 147 | ?assertEqual(undefined, couchbeam_doc:get_value("b", Doc)), 148 | Doc1 = couchbeam_doc:set_value("b", 1, Doc), 149 | ?assertEqual(1, couchbeam_doc:get_value("b", Doc1)), 150 | Doc2 = couchbeam_doc:set_value("b", 0, Doc1), 151 | ?assertEqual(0, couchbeam_doc:get_value("b", Doc2)), 152 | ok. 153 | 154 | delete_value_test() -> 155 | Doc = {[{<<"a">>, 1}, {<<"b">>, 1}]}, 156 | Doc1 = couchbeam_doc:delete_value("b", Doc), 157 | ?assertEqual(undefined, couchbeam_doc:get_value("b", Doc1)), 158 | ok. 159 | 160 | extend_test() -> 161 | Doc = {[{<<"a">>, 1}]}, 162 | ?assertEqual(1, couchbeam_doc:get_value("a", Doc)), 163 | ?assertEqual(undefined, couchbeam_doc:get_value("b", Doc)), 164 | ?assertEqual(undefined, couchbeam_doc:get_value("c", Doc)), 165 | Doc1 = couchbeam_doc:extend([{<<"b">>, 1}, {<<"c">>, 1}], Doc), 166 | ?assertEqual(1, couchbeam_doc:get_value("b", Doc1)), 167 | ?assertEqual(1, couchbeam_doc:get_value("c", Doc1)), 168 | Doc2 = couchbeam_doc:extend([{<<"b">>, 3}, {<<"d">>, 1}], Doc1), 169 | ?assertEqual(3, couchbeam_doc:get_value("b", Doc2)), 170 | ?assertEqual(1, couchbeam_doc:get_value("d", Doc2)), 171 | ok. 172 | 173 | id_rev_test() -> 174 | Doc = {[{<<"a">>, 1}]}, 175 | ?assertEqual(undefined, couchbeam_doc:get_id(Doc)), 176 | ?assertEqual(undefined, couchbeam_doc:get_rev(Doc)), 177 | ?assertEqual({undefined, undefined}, couchbeam_doc:get_idrev(Doc)), 178 | Doc1 = couchbeam_doc:extend([{<<"_id">>, 1}, {<<"_rev">>, 1}], Doc), 179 | ?assertEqual(1, couchbeam_doc:get_id(Doc1)), 180 | ?assertEqual(1, couchbeam_doc:get_rev(Doc1)), 181 | ?assertEqual({1, 1}, couchbeam_doc:get_idrev(Doc1)), 182 | ok. 183 | 184 | is_saved_test() -> 185 | Doc = {[{<<"a">>, 1}]}, 186 | ?assertEqual(false, couchbeam_doc:is_saved(Doc)), 187 | Doc1 = couchbeam_doc:set_value(<<"_rev">>, <<"x">>, Doc), 188 | ?assertEqual(true, couchbeam_doc:is_saved(Doc1)), 189 | ok. 190 | 191 | 192 | take_value_test() -> 193 | Doc = {[{<<"a">>, 1}, {<<"b">>, 2}]}, 194 | ?assertEqual({undefined, Doc}, couchbeam_doc:take_value(<<"c">>, Doc)), 195 | ?assertEqual({1, {[{<<"b">>, 2}]}}, couchbeam_doc:take_value(<<"a">>, Doc)), 196 | ok. 197 | 198 | -endif. 199 | 200 | 201 | -------------------------------------------------------------------------------- /src/couchbeam_ejson.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | %%% 6 | 7 | -module(couchbeam_ejson). 8 | 9 | -export([encode/1, decode/1]). 10 | -export([post_decode/1]). 11 | 12 | -include("couchbeam.hrl"). 13 | 14 | 15 | -ifndef('WITH_JIFFY'). 16 | -define(JSON_ENCODE(D), jsx:encode(pre_encode(D))). 17 | -define(JSON_DECODE(D), post_decode(jsx:decode(D, [{return_maps, false}]))). 18 | 19 | -else. 20 | -define(JSON_ENCODE(D), jiffy:encode(D, [uescape])). 21 | -define(JSON_DECODE(D), jiffy:decode(D)). 22 | -endif. 23 | 24 | 25 | -spec encode(ejson()) -> binary(). 26 | 27 | %% @doc encode an erlang term to JSON. Throw an exception if there is 28 | %% any error. 29 | encode(D) -> 30 | ?JSON_ENCODE(D). 31 | 32 | -spec decode(binary()) -> ejson(). 33 | %% @doc decode a binary to an EJSON term. Throw an exception if there is 34 | %% any error. 35 | decode(D) -> 36 | try 37 | ?JSON_DECODE(D) 38 | catch 39 | throw:Error -> 40 | throw({invalid_json, Error}); 41 | error:badarg -> 42 | throw({invalid_json, badarg}) 43 | end. 44 | 45 | pre_encode({[]}) -> 46 | [{}]; 47 | pre_encode({PropList}) -> 48 | pre_encode(PropList); 49 | pre_encode([{_, _}|_] = PropList) -> 50 | [ {Key, pre_encode(Value)} || {Key, Value} <- PropList ]; 51 | pre_encode(List) when is_list(List) -> 52 | [ pre_encode(Term) || Term <- List ]; 53 | pre_encode(true) -> 54 | true; 55 | pre_encode(false) -> 56 | false; 57 | pre_encode(null) -> 58 | null; 59 | pre_encode(Atom) when is_atom(Atom) -> 60 | erlang:atom_to_binary(Atom, utf8); 61 | pre_encode(Term) when is_integer(Term); is_float(Term); is_binary(Term) -> 62 | Term. 63 | 64 | post_decode({[{}]}) -> 65 | {[]}; 66 | post_decode([{}]) -> 67 | {[]}; 68 | post_decode([{_Key, _Value} | _Rest] = PropList) -> 69 | {[ {Key, post_decode(Value)} || {Key, Value} <- PropList ]}; 70 | post_decode(List) when is_list(List) -> 71 | [ post_decode(Term) || Term <- List]; 72 | post_decode({Term}) -> 73 | post_decode(Term); 74 | post_decode(Term) -> 75 | Term. 76 | -------------------------------------------------------------------------------- /src/couchbeam_httpc.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_httpc). 7 | 8 | -export([request/5, 9 | db_request/5, db_request/6, 10 | json_body/1, 11 | db_resp/2, 12 | make_headers/4, 13 | maybe_oauth_header/4]). 14 | %% urls utils 15 | -export([server_url/1, db_url/1, doc_url/2]). 16 | %% atts utols 17 | -export([reply_att/1, wait_mp_doc/2, len_doc_to_mp_stream/3, send_mp_doc/5]). 18 | 19 | -include("couchbeam.hrl"). 20 | 21 | request(Method, Url, Headers, Body, Options) -> 22 | {FinalHeaders, FinalOpts} = make_headers(Method, Url, Headers, 23 | Options), 24 | 25 | hackney:request(Method, Url , FinalHeaders, Body, FinalOpts). 26 | 27 | db_request(Method, Url, Headers, Body, Options) -> 28 | db_request(Method, Url, Headers, Body, Options, []). 29 | 30 | db_request(Method, Url, Headers, Body, Options, Expect) -> 31 | Resp = request(Method, Url, Headers, Body, Options), 32 | db_resp(Resp, Expect). 33 | 34 | json_body(Ref) -> 35 | case hackney:body(Ref) of 36 | {ok, Body} -> 37 | couchbeam_ejson:decode(Body); 38 | {error, _} = Error -> 39 | Error 40 | end. 41 | 42 | make_headers(Method, Url, Headers, Options) -> 43 | Headers1 = case couchbeam_util:get_value(<<"Accept">>, Headers) of 44 | undefined -> 45 | [{<<"Accept">>, <<"application/json, */*;q=0.9">>} | Headers]; 46 | _ -> 47 | Headers 48 | end, 49 | {Headers2, Options1} = maybe_oauth_header(Method, Url, Headers1, Options), 50 | maybe_proxyauth_header(Headers2, Options1). 51 | 52 | 53 | maybe_oauth_header(Method, Url, Headers, Options) -> 54 | case couchbeam_util:get_value(oauth, Options) of 55 | undefined -> 56 | {Headers, Options}; 57 | OauthProps -> 58 | Hdr = couchbeam_util:oauth_header(Url, Method, OauthProps), 59 | {[Hdr|Headers], proplists:delete(oauth, Options)} 60 | end. 61 | 62 | maybe_proxyauth_header(Headers, Options) -> 63 | case couchbeam_util:get_value(proxyauth, Options) of 64 | undefined -> 65 | {Headers, Options}; 66 | ProxyauthProps -> 67 | {lists:append([ProxyauthProps,Headers]), proplists:delete(proxyauth, Options)} 68 | end. 69 | 70 | db_resp({ok, Ref}=Resp, _Expect) when is_reference(Ref) -> 71 | Resp; 72 | db_resp({ok, 401, _}, _Expect) -> 73 | {error, unauthenticated}; 74 | db_resp({ok, 403, _}, _Expect) -> 75 | {error, forbidden}; 76 | db_resp({ok, 404, _}, _Expect) -> 77 | {error, not_found}; 78 | db_resp({ok, 409, _}, _Expect) -> 79 | {error, conflict}; 80 | db_resp({ok, 412, _}, _Expect) -> 81 | {error, precondition_failed}; 82 | db_resp({ok, _, _}=Resp, []) -> 83 | Resp; 84 | db_resp({ok, Status, Headers}=Resp, Expect) -> 85 | case lists:member(Status, Expect) of 86 | true -> Resp; 87 | false -> 88 | {error, {bad_response, {Status, Headers, <<>>}}} 89 | end; 90 | db_resp({ok, 401, _, Ref}, _Expect) -> 91 | hackney:skip_body(Ref), 92 | {error, unauthenticated}; 93 | db_resp({ok, 403, _, Ref}, _Expect) -> 94 | hackney:skip_body(Ref), 95 | {error, forbidden}; 96 | db_resp({ok, 404, _, Ref}, _Expect) -> 97 | hackney:skip_body(Ref), 98 | {error, not_found}; 99 | db_resp({ok, 409, _, Ref}, _Expect) -> 100 | hackney:skip_body(Ref), 101 | {error, conflict}; 102 | db_resp({ok, 412, _, Ref}, _Expect) -> 103 | hackney:skip_body(Ref), 104 | {error, precondition_failed}; 105 | db_resp({ok, _, _, _}=Resp, []) -> 106 | Resp; 107 | db_resp({ok, Status, Headers, Ref}=Resp, Expect) -> 108 | case lists:member(Status, Expect) of 109 | true -> Resp; 110 | false -> {error, {bad_response, {Status, Headers, db_resp_body(Ref)}}} 111 | end; 112 | db_resp(Error, _Expect) -> 113 | Error. 114 | 115 | db_resp_body(Ref) -> 116 | case hackney:body(Ref) of 117 | {ok, Body} -> Body; 118 | {error, _} -> 119 | %% Try to close the connection to prevent leaks 120 | catch hackney:close(Ref), 121 | <<>> 122 | end. 123 | 124 | %% @doc Asemble the server URL for the given client 125 | %% @spec server_url({Host, Port}) -> iolist() 126 | server_url(#server{url=Url}) -> 127 | Url. 128 | 129 | db_url(#db{name=DbName}) -> 130 | DbName. 131 | 132 | doc_url(Db, DocId) -> 133 | iolist_to_binary([db_url(Db), <<"/">>, DocId]). 134 | 135 | 136 | %% attachments handling 137 | 138 | %% @hidden 139 | reply_att(ok) -> 140 | ok; 141 | reply_att(done) -> 142 | done; 143 | reply_att({ok, 404, _, Ref}) -> 144 | hackney:skip_body(Ref), 145 | {error, not_found}; 146 | reply_att({ok, 409, _, Ref}) -> 147 | hackney:skip_body(Ref), 148 | {error, conflict}; 149 | reply_att({ok, Status, _, Ref}) when Status =:= 200 orelse Status =:= 201 -> 150 | case couchbeam_httpc:json_body(Ref) of 151 | {[{<<"ok">>, true}|R]} -> 152 | {ok, {R}}; 153 | {error, _} = Error -> 154 | Error 155 | end; 156 | reply_att({ok, Status, Headers, Ref}) -> 157 | {ok, Body} = hackney:body(Ref), 158 | {error, {bad_response, {Status, Headers, Body}}}; 159 | reply_att(Error) -> 160 | Error. 161 | 162 | %% @hidden 163 | wait_mp_doc(Ref, Buffer) -> 164 | %% we are always waiting for the full doc 165 | case hackney:stream_multipart(Ref) of 166 | {headers, _} -> 167 | wait_mp_doc(Ref, Buffer); 168 | {body, Data} -> 169 | NBuffer = << Buffer/binary, Data/binary >>, 170 | wait_mp_doc(Ref, NBuffer); 171 | end_of_part when Buffer =:= <<>> -> 172 | %% end of part in multipart/mixed 173 | wait_mp_doc(Ref, Buffer); 174 | end_of_part -> 175 | %% decode the doc 176 | {Props} = Doc = couchbeam_ejson:decode(Buffer), 177 | case couchbeam_util:get_value(<<"_attachments">>, Props, {[]}) of 178 | {[]} -> 179 | %% not attachments wait for the eof or the next doc 180 | NState = {Ref, fun() -> wait_mp_doc(Ref, <<>>) end}, 181 | {doc, Doc, NState}; 182 | {Atts} -> 183 | %% start to receive the attachments 184 | %% we get the list of attnames for the versions of 185 | %% couchdb that don't provide the att name in the 186 | %% header. 187 | AttNames = [AttName || {AttName, _} <- Atts], 188 | NState = {Ref, fun() -> 189 | wait_mp_att(Ref, {nil, AttNames}) 190 | end}, 191 | {doc, Doc, NState} 192 | end; 193 | mp_mixed -> 194 | %% we are starting a multipart/mixed (probably on revs) 195 | %% continue 196 | wait_mp_doc(Ref, Buffer); 197 | mp_mixed_eof -> 198 | %% end of multipar/mixed wait for the next doc 199 | wait_mp_doc(Ref, Buffer); 200 | eof -> 201 | eof 202 | end. 203 | 204 | %% @hidden 205 | wait_mp_att(Ref, {AttName, AttNames}) -> 206 | case hackney:stream_multipart(Ref) of 207 | {headers, Headers} -> 208 | %% old couchdb api doesn't give the content-disposition 209 | %% header so we have to use the list of att names given in 210 | %% the doc. Hopefully the erlang parser keeps it in order. 211 | case hackney_headers:get_value(<<"content-disposition">>, 212 | hackney_headers:new(Headers)) of 213 | undefined -> 214 | [Name | Rest] = AttNames, 215 | NState = {Ref, fun() -> 216 | wait_mp_att(Ref, {Name, Rest}) 217 | end}, 218 | {att, Name, NState}; 219 | CDisp -> 220 | {_, Props} = content_disposition(CDisp), 221 | Name = proplists:get_value(<<"filename">>, Props), 222 | [_ | Rest] = AttNames, 223 | NState = {Ref, fun() -> 224 | wait_mp_att(Ref, {Name, Rest}) 225 | end}, 226 | {att, Name, NState} 227 | end; 228 | {body, Data} -> 229 | %% return the attachment par 230 | NState = {Ref, fun() -> wait_mp_att(Ref, {AttName, AttNames}) end}, 231 | {att_body, AttName, Data, NState}; 232 | end_of_part -> 233 | %% wait for the next attachment 234 | NState = {Ref, fun() -> wait_mp_att(Ref, {nil, AttNames}) end}, 235 | {att_eof, AttName, NState}; 236 | mp_mixed_eof -> 237 | %% wait for the next doc 238 | wait_mp_doc(Ref, <<>>); 239 | eof -> 240 | %% we are done with the multipart request 241 | eof 242 | end. 243 | 244 | %% @hidden 245 | content_disposition(Data) -> 246 | hackney_bstr:token_ci(Data, fun 247 | (_Rest, <<>>) -> 248 | {error, badarg}; 249 | (Rest, Disposition) -> 250 | hackney_bstr:params(Rest, fun 251 | (<<>>, Params) -> {Disposition, Params}; 252 | (_Rest2, _) -> {error, badarg} 253 | end) 254 | end). 255 | 256 | %% @hidden 257 | len_doc_to_mp_stream(Atts, Boundary, {Props}) -> 258 | {AttsSize, Stubs} = lists:foldl(fun(Att, {AccSize, AccAtts}) -> 259 | {AttLen, Name, Type, Encoding, _Msg} = att_info(Att), 260 | AccSize1 = AccSize + 261 | 4 + %% \r\n\r\n 262 | AttLen + 263 | byte_size(hackney_bstr:to_binary(AttLen)) + 264 | 4 + %% "\r\n--" 265 | byte_size(Boundary) + 266 | byte_size(Name) + 267 | byte_size(Type) + 268 | byte_size(<<"\r\nContent-Disposition: attachment; filename=\"\"">> ) + 269 | byte_size(<<"\r\nContent-Type: ">>) + 270 | byte_size(<<"\r\nContent-Length: ">>) + 271 | case Encoding of 272 | <<"identity">> -> 273 | 0; 274 | _ -> 275 | byte_size(Encoding) + 276 | byte_size(<<"\r\nContent-Encoding: ">>) 277 | end, 278 | AccAtts1 = [{Name, {[{<<"content_type">>, Type}, 279 | {<<"length">>, AttLen}, 280 | {<<"follows">>, true}, 281 | {<<"encoding">>, Encoding}]}} 282 | | AccAtts], 283 | {AccSize1, AccAtts1} 284 | end, {0, []}, Atts), 285 | 286 | Doc1 = case couchbeam_util:get_value(<<"_attachments">>, Props) of 287 | undefined -> 288 | {Props ++ [{<<"_attachments">>, {Stubs}}]}; 289 | {OldAtts} -> 290 | %% remove updated attachments from the old list of 291 | %% attachments 292 | OldAtts1 = lists:foldl(fun({Name, AttProps}, Acc) -> 293 | case couchbeam_util:get_value(Name, Stubs) of 294 | undefined -> 295 | [{Name, AttProps} | Acc]; 296 | _ -> 297 | Acc 298 | end 299 | end, [], OldAtts), 300 | %% update the list of the attachnebts with the attachments 301 | %% that will be sent in the multipart 302 | FinalAtts = lists:reverse(OldAtts1) ++ Stubs, 303 | {lists:keyreplace(<<"_attachments">>, 1, Props, 304 | {<<"_attachments">>, {FinalAtts}})} 305 | end, 306 | 307 | %% eencode the doc 308 | JsonDoc = couchbeam_ejson:encode(Doc1), 309 | 310 | %% calculate the final size with the doc part 311 | FinalSize = 2 + % "--" 312 | byte_size(Boundary) + 313 | 36 + % "\r\ncontent-type: application/json\r\n\r\n" 314 | byte_size(JsonDoc) + 315 | 4 + % "\r\n--" 316 | byte_size(Boundary) + 317 | + AttsSize + 318 | 2, % "--" 319 | 320 | {FinalSize, JsonDoc, Doc1}. 321 | 322 | %% @hidden 323 | send_mp_doc(Atts, Ref, Boundary, JsonDoc, Doc) -> 324 | %% send the doc 325 | DocParts = [<<"--", Boundary/binary >>, 326 | <<"Content-Type: application/json">>, 327 | <<>>, JsonDoc, <<>>], 328 | DocBin = hackney_bstr:join(DocParts, <<"\r\n">>), 329 | case hackney:send_body(Ref, DocBin) of 330 | ok -> 331 | send_mp_doc_atts(Atts, Ref, Doc, Boundary); 332 | Error -> 333 | Error 334 | end. 335 | 336 | %% @hidden 337 | send_mp_doc_atts([], Ref, Doc, Boundary) -> 338 | %% no more attachments, send the final boundary (eof) 339 | case hackney:send_body(Ref, <<"\r\n--", Boundary/binary, "--" >>) of 340 | ok -> 341 | %% collect the response. 342 | mp_doc_reply(Ref, Doc); 343 | Error -> 344 | Error 345 | end; 346 | 347 | send_mp_doc_atts([Att | Rest], Ref, Doc, Boundary) -> 348 | {AttLen, Name, Type, Encoding, Msg} = att_info(Att), 349 | BinAttLen = hackney_bstr:to_binary(AttLen), 350 | AttHeadersParts = [<<"--", Boundary/binary >>, 351 | <<"Content-Disposition: attachment; filename=\"", Name/binary, "\"" >>, 352 | <<"Content-Type: ", Type/binary >>, 353 | <<"Content-Length: ", BinAttLen/binary >>], 354 | 355 | AttHeadersParts1 = AttHeadersParts ++ 356 | case Encoding of 357 | <<"identity">> -> 358 | [<<>>, <<>>]; 359 | _ -> 360 | [<<"Content-Encoding: ", Encoding/binary >>, <<>>, 361 | <<>>] 362 | end, 363 | AttHeadersBin = hackney_bstr:join(AttHeadersParts1, <<"\r\n">>), 364 | 365 | %% first send the att headers 366 | case hackney:send_body(Ref, AttHeadersBin) of 367 | ok -> 368 | %% send the attachment by itself 369 | case hackney:send_body(Ref, Msg) of 370 | ok -> 371 | %% everything is OK continue to the next attachment 372 | send_mp_doc_atts(Rest, Ref, Doc, Boundary); 373 | Error -> 374 | Error 375 | end; 376 | Error -> 377 | Error 378 | end. 379 | 380 | %% @hidden 381 | mp_doc_reply(Ref, Doc) -> 382 | Resp = hackney:start_response(Ref), 383 | case couchbeam_httpc:db_resp(Resp, [200, 201]) of 384 | {ok, _, _, Ref} -> 385 | {JsonProp} = couchbeam_httpc:json_body(Ref), 386 | NewRev = couchbeam_util:get_value(<<"rev">>, JsonProp), 387 | NewDocId = couchbeam_util:get_value(<<"id">>, JsonProp), 388 | %% set the new doc ID 389 | Doc1 = couchbeam_doc:set_value(<<"_id">>, NewDocId, Doc), 390 | %% set the new rev 391 | FinalDoc = couchbeam_doc:set_value(<<"_rev">>, NewRev, Doc1), 392 | %% return the updated document 393 | {ok, FinalDoc}; 394 | Error -> 395 | Error 396 | end. 397 | 398 | %% @hidden 399 | att_info({Name, {file, Path}=Msg}) -> 400 | CType = mimerl:filename(hackney_bstr:to_binary(Name)), 401 | Len = filelib:file_size(Path), 402 | {Len, Name, CType, <<"identity">>, Msg}; 403 | att_info({Name, Bin}) when is_list(Bin) -> 404 | att_info({Name, iolist_to_binary(Bin)}); 405 | att_info({Name, Bin}) when is_binary(Bin) -> 406 | CType = mimerl:filename(hackney_bstr:to_binary(Name)), 407 | {byte_size(Bin), Name, CType, <<"identity">>, Bin}; 408 | att_info({Name, {file, Path}=Msg, Encoding}) -> 409 | CType = mimerl:filename(hackney_bstr:to_binary(Name)), 410 | Len = filelib:file_size(Path), 411 | {Len, Name, CType, Encoding, Msg}; 412 | att_info({Name, {Fun, _Acc0}=Msg, Len}) when is_function(Fun) -> 413 | {Len, Name, <<"application/octet-stream">>, <<"identity">>, Msg}; 414 | att_info({Name, Fun, Len}) when is_function(Fun) -> 415 | {Len, Name, <<"application/octet-stream">>, <<"identity">>, Fun}; 416 | att_info({Name, Bin, Encoding}) when is_binary(Bin) -> 417 | CType = mimerl:filename(hackney_bstr:to_binary(Name)), 418 | {byte_size(Bin), Name, CType, Encoding, Bin}; 419 | att_info({Name, {Fun, _Acc0}=Msg, Len, Encoding}) when is_function(Fun) -> 420 | {Len, Name, <<"application/octet-stream">>, Encoding, Msg}; 421 | att_info({Name, Fun, Len, Encoding}) when is_function(Fun) -> 422 | {Len, Name, <<"application/octet-stream">>, Encoding, Fun}; 423 | att_info({Name, Bin, CType, Encoding}) when is_binary(Bin) -> 424 | {byte_size(Bin), Name, CType, Encoding, Bin}; 425 | att_info({Name, {Fun, _Acc0}=Msg, Len, CType, Encoding}) 426 | when is_function(Fun) -> 427 | {Len, Name, CType, Encoding, Msg}; 428 | att_info({Name, Fun, Len, CType, Encoding}) when is_function(Fun) -> 429 | {Len, Name, CType, Encoding, Fun}. 430 | -------------------------------------------------------------------------------- /src/couchbeam_sup.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_sup). 7 | -author('Benoît Chesneau '). 8 | -behaviour(supervisor). 9 | 10 | -export([start_link/0, init/1]). 11 | 12 | -define(SERVER, ?MODULE). 13 | 14 | 15 | start_link() -> 16 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 17 | 18 | init([]) -> 19 | %% gen_server to cache UUIDs generated by the couchdb node. 20 | Uuids = {couchbeam_uuids, 21 | {couchbeam_uuids, start_link, []}, 22 | permanent,2000,worker, [couchbeam_uuids]}, 23 | 24 | %% view stream supervisor 25 | ViewSup = {couchbeam_view_sup, 26 | {couchbeam_view_sup, start_link, []}, 27 | permanent, 2000, supervisor, [couchbeam_view_sup]}, 28 | 29 | %% changes stream supervisor 30 | ChangesSup = {couchbeam_changes_sup, 31 | {couchbeam_changes_sup, start_link, []}, 32 | permanent, 2000, supervisor, [couchbeam_changes_sup]}, 33 | 34 | {ok, {{one_for_one, 10, 3600}, [Uuids, ViewSup, ChangesSup]}}. 35 | -------------------------------------------------------------------------------- /src/couchbeam_util.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_util). 7 | 8 | -include_lib("hackney/include/hackney.hrl"). 9 | -include_lib("hackney/include/hackney_lib.hrl"). 10 | 11 | -export([dbname/1]). 12 | -export([encode_docid/1, encode_att_name/1]). 13 | -export([parse_options/1, parse_options/2]). 14 | -export([to_list/1, to_binary/1, to_integer/1, to_atom/1]). 15 | -export([binary_env/2]). 16 | -export([encode_query/1, encode_query_value/2]). 17 | -export([oauth_header/3]). 18 | -export([propmerge/3, propmerge1/2]). 19 | -export([get_value/2, get_value/3]). 20 | -export([deprecated/3, shutdown_sync/1]). 21 | -export([start_app_deps/1, get_app_env/2]). 22 | -export([encode_docid1/1, encode_docid_noop/1]). 23 | -export([force_param/3]). 24 | -export([proxy_token/2, proxy_header/3]). 25 | 26 | -define(PROXY_AUTH_HEADERS,[ 27 | {username,<<"X-Auth-CouchDB-UserName">>}, 28 | {roles,<<"X-Auth-CouchDB-Roles">>}, 29 | {token,<<"X-Auth-CouchDB-Token">>}]). 30 | 31 | -define(ENCODE_DOCID_FUNC, encode_docid1). 32 | 33 | dbname(DbName) when is_list(DbName) -> 34 | list_to_binary(DbName); 35 | dbname(DbName) when is_binary(DbName) -> 36 | DbName; 37 | dbname(DbName) -> 38 | erlang:error({illegal_database_name, DbName}). 39 | 40 | encode_att_name(Name) when is_binary(Name) -> 41 | encode_att_name(xmerl_ucs:from_utf8(Name)); 42 | encode_att_name(Name) -> 43 | Parts = lists:foldl(fun(P, Att) -> 44 | [xmerl_ucs:to_utf8(P)|Att] 45 | end, [], string:tokens(Name, "/")), 46 | ?MODULE:to_binary(lists:flatten(Parts)). 47 | 48 | encode_docid(DocId) when is_list(DocId) -> 49 | encode_docid(list_to_binary(DocId)); 50 | encode_docid(DocId)-> 51 | ?ENCODE_DOCID_FUNC(DocId). 52 | 53 | encode_docid1(DocId) -> 54 | case DocId of 55 | << "_design/", Rest/binary >> -> 56 | Rest1 = hackney_url:urlencode(Rest, [noplus]), 57 | <<"_design/", Rest1/binary >>; 58 | _ -> 59 | hackney_url:urlencode(DocId, [noplus]) 60 | end. 61 | 62 | encode_docid_noop(DocId) -> 63 | DocId. 64 | 65 | %% @doc Encode needed value of Query proplists in json 66 | encode_query([]) -> 67 | []; 68 | encode_query(QSL) when is_list(QSL) -> 69 | lists:foldl(fun({K, V}, Acc) -> 70 | V1 = encode_query_value(K, V), 71 | [{K, V1}|Acc] 72 | end, [], QSL); 73 | encode_query(QSL) -> 74 | QSL. 75 | 76 | %% @doc Encode value in JSON if needed depending on the key 77 | encode_query_value(K, V) when is_atom(K) -> 78 | encode_query_value(atom_to_list(K), V); 79 | encode_query_value(K, V) when is_binary(K) -> 80 | encode_query_value(binary_to_list(K), V); 81 | encode_query_value(_K, V) -> V. 82 | 83 | % build oauth header 84 | oauth_header(Url, Action, OauthProps) when is_binary(Url) -> 85 | oauth_header(binary_to_list(Url),Action, OauthProps); 86 | oauth_header(Url, Action, OauthProps) -> 87 | #hackney_url{qs=QS} = hackney_url:parse_url(Url), 88 | QSL = [{binary_to_list(K), binary_to_list(V)} || {K,V} <- 89 | hackney_url:parse_qs(QS)], 90 | 91 | % get oauth paramerers 92 | ConsumerKey = to_list(get_value(consumer_key, OauthProps)), 93 | Token = to_list(get_value(token, OauthProps)), 94 | TokenSecret = to_list(get_value(token_secret, OauthProps)), 95 | ConsumerSecret = to_list(get_value(consumer_secret, OauthProps)), 96 | SignatureMethodStr = to_list(get_value(signature_method, 97 | OauthProps, "HMAC-SHA1")), 98 | 99 | SignatureMethodAtom = case SignatureMethodStr of 100 | "PLAINTEXT" -> 101 | plaintext; 102 | "HMAC-SHA1" -> 103 | hmac_sha1; 104 | "RSA-SHA1" -> 105 | rsa_sha1 106 | end, 107 | Consumer = {ConsumerKey, ConsumerSecret, SignatureMethodAtom}, 108 | Method = case Action of 109 | delete -> "DELETE"; 110 | get -> "GET"; 111 | post -> "POST"; 112 | put -> "PUT"; 113 | head -> "HEAD" 114 | end, 115 | Params = oauth:sign(Method, Url, QSL, Consumer, Token, TokenSecret) -- QSL, 116 | 117 | Realm = "OAuth " ++ oauth:header_params_encode(Params), 118 | {<<"Authorization">>, list_to_binary(Realm)}. 119 | 120 | 121 | %% @doc merge 2 proplists. All the Key - Value pairs from both proplists 122 | %% are included in the new proplists. If a key occurs in both dictionaries 123 | %% then Fun is called with the key and both values to return a new 124 | %% value. This a wreapper around dict:merge 125 | propmerge(F, L1, L2) -> 126 | dict:to_list(dict:merge(F, dict:from_list(L1), dict:from_list(L2))). 127 | 128 | %% @doc Update a proplist with values of the second. In case the same 129 | %% key is in 2 proplists, the value from the first are kept. 130 | propmerge1(L1, L2) -> 131 | propmerge(fun(_, V1, _) -> V1 end, L1, L2). 132 | 133 | %% @doc replace a value in a proplist 134 | force_param(Key, Value, Options) -> 135 | case couchbeam_util:get_value(Key, Options) of 136 | undefined -> 137 | [{Key, Value} | Options]; 138 | _ -> 139 | lists:keystore(Key, 1, Options, {Key, Value}) 140 | end. 141 | 142 | %% @doc emulate proplists:get_value/2,3 but use faster lists:keyfind/3 143 | -spec get_value(Key :: term(), Prop :: [term()]) -> term(). 144 | get_value(Key, Prop) -> 145 | get_value(Key, Prop, undefined). 146 | 147 | -spec get_value(Key :: term(), Prop :: [term()], Default :: term()) -> term(). 148 | get_value(Key, Prop, Default) -> 149 | case lists:keyfind(Key, 1, Prop) of 150 | false -> 151 | case lists:member(Key, Prop) of 152 | true -> true; 153 | false -> Default 154 | end; 155 | {Key, V} -> % only return V if a two-tuple is found 156 | V; 157 | Other when is_tuple(Other) -> % otherwise return the default 158 | Default 159 | end. 160 | 161 | %% @doc make view options a list 162 | parse_options(Options) -> 163 | parse_options(Options, []). 164 | 165 | parse_options([], Acc) -> 166 | Acc; 167 | parse_options([V|Rest], Acc) when is_atom(V) -> 168 | parse_options(Rest, [{atom_to_list(V), true}|Acc]); 169 | parse_options([{K,V}|Rest], Acc) when is_list(K) -> 170 | parse_options(Rest, [{K,V}|Acc]); 171 | parse_options([{K,V}|Rest], Acc) when is_binary(K) -> 172 | parse_options(Rest, [{binary_to_list(K),V}|Acc]); 173 | parse_options([{K,V}|Rest], Acc) when is_atom(K) -> 174 | parse_options(Rest, [{atom_to_list(K),V}|Acc]); 175 | parse_options(_,_) -> 176 | fail. 177 | 178 | to_binary(V) when is_binary(V) -> 179 | V; 180 | to_binary(V) when is_list(V) -> 181 | try 182 | list_to_binary(V) 183 | catch 184 | _ -> 185 | list_to_binary(io_lib:format("~p", [V])) 186 | end; 187 | to_binary(V) when is_atom(V) -> 188 | list_to_binary(atom_to_list(V)); 189 | to_binary(V) -> 190 | V. 191 | 192 | to_integer(V) when is_integer(V) -> 193 | V; 194 | to_integer(V) when is_list(V) -> 195 | erlang:list_to_integer(V); 196 | to_integer(V) when is_binary(V) -> 197 | erlang:list_to_integer(binary_to_list(V)). 198 | 199 | to_list(V) when is_list(V) -> 200 | V; 201 | to_list(V) when is_binary(V) -> 202 | binary_to_list(V); 203 | to_list(V) when is_atom(V) -> 204 | atom_to_list(V); 205 | to_list(V) -> 206 | V. 207 | 208 | to_atom(V) when is_atom(V) -> 209 | V; 210 | to_atom(V) when is_list(V) -> 211 | list_to_atom(V); 212 | to_atom(V) when is_binary(V) -> 213 | list_to_atom(binary_to_list(V)); 214 | to_atom(V) -> 215 | list_to_atom(lists:flatten(io_lib:format("~p", [V]))). 216 | 217 | 218 | binary_env(Key, Default) -> 219 | Value = os:getenv(Key, Default), 220 | to_binary(Value). 221 | 222 | deprecated(Old, New, When) -> 223 | io:format( 224 | << 225 | "WARNING: function deprecated~n" 226 | "Function '~p' has been deprecated~n" 227 | "in favor of '~p'.~n" 228 | "'~p' will be removed ~s.~n~n" 229 | >>, [Old, New, Old, When]). 230 | 231 | shutdown_sync(Pid) when not is_pid(Pid)-> 232 | ok; 233 | shutdown_sync(Pid) -> 234 | MRef = erlang:monitor(process, Pid), 235 | try 236 | catch unlink(Pid), 237 | catch exit(Pid, shutdown), 238 | receive 239 | {'DOWN', MRef, _, _, _} -> 240 | ok 241 | end 242 | after 243 | erlang:demonitor(MRef, [flush]) 244 | end. 245 | 246 | %% @spec start_app_deps(App :: atom()) -> ok 247 | %% @doc Start depedent applications of App. 248 | start_app_deps(App) -> 249 | {ok, DepApps} = application:get_key(App, applications), 250 | [ensure_started(A) || A <- DepApps], 251 | ok. 252 | 253 | %% @spec ensure_started(Application :: atom()) -> ok 254 | %% @doc Start the named application if not already started. 255 | ensure_started(App) -> 256 | case application:start(App) of 257 | ok -> 258 | ok; 259 | {error, {already_started, App}} -> 260 | ok 261 | end. 262 | 263 | get_app_env(Env, Default) -> 264 | case application:get_env(couchbeam, Env) of 265 | {ok, Val} -> Val; 266 | undefined -> Default 267 | end. 268 | 269 | proxy_header(UserName,Roles,Secret) -> 270 | proxy_header(UserName,Roles,Secret,?PROXY_AUTH_HEADERS). 271 | 272 | proxy_header(UserName,Roles,Secret,HeaderNames) -> 273 | proxy_header_token(UserName,Roles,proxy_token(Secret,UserName),HeaderNames). 274 | 275 | proxy_header_token(UserName,Roles,Token,L) -> 276 | [ 277 | {hgv(username,L), UserName}, 278 | {hgv(roles,L), Roles}, 279 | {hgv(token,L), Token} 280 | ]. 281 | 282 | hgv(N,L) -> 283 | get_value(N,L,get_value(N,?PROXY_AUTH_HEADERS)). 284 | 285 | proxy_token(Secret,UserName) -> 286 | hackney_bstr:to_hex(hmac(sha, Secret, UserName)). 287 | 288 | -ifdef(USE_CRYPTO_MAC). 289 | hmac(Alg, Key, Data) -> 290 | crypto:mac(hmac, Alg, Key, Data). 291 | -else. 292 | hmac(Alg, Key, Data) -> 293 | crypto:hmac(Alg, Key, Data). 294 | -endif. 295 | -------------------------------------------------------------------------------- /src/couchbeam_uuids.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_uuids). 7 | -behaviour(gen_server). 8 | 9 | -export([random/0, 10 | utc_random/0]). 11 | 12 | -export([start_link/0]). 13 | -export([get_uuids/2]). 14 | 15 | %% gen_server callbacks 16 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 17 | terminate/2, code_change/3]). 18 | 19 | 20 | -include("couchbeam.hrl"). 21 | 22 | -define(START_RETRY, 200). 23 | -define(MAX_RETRY, 10000). 24 | 25 | -record(state, {}). 26 | 27 | %% @doc return a random uuid 28 | -spec random() -> binary(). 29 | random() -> 30 | list_to_binary(hackney_bstr:to_hex(crypto:strong_rand_bytes(16))). 31 | 32 | %% @doc return a random uuid based on time 33 | -spec utc_random() -> binary(). 34 | utc_random() -> 35 | utc_suffix(hackney_bstr:to_hex(crypto:strong_rand_bytes(9))). 36 | 37 | %% @doc Get a list of uuids from the server 38 | %% @spec get_uuids(server(), integer()) -> lists() 39 | get_uuids(Server, Count) -> 40 | gen_server:call(?MODULE, {get_uuids, Server, Count}, infinity). 41 | 42 | %%-------------------------------------------------------------------- 43 | %% Function: start_link/0 44 | %% Description: Starts the server 45 | %%-------------------------------------------------------------------- 46 | %% @doc Starts the couchbeam process linked to the calling process. Usually 47 | %% invoked by the supervisor couchbeam_sup 48 | %% @spec start_link() -> {ok, pid()} 49 | start_link() -> 50 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 51 | 52 | %%--------------------------------------------------------------------------- 53 | %% gen_server callbacks 54 | %%--------------------------------------------------------------------------- 55 | %% @private 56 | 57 | init(_) -> 58 | process_flag(trap_exit, true), 59 | ets:new(couchbeam_uuids, [named_table, public, {keypos, 2}]), 60 | {ok, #state{}}. 61 | 62 | handle_call({get_uuids, #server{url=Url}=Server, Count}, 63 | _From, State) -> 64 | {ok, Uuids} = do_get_uuids(Server, Count, [], 65 | ets:lookup(couchbeam_uuids, Url)), 66 | {reply, Uuids, State}. 67 | 68 | 69 | handle_cast(_Msg, State) -> 70 | {noreply, State}. 71 | 72 | 73 | handle_info(_Info, State) -> 74 | {noreply, State}. 75 | 76 | terminate(_Reason, _State) -> 77 | ok. 78 | 79 | code_change(_OldVsn, State, _Extra) -> 80 | {ok, State}. 81 | 82 | %%-------------------------------------------------------------------- 83 | %%% Internal functions 84 | %%-------------------------------------------------------------------- 85 | do_get_uuids(_Server, Count, Acc, _) when length(Acc) >= Count -> 86 | {ok, Acc}; 87 | do_get_uuids(Server, Count, Acc, []) -> 88 | {ok, ServerUuids} = get_new_uuids(Server), 89 | do_get_uuids(Server, Count, Acc, [ServerUuids]); 90 | do_get_uuids(Server, Count, Acc, [#server_uuids{uuids=Uuids}]) -> 91 | case Uuids of 92 | [] -> 93 | {ok, ServerUuids} = get_new_uuids(Server), 94 | do_get_uuids(Server, Count, Acc, [ServerUuids]); 95 | _ when length(Uuids) < Count -> 96 | {ok, ServerUuids} = get_new_uuids(Server, Uuids), 97 | do_get_uuids(Server, Count, Acc, [ServerUuids]); 98 | _ -> 99 | {Acc1, Uuids1} = do_get_uuids1(Acc, Uuids, Count), 100 | #server{url=Url} = Server, 101 | ServerUuids = #server_uuids{server_url=Url, uuids=Uuids1}, 102 | ets:insert(couchbeam_uuids, ServerUuids), 103 | do_get_uuids(Server, Count, Acc1, [ServerUuids]) 104 | end. 105 | 106 | 107 | 108 | do_get_uuids1(Acc, Uuids, 0) -> 109 | {Acc, Uuids}; 110 | do_get_uuids1(Acc, [Uuid|Rest], Count) -> 111 | do_get_uuids1([Uuid|Acc], Rest, Count-1). 112 | 113 | 114 | get_new_uuids(Server) -> 115 | get_new_uuids(Server, []). 116 | 117 | get_new_uuids(Server, Acc) -> 118 | get_new_uuids(Server, ?START_RETRY, Acc). 119 | 120 | get_new_uuids(#server{url=ServerUrl, options=Opts}=Server, Backoff, Acc) -> 121 | Count = list_to_binary(integer_to_list(1000 - length(Acc))), 122 | Url = hackney_url:make_url(ServerUrl, <<"/_uuids">>, [{<<"count">>, Count}]), 123 | case couchbeam_httpc:request(get, Url, [], <<>>, Opts) of 124 | {ok, 200, _, Ref} -> 125 | {ok, Body} = hackney:body(Ref), 126 | {[{<<"uuids">>, Uuids}]} = couchbeam_ejson:decode(Body), 127 | ServerUuids = #server_uuids{server_url=ServerUrl, 128 | uuids=(Acc ++ Uuids)}, 129 | ets:insert(couchbeam_uuids, ServerUuids), 130 | {ok, ServerUuids}; 131 | {ok, Status, Headers, Ref} -> 132 | {ok, Body} = hackney:body(Ref), 133 | {error, {bad_response, {Status, Headers, Body}}}; 134 | {error, closed} -> 135 | wait_for_retry(Server, Backoff, Acc); 136 | {error, connect_timeout} -> 137 | wait_for_retry(Server, Backoff, Acc); 138 | Error -> 139 | Error 140 | end. 141 | 142 | wait_for_retry(_S, ?MAX_RETRY, _A) -> 143 | {error, max_retry}; 144 | wait_for_retry(S, B, A) -> 145 | %% increment backoff 146 | B2 = min(B bsl 1, ?MAX_RETRY), 147 | timer:sleep(B2), 148 | get_new_uuids(S, B2, A). 149 | 150 | utc_suffix(Suffix) -> 151 | Now = {_, _, Micro} = erlang:timestamp(), 152 | Nowish = calendar:now_to_universal_time(Now), 153 | Nowsecs = calendar:datetime_to_gregorian_seconds(Nowish), 154 | Then = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), 155 | Prefix = io_lib:format("~14.16.0b", [(Nowsecs - Then) * 1000000 + Micro]), 156 | list_to_binary(Prefix ++ Suffix). 157 | -------------------------------------------------------------------------------- /src/couchbeam_view_stream.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_view_stream). 7 | 8 | -export([start_link/4]). 9 | 10 | -export([init_stream/5, 11 | maybe_continue/1, 12 | system_continue/3, 13 | system_terminate/4, 14 | system_code_change/4]). 15 | 16 | 17 | -export([init/1, 18 | handle_event/2, 19 | wait_rows/2, 20 | wait_rows1/2, 21 | wait_val/2, 22 | collect_object/2, 23 | maybe_continue_decoding/1]). 24 | 25 | 26 | -include("couchbeam.hrl"). 27 | 28 | -record(state, {parent, 29 | owner, 30 | req, 31 | ref, 32 | mref, 33 | client_ref=nil, 34 | decoder, 35 | async=normal}). 36 | 37 | -record(viewst, {parent, 38 | owner, 39 | ref, 40 | mref, 41 | client_ref, 42 | async=false}). 43 | 44 | 45 | -define(TIMEOUT, 10000). 46 | -define(DEFAULT_CHANGES_POLL, 5000). % we check every 5secs 47 | 48 | 49 | 50 | start_link(Owner, StreamRef, {Db, Url, Args}, StreamOptions) -> 51 | proc_lib:start_link(?MODULE, init_stream, [self(), Owner, StreamRef, 52 | {Db, Url, Args}, 53 | StreamOptions]). 54 | 55 | init_stream(Parent, Owner, StreamRef, {_Db, _Url, _Args}=Req, 56 | StreamOptions) -> 57 | 58 | 59 | Async = proplists:get_value(async, StreamOptions, normal), 60 | 61 | %% monitor the process receiving the messages 62 | MRef = erlang:monitor(process, Owner), 63 | 64 | %% tell to the parent that we are ok 65 | proc_lib:init_ack(Parent, {ok, self()}), 66 | 67 | InitState = #state{parent=Parent, 68 | owner=Owner, 69 | req=Req, 70 | ref=StreamRef, 71 | mref=MRef, 72 | async=Async}, 73 | 74 | %% connect to the view 75 | try 76 | case do_init_stream(Req, InitState) of 77 | {ok, State} -> 78 | %% register the stream 79 | ets:insert(couchbeam_view_streams, [{StreamRef, self()}]), 80 | %% start the loop 81 | loop(State); 82 | Error -> 83 | report_error(Error, StreamRef, Owner) 84 | end 85 | after 86 | %% Always clean up the monitor reference 87 | erlang:demonitor(MRef, [flush]) 88 | end, 89 | ok. 90 | 91 | do_init_stream({#db{options=Opts}, Url, Args}, #state{mref=MRef}=State) -> 92 | %% we are doing the request asynchronously 93 | FinalOpts = [{async, once} | Opts], 94 | Reply = case Args#view_query_args.method of 95 | get -> 96 | couchbeam_httpc:request(get, Url, [], <<>>, FinalOpts); 97 | post -> 98 | Body = couchbeam_ejson:encode({[{<<"keys">>, 99 | Args#view_query_args.keys}]}), 100 | Headers = [{<<"Content-Type">>, <<"application/json">>}], 101 | couchbeam_httpc:request(post, Url, Headers, Body, FinalOpts) 102 | end, 103 | 104 | case Reply of 105 | {ok, Ref} -> 106 | Req = hackney:request_info(Ref), 107 | Mod = proplists:get_value(transport, Req), 108 | S = proplists:get_value(socket, Req), 109 | _ = Mod:controlling_process(S, self()), 110 | receive 111 | {'DOWN', MRef, _, _, _} -> 112 | %% parent exited there is no need to continue 113 | exit(normal); 114 | {hackney_response, Ref, {status, 200, _}} -> 115 | #state{parent=Parent, 116 | owner=Owner, 117 | ref=StreamRef, 118 | async=Async} = State, 119 | 120 | DecoderFun = jsx:decoder(?MODULE, [Parent, Owner, 121 | StreamRef, MRef, Ref, 122 | Async], [stream]), 123 | {ok, State#state{client_ref=Ref, 124 | decoder=DecoderFun}}; 125 | 126 | {hackney_response, Ref, {status, 404, _}} -> 127 | {error, not_found}; 128 | {hackney_response, Ref, {status, Status, Reason}} -> 129 | {error, {http_error, Status, Reason}}; 130 | {hackney_response, Ref, {error, Reason}} -> 131 | {error, Reason} 132 | after ?TIMEOUT -> 133 | {error, timeout} 134 | end; 135 | Error -> 136 | {error, Error} 137 | end. 138 | 139 | 140 | 141 | loop(#state{owner=Owner, 142 | ref=StreamRef, 143 | mref=MRef, 144 | client_ref=ClientRef}=State) -> 145 | 146 | hackney:stream_next(ClientRef), 147 | receive 148 | {'DOWN', MRef, _, _, _} -> 149 | %% parent exited there is no need to continue 150 | exit(normal); 151 | {hackney_response, ClientRef, {headers, _Headers}} -> 152 | loop(State); 153 | {hackney_response, ClientRef, done} -> 154 | %% unregister the stream 155 | ets:delete(couchbeam_view_streams, StreamRef), 156 | %% tell to the owner that we are done and exit, 157 | Owner ! {StreamRef, done}; 158 | {hackney_response, ClientRef, Data} when is_binary(Data) -> 159 | decode_data(Data, State); 160 | {hackney_response, ClientRef, Error} -> 161 | ets:delete(couchbeam_view_streams, StreamRef), 162 | %% report the error 163 | report_error(Error, StreamRef, Owner), 164 | exit(Error) 165 | end. 166 | 167 | decode_data(Data, #state{owner=Owner, 168 | ref=StreamRef, 169 | client_ref=ClientRef, 170 | decoder=DecodeFun}=State) -> 171 | try 172 | {incomplete, DecodeFun2} = DecodeFun(Data), 173 | try DecodeFun2(end_stream) of done -> 174 | %% stop the request 175 | {ok, _} = hackney:stop_async(ClientRef), 176 | %% skip the rest of the body so the socket is 177 | %% replaced in the pool 178 | catch hackney:skip_body(ClientRef), 179 | %% unregister the stream 180 | ets:delete(couchbeam_view_streams, StreamRef), 181 | %% tell to the owner that we are done and exit, 182 | Owner ! {StreamRef, done} 183 | catch error:badarg -> 184 | maybe_continue(State#state{decoder=DecodeFun2}) 185 | end 186 | catch error:badarg -> 187 | maybe_close(State), 188 | exit(badarg) 189 | end. 190 | 191 | maybe_continue(#state{parent=Parent, owner=Owner, ref=Ref, mref=MRef, 192 | async=once}=State) -> 193 | 194 | receive 195 | {'DOWN', MRef, _, _, _} -> 196 | %% parent exited there is no need to continue 197 | maybe_close(State), 198 | exit(normal); 199 | {Ref, stream_next} -> 200 | loop(State); 201 | {Ref, cancel} -> 202 | maybe_close(State), 203 | %% unregister the stream 204 | ets:delete(couchbeam_view_streams, Ref), 205 | %% tell the parent we exited 206 | Owner ! {Ref, ok}; 207 | {system, From, Request} -> 208 | sys:handle_system_msg(Request, From, Parent, ?MODULE, [], 209 | {loop, State}); 210 | Else -> 211 | error_logger:error_msg("Unexpected message: ~w~n", [Else]), 212 | %% unregister the stream 213 | ets:delete(couchbeam_view_streams, Ref), 214 | %% report the error 215 | report_error(Else, Ref, Owner), 216 | exit(Else) 217 | after 0 -> 218 | loop(State) 219 | end; 220 | maybe_continue(#state{parent=Parent, 221 | owner=Owner, 222 | ref=Ref, 223 | mref=MRef}=State) -> 224 | receive 225 | {'DOWN', MRef, _, _, _} -> 226 | %% parent exited there is no need to continue 227 | maybe_close(State), 228 | exit(normal); 229 | {Ref, cancel} -> 230 | maybe_close(State), 231 | %% unregister the stream 232 | ets:delete(couchbeam_view_streams, Ref), 233 | %% tell the parent we exited 234 | Owner ! {Ref, ok}; 235 | {Ref, pause} -> 236 | erlang:hibernate(?MODULE, maybe_continue, [State]); 237 | {Ref, resume} -> 238 | loop(State); 239 | {system, From, Request} -> 240 | sys:handle_system_msg(Request, From, Parent, ?MODULE, [], 241 | {loop, State}); 242 | Else -> 243 | error_logger:error_msg("Unexpected message: ~w~n", [Else]), 244 | %% unregister the stream 245 | ets:delete(couchbeam_view_streams, Ref), 246 | %% report the error 247 | report_error(Else, Ref, Owner), 248 | exit(Else) 249 | after 0 -> 250 | loop(State) 251 | end. 252 | 253 | maybe_close(#state{client_ref=nil}) -> 254 | ok; 255 | maybe_close(#state{client_ref=Ref}) -> 256 | hackney:close(Ref). 257 | 258 | 259 | system_continue(_, _, {maybe_continue, State}) -> 260 | maybe_continue(State); 261 | system_continue(_, _, {loop, State}) -> 262 | loop(State). 263 | 264 | -spec system_terminate(any(), _, _, _) -> no_return(). 265 | system_terminate(Reason, _, _, #state{ref=StreamRef, 266 | client_ref=ClientRef}) -> 267 | hackney:close(ClientRef), 268 | %% unregister the stream 269 | catch ets:delete(couchbeam_view_streams, StreamRef), 270 | exit(Reason). 271 | 272 | system_code_change(Misc, _, _, _) -> 273 | {ok, Misc}. 274 | 275 | 276 | %%% json decoder %%% 277 | 278 | init([Parent, Owner, StreamRef, MRef, ClientRef, Async]) -> 279 | InitialState = #viewst{parent=Parent, 280 | owner=Owner, 281 | ref=StreamRef, 282 | mref=MRef, 283 | client_ref=ClientRef, 284 | async=Async}, 285 | {wait_rows, 0, [[]], InitialState}. 286 | 287 | handle_event(end_json, _) -> 288 | done; 289 | handle_event(Event, {Fun, _, _, _}=St) -> 290 | ?MODULE:Fun(Event, St). 291 | 292 | 293 | 294 | wait_rows(start_object, St) -> 295 | St; 296 | wait_rows(end_object, St) -> 297 | St; 298 | wait_rows({key, <<"rows">>}, {_, _, _, ViewSt}) -> 299 | {wait_rows1, 0, [[]], ViewSt}; 300 | wait_rows({key, <<"total_rows">>}, {_, _, _, ViewSt}) -> 301 | {wait_val, 0, [[]], ViewSt}; 302 | wait_rows({key, <<"offset">>}, {_, _, _, ViewSt}) -> 303 | {wait_val, 0, [[]], ViewSt}. 304 | 305 | wait_val({_, _}, {_, _, _, ViewSt}) -> 306 | {wait_rows, 0, [[]], ViewSt}. 307 | 308 | wait_rows1(start_array, {_, _, _, ViewSt}) -> 309 | {wait_rows1, 0, [[]], ViewSt}; 310 | wait_rows1(start_object, {_, _, Terms, ViewSt}) -> 311 | {collect_object, 0, [[]|Terms], ViewSt}; 312 | wait_rows1(end_array, {_, _, _, ViewSt}) -> 313 | {wait_rows, 0, [[]], ViewSt}. 314 | 315 | 316 | collect_object(start_object, {_, NestCount, Terms, ViewSt}) -> 317 | {collect_object, NestCount + 1, [[]|Terms], ViewSt}; 318 | 319 | collect_object(end_object, {_, NestCount, [[], {key, Key}, Last|Terms], 320 | ViewSt}) -> 321 | {collect_object, NestCount - 1, [[{Key, {[{}]}}] ++ Last] ++ Terms, 322 | ViewSt}; 323 | 324 | collect_object(end_object, {_, NestCount, [Object, {key, Key}, 325 | Last|Terms], ViewSt}) -> 326 | {collect_object, NestCount - 1, 327 | [[{Key, {lists:reverse(Object)}}] ++ Last] ++ Terms, ViewSt}; 328 | 329 | collect_object(end_object, {_, 0, [[], Last|Terms], ViewSt}) -> 330 | [[Row]] = [[{[{}]}] ++ Last] ++ Terms, 331 | send_row(Row, ViewSt); 332 | 333 | collect_object(end_object, {_, NestCount, [[], Last|Terms], ViewSt}) -> 334 | {collect_object, NestCount - 1, [[{[{}]}] ++ Last] ++ Terms, ViewSt}; 335 | 336 | collect_object(end_object, {_, 0, [Object, Last|Terms], ViewSt}) -> 337 | [[Row]] = [[{lists:reverse(Object)}] ++ Last] ++ Terms, 338 | send_row(Row, ViewSt); 339 | 340 | 341 | collect_object(end_object, {_, NestCount, [Object, Last|Terms], ViewSt}) -> 342 | Acc = [[{lists:reverse(Object)}] ++ Last] ++ Terms, 343 | {collect_object, NestCount - 1, Acc, ViewSt}; 344 | 345 | 346 | collect_object(start_array, {_, NestCount, Terms, ViewSt}) -> 347 | {collect_object, NestCount, [[]|Terms], ViewSt}; 348 | collect_object(end_array, {_, NestCount, [List, {key, Key}, Last|Terms], 349 | ViewSt}) -> 350 | {collect_object, NestCount, 351 | [[{Key, lists:reverse(List)}] ++ Last] ++ Terms, ViewSt}; 352 | collect_object(end_array, {_, NestCount, [List, Last|Terms], ViewSt}) -> 353 | {collect_object, NestCount, [[lists:reverse(List)] ++ Last] ++ Terms, 354 | ViewSt}; 355 | 356 | collect_object({key, Key}, {_, NestCount, Terms, ViewSt}) -> 357 | {collect_object, NestCount, [{key, Key}] ++ Terms, 358 | ViewSt}; 359 | 360 | collect_object({_, Event}, {_, NestCount, [{key, Key}, Last|Terms], ViewSt}) -> 361 | {collect_object, NestCount, [[{Key, Event}] ++ Last] ++ Terms, ViewSt}; 362 | collect_object({_, Event}, {_, NestCount, [Last|Terms], ViewSt}) -> 363 | {collect_object, NestCount, [[Event] ++ Last] ++ Terms, ViewSt}. 364 | 365 | send_row(Row, #viewst{owner=Owner, ref=Ref}=ViewSt) -> 366 | Owner ! {Ref, {row, couchbeam_ejson:post_decode(Row)}}, 367 | maybe_continue_decoding(ViewSt). 368 | 369 | %% eventually wait for the next call from the parent 370 | maybe_continue_decoding(#viewst{parent=Parent, 371 | owner=Owner, 372 | ref=Ref, 373 | mref=MRef, 374 | client_ref=ClientRef, 375 | async=once}=ViewSt) -> 376 | receive 377 | {'DOWN', MRef, _, _, _} -> 378 | %% parent exited there is no need to continue 379 | exit(normal); 380 | {Ref, stream_next} -> 381 | {wait_rows1, 0, [[]], ViewSt}; 382 | {Ref, cancel} -> 383 | hackney:close(ClientRef), 384 | %% unregister the stream 385 | ets:delete(couchbeam_view_streams, Ref), 386 | %% tell the parent we exited 387 | Owner ! {Ref, ok}, 388 | %% and exit 389 | exit(normal); 390 | {system, From, Request} -> 391 | sys:handle_system_msg(Request, From, Parent, ?MODULE, [], 392 | {maybe_continue_decoding, ViewSt}); 393 | Else -> 394 | error_logger:error_msg("Unexpected message: ~w~n", [Else]), 395 | %% unregister the stream 396 | ets:delete(couchbeam_view_streams, Ref), 397 | %% report the error 398 | report_error(Else, Ref, Owner), 399 | exit(Else) 400 | after 5000 -> 401 | erlang:hibernate(?MODULE, maybe_continue_decoding, [ViewSt]) 402 | end; 403 | 404 | maybe_continue_decoding(#viewst{parent=Parent, 405 | owner=Owner, 406 | ref=Ref, 407 | mref=MRef, 408 | client_ref=ClientRef}=ViewSt) -> 409 | receive 410 | {'DOWN', MRef, _, _, _} -> 411 | %% parent exited there is no need to continue 412 | exit(normal); 413 | {Ref, cancel} -> 414 | hackney:close(ClientRef), 415 | Owner ! {Ref, ok}, 416 | exit(normal); 417 | {Ref, pause} -> 418 | erlang:hibernate(?MODULE, maybe_continue_decoding, [ViewSt]); 419 | {Ref, resume} -> 420 | {wait_rows1, 0, [[]], ViewSt}; 421 | {system, From, Request} -> 422 | sys:handle_system_msg(Request, From, Parent, ?MODULE, [], 423 | {maybe_continue_decoding, ViewSt}); 424 | Else -> 425 | error_logger:error_msg("Unexpected message: ~w~n", [Else]), 426 | report_error(Else, Ref, Owner), 427 | exit(Else) 428 | after 0 -> 429 | {wait_rows1, 0, [[]], ViewSt} 430 | end. 431 | 432 | report_error({error, _What}=Error, Ref, Pid) -> 433 | Pid ! {Ref, Error}; 434 | report_error(What, Ref, Pid) -> 435 | Pid ! {Ref, {error, What}}. 436 | -------------------------------------------------------------------------------- /src/couchbeam_view_sup.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | -module(couchbeam_view_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API. 11 | -export([start_link/0]). 12 | 13 | %% supervisor. 14 | -export([init/1]). 15 | 16 | -define(SUPERVISOR, ?MODULE). 17 | 18 | %% API. 19 | 20 | -spec start_link() -> {ok, pid()}. 21 | start_link() -> 22 | supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). 23 | 24 | %% supervisor. 25 | 26 | init([]) -> 27 | 28 | %% start table to keep async streams ref 29 | ets:new(couchbeam_view_streams, [set, public, named_table]), 30 | 31 | %% define a stream spec 32 | Stream = {couchbeam_view_stream, {couchbeam_view_stream, start_link, []}, 33 | temporary, infinity, worker, [couchbeam_view_stream]}, 34 | 35 | {ok, {{simple_one_for_one, 10, 3600}, [Stream]}}. 36 | -------------------------------------------------------------------------------- /src/gen_changes.erl: -------------------------------------------------------------------------------- 1 | %%% -*- erlang -*- 2 | %%% 3 | %%% This file is part of couchbeam released under the MIT license. 4 | %%% See the NOTICE for more information. 5 | 6 | %% @doc gen_changes CouchDB continuous changes consumer behavior 7 | %% This behaviour allows you to create easily a server that consume 8 | %% Couchdb continuous changes 9 | 10 | -module(gen_changes). 11 | 12 | -include("couchbeam.hrl"). 13 | 14 | -behavior(gen_server). 15 | 16 | -export([start_link/4]). 17 | -export([init/1, 18 | handle_call/3, 19 | handle_cast/2, 20 | handle_info/2, 21 | terminate/2, 22 | code_change/3]). 23 | -export([behaviour_info/1]). 24 | 25 | -export([call/2, 26 | call/3, 27 | cast/2]). 28 | 29 | -export([stop/1, get_seq/1]). 30 | 31 | 32 | behaviour_info(callbacks) -> 33 | [{init, 1}, 34 | {handle_change, 2}, 35 | {handle_call, 3}, 36 | {handle_cast, 2}, 37 | {handle_info, 2}, 38 | {terminate, 2}]; 39 | behaviour_info(_) -> 40 | undefined. 41 | 42 | call(Name, Request) -> 43 | gen_server:call(Name, Request). 44 | 45 | call(Name, Request, Timeout) -> 46 | gen_server:call(Name, Request, Timeout). 47 | 48 | cast(Dest, Request) -> 49 | gen_server:cast(Dest, Request). 50 | 51 | %% @doc create a gen_changes process as part of a supervision tree. 52 | %% The function should be called, directly or indirectly, by the supervisor. 53 | %% @spec start_link(Module, Db::db(), Options::changesoptions(), 54 | %% InitArgs::list()) -> term() 55 | %% changesoptions() = [changeoption()] 56 | %% changeoption() = {include_docs, string()} | 57 | %% {filter, string()} | 58 | %% {since, integer()|string()} | 59 | %% {heartbeat, string()|boolean()} 60 | start_link(Module, Db, Options, InitArgs) -> 61 | gen_server:start_link(?MODULE, [Module, Db, Options, InitArgs], []). 62 | 63 | init([Module, Db, Options, InitArgs]) -> 64 | case Module:init(InitArgs) of 65 | {ok, ModState} -> 66 | case couchbeam_changes:follow(Db, Options) of 67 | {ok, StreamRef} -> 68 | LastSeq = proplists:get_value(since, Options, 0), 69 | {ok, #gen_changes_state{stream_ref=StreamRef, 70 | mod=Module, 71 | modstate=ModState, 72 | db=Db, 73 | options=Options, 74 | last_seq=LastSeq}}; 75 | {error, Error} -> 76 | Module:terminate(Error, ModState), 77 | {stop, Error} 78 | end; 79 | Error -> 80 | Error 81 | end. 82 | 83 | stop(Pid) when is_pid(Pid) -> 84 | gen_server:cast(Pid, stop). 85 | 86 | get_seq(Pid) when is_pid(Pid) -> 87 | gen_server:call(Pid, get_seq). 88 | 89 | handle_call(get_seq, _From, State=#gen_changes_state{last_seq=Seq}) -> 90 | {reply, Seq, State}; 91 | handle_call(Request, From, 92 | State=#gen_changes_state{mod=Module, modstate=ModState}) -> 93 | case Module:handle_call(Request, From, ModState) of 94 | {reply, Reply, NewModState} -> 95 | {reply, Reply, State#gen_changes_state{modstate=NewModState}}; 96 | {reply, Reply, NewModState, A} 97 | when A =:= hibernate orelse is_number(A) -> 98 | {reply, Reply, State#gen_changes_state{modstate=NewModState}, A}; 99 | {noreply, NewModState} -> 100 | {noreply, State#gen_changes_state{modstate=NewModState}}; 101 | {noreply, NewModState, A} when A =:= hibernate orelse is_number(A) -> 102 | {noreply, State#gen_changes_state{modstate=NewModState}, A}; 103 | {stop, Reason, NewModState} -> 104 | {stop, Reason, State#gen_changes_state{modstate=NewModState}}; 105 | {stop, Reason, Reply, NewModState} -> 106 | {stop, Reason, Reply, State#gen_changes_state{modstate=NewModState}} 107 | end. 108 | 109 | handle_cast(stop, State) -> 110 | {stop, normal, State}; 111 | handle_cast(Msg, State=#gen_changes_state{mod=Module, modstate=ModState}) -> 112 | case Module:handle_cast(Msg, ModState) of 113 | {noreply, NewModState} -> 114 | {noreply, State#gen_changes_state{modstate=NewModState}}; 115 | {noreply, NewModState, A} when A =:= hibernate orelse is_number(A) -> 116 | {noreply, State#gen_changes_state{modstate=NewModState}, A}; 117 | {stop, Reason, NewModState} -> 118 | {stop, Reason, State#gen_changes_state{modstate=NewModState}} 119 | end. 120 | 121 | 122 | handle_info({Ref, Msg}, 123 | State=#gen_changes_state{mod=Module, modstate=ModState, 124 | stream_ref=Ref}) -> 125 | 126 | State2 = case Msg of 127 | {done, LastSeq} -> 128 | State#gen_changes_state{last_seq=LastSeq}; 129 | {change, Change} -> 130 | Seq = couchbeam_doc:get_value(<<"seq">>, Change), 131 | State#gen_changes_state{last_seq=Seq} 132 | end, 133 | 134 | try Module:handle_change(Msg, ModState) of 135 | {noreply, NewModState} -> 136 | {noreply, State2#gen_changes_state{modstate=NewModState}}; 137 | {noreply, NewModState, A} when A =:= hibernate orelse is_number(A) -> 138 | {noreply, State2#gen_changes_state{modstate=NewModState}, A}; 139 | {stop, Reason, NewModState} -> 140 | {stop, Reason, State2#gen_changes_state{modstate=NewModState}}; 141 | Other -> 142 | error_logger:error_msg("~p:handle_change/2 returned unexpected value: ~p~n", [Module, Other]), 143 | {stop, {bad_return_value, Other}, State2} 144 | catch 145 | Class:Reason:Stacktrace -> 146 | error_logger:error_msg("~p:handle_change/2 crashed: ~p:~p~n~p~n", [Module, Class, Reason, Stacktrace]), 147 | {stop, {handle_change_crashed, {Class, Reason}}, State2} 148 | end; 149 | 150 | 151 | handle_info({Ref, {error, Error}}, 152 | State=#gen_changes_state{stream_ref=Ref, last_seq=LastSeq}) -> 153 | handle_info({error, [Error, {last_seq, LastSeq}]}, State); 154 | 155 | handle_info(Info, State=#gen_changes_state{mod=Module, modstate=ModState}) -> 156 | case Module:handle_info(Info, ModState) of 157 | {noreply, NewModState} -> 158 | {noreply, State#gen_changes_state{modstate=NewModState}}; 159 | {noreply, NewModState, A} when A =:= hibernate orelse is_number(A) -> 160 | {noreply, State#gen_changes_state{modstate=NewModState}, A}; 161 | {stop, Reason, NewModState} -> 162 | {stop, Reason, State#gen_changes_state{modstate=NewModState}} 163 | end. 164 | 165 | code_change(_OldVersion, State, _Extra) -> 166 | %% TODO: support code changes? 167 | {ok, State}. 168 | 169 | terminate(Reason, #gen_changes_state{stream_ref=Ref, 170 | mod=Module, modstate=ModState}) -> 171 | Module:terminate(Reason, ModState), 172 | couchbeam_changes:cancel_stream(Ref), 173 | ok. 174 | -------------------------------------------------------------------------------- /support/1M: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/couchbeam/1942999129a878ed8ea908aada32ae54c4bdcc9f/support/1M --------------------------------------------------------------------------------