├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .mocharc.json ├── .npmignore ├── .travis.yml ├── CHANGES.md ├── CONTRIBUTING.md ├── DCO1.1.txt ├── Jenkinsfile ├── LICENSE.md ├── MIGRATION.md ├── README.md ├── api-migration.md ├── cloudant.js ├── example ├── README.md ├── crud.js └── package.json ├── lib ├── client.js ├── clientutils.js ├── eventrelay.js ├── passthroughduplex.js ├── reconfigure.js └── tokens │ ├── CookieTokenManager.js │ ├── IamTokenManager.js │ └── TokenManager.js ├── package-lock.json ├── package.json ├── plugins ├── base.js ├── cookieauth.js ├── iamauth.js └── retry.js ├── soak_tests ├── README.md ├── animaldb_bulk_docs.json ├── bulk_docs.json ├── doc.json └── soak.js ├── test ├── client.js ├── clientutils.js ├── cloudant.js ├── doc_validation.js ├── eventrelay.js ├── fixtures │ ├── all_docs_include_docs.json │ ├── bulk_docs.json │ ├── testplugin.js │ └── testplugins.js ├── issues │ └── 292.js ├── legacy │ ├── api.js │ ├── plugin.js │ ├── readme-examples.js │ ├── reconfigure.js │ ├── y.css │ └── y.css.gz ├── nock.js ├── partitioned_databases.js ├── passthroughduplex.js ├── plugins │ ├── cookieauth.js │ ├── iamauth.js │ └── retry.js ├── readmeexamples.js ├── stream.js ├── tokens │ └── TokenManager.js └── typescript │ └── cloudant.ts └── types ├── index.d.ts ├── tests.ts ├── tsconfig.json └── tslint.json /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | coverage/ 4 | docs/ 5 | jsdoc/ 6 | node_modules/ 7 | templates/ 8 | test/legacy/ 9 | test/typescript/ 10 | tmp/ 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "script" 4 | }, 5 | "extends": "semistandard", 6 | "plugins": [ 7 | "standard", 8 | "promise", 9 | "header" 10 | ], 11 | "rules": { 12 | "space-before-function-paren": ["error", "never"], 13 | "handle-callback-err": "off", 14 | "strict": ["error", "global"], 15 | "header/header": [2, "line", [ 16 | {"pattern": "^\\ Copyright © 20\\d\\d(?:, 20\\d\\d)? IBM Corp\\. All rights reserved\\.$"}, 17 | "", 18 | " Licensed under the Apache License, Version 2.0 (the \"License\");", 19 | " you may not use this file except in compliance with the License.", 20 | " You may obtain a copy of the License at", 21 | "", 22 | " http://www.apache.org/licenses/LICENSE-2.0", 23 | "", 24 | " Unless required by applicable law or agreed to in writing, software", 25 | " distributed under the License is distributed on an \"AS IS\" BASIS,", 26 | " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.", 27 | " See the License for the specific language governing permissions and", 28 | " limitations under the License." 29 | ] 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please [read these guidelines](http://ibm.biz/cdt-issue-guide) before opening an issue. 2 | 3 | 4 | 5 | ## Bug Description 6 | 7 | ### 1. Steps to reproduce and the simplest code sample possible to demonstrate the issue 8 | 12 | 13 | ### 2. What you expected to happen 14 | 15 | ### 3. What actually happened 16 | 17 | ## Environment details 18 | 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | ## Checklist 5 | 6 | - [ ] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](../blob/master/DCO1.1.txt) 7 | - [ ] Added tests for code changes _or_ test/build only changes 8 | - [ ] Updated the change log file (`CHANGES.md`|`CHANGELOG.md`) _or_ test/build only changes 9 | - [ ] Completed the PR template below: 10 | 11 | ## Description 12 | 29 | 30 | ## Approach 31 | 32 | 38 | 39 | ## Schema & API Changes 40 | 41 | 52 | 53 | ## Security and Privacy 54 | 55 | 66 | 67 | ## Testing 68 | 69 | 92 | 93 | ## Monitoring and Logging 94 | 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *tmp.* 3 | *.log 4 | node_modules/ 5 | coverage/ 6 | /.env 7 | test/typescript/cloudant.js 8 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": "test/typescript/*.js", 3 | "recursive": true, 4 | "timeout": 60000 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package 2 | 3 | # build tool config 4 | .travis.yml 5 | Jenkinsfile 6 | types/tsconfig.json 7 | 8 | # code examples 9 | example* 10 | 11 | # docs 12 | CONTRIBUTING.md 13 | DCO1.1.txt 14 | 15 | # github config 16 | .github 17 | 18 | # IDEs 19 | .idea 20 | .vscode 21 | 22 | # linters 23 | .eslintrc* 24 | .eslintignore 25 | types/tslint.json 26 | 27 | # npm 28 | .npmignore 29 | 30 | # tests 31 | .env 32 | citest 33 | test* 34 | toxytests 35 | soak_tests 36 | 37 | # test logs 38 | *.log 39 | *.dump 40 | logs 41 | out 42 | tmp 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: "node_js" 2 | branches: 3 | only: 4 | - master 5 | - next 6 | - rewrite 7 | node_js: 8 | - "16" 9 | - "14" 10 | - "12" 11 | os: 12 | - linux 13 | before_install: 14 | - npm update -g npm 15 | install: npm install 16 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # UNRELEASED 2 | - [DEPRECATED] This library is end-of-life and no longer supported. 3 | 4 | # 4.5.1 (2021-09-16) 5 | - [FIXED] Issue where new session cookies from pre-emptive renewal would not persist beyond the original session 6 | lifetime. 7 | 8 | # 4.5.0 (2021-08-26) 9 | - [IMPROVED] - Document IDs and attachment names are now rejected if they could cause an unexpected 10 | Cloudant request. We have seen that some applications pass unsantized document IDs to SDK functions 11 | (e.g. direct from user requests). In response to this we have updated many functions to reject 12 | obviously invalid paths. However, for complete safety applications must still validate that 13 | document IDs and attachment names match expected patterns. 14 | 15 | # 4.4.1 (2021-08-02) 16 | - [FIXED] Hang caused by plugins (i.e. retry plugin) preventing callback execution 17 | by attempting to retry on errors received after starting to return the response body. 18 | - [DEPRECATED] This library is now deprecated and will be EOL on Dec 31 2021. 19 | 20 | # 4.4.0 (2021-06-18) 21 | - [FIXED] Parsing of max-age from Set-Cookie headers. 22 | - [FIXED] Double callback if plugin errors when updating state. 23 | - [NOTE] Updated engines to remove EOL Node.js versions (minimum is now Node.js 12). 24 | 25 | # 4.3.1 (2021-03-17) 26 | - [NEW] Add migration guide to the newly supported cloudant-node-sdk 27 | (package: @ibm-cloud/cloudant). 28 | - [FIXED] Update README to prefer url to account. 29 | 30 | # 4.3.0 (2020-08-17) 31 | - [FIXED] Stopped constant token auto renewal in the absence of a cookie 32 | header Max-Age. 33 | - [UPGRADED] Upgrade package: @types/request@^2.48.4. 34 | - [UPGRADED] Upgrade package: nano@~8.2.2. 35 | 36 | # 4.2.4 (2020-03-02) 37 | - [FIXED] Pinned Nano to version 8.1 to resolve issue with extending upstream 38 | TypeScript changes in Nano version 8.2.0. 39 | 40 | # 4.2.3 (2019-12-05) 41 | - [FIXED] Expose BasePlugin. 42 | - [FIXED] Prevent double encoding of credentials passed in URL user information 43 | when using the `cookieauth` plugin. 44 | - [IMPROVED] Documented the characters that are required to be encoded in URL 45 | user information. 46 | - [IMPROVED] Documented the legacy compatibility behaviour that always adds the 47 | `cookieauth` plugin when using the initialization callback functionality. 48 | 49 | # 4.2.2 (2019-10-23) 50 | - [FIXED] Stopped disabling the IAM auth plugin after failed IAM 51 | authentications. Subsequent requests will re-request authorization, 52 | potentially failing again if the original authentication failure was not 53 | temporary. 54 | - [FIXED] Ensure IAM API key can be correctly changed. 55 | - [FIXED] Callback with an error when a user cannot be authenticated using IAM. 56 | - [FIXED] Ensure authorization tokens are not unnecessarily requested. 57 | - [IMPROVED] Do not apply cookie authentication by default in the case that no 58 | credentials are provided. 59 | - [IMPROVED] Preemptively renew authorization tokens that are due to expire. 60 | 61 | # 4.2.1 (2019-08-29) 62 | - [FIXED] Include all built-in plugin modules in webpack bundle. 63 | 64 | # 4.2.0 (2019-08-27) 65 | - [NEW] Added option to set new IAM API key. 66 | - [FIXED] Allow plugins to be loaded from outside the 'plugins/' directory. 67 | - [FIXED] Retry bad IAM token requests. 68 | 69 | # 4.1.1 (2019-06-17) 70 | - [FIXED] Remove unnecessary `npm-cli-login` dependency. 71 | 72 | # 4.1.0 (2019-05-14) 73 | - [NEW] Added partitioned database support. 74 | 75 | # 4.0.0 (2019-03-12) 76 | - [NEW] Added option for client to authenticate with IAM token server. 77 | - [FIXED] Add `vcapServiceName` configuration option to TS declarations. 78 | - [FIXED] Case where `.resume()` was called on an undefined response. 79 | - [FIXED] Updated the default IAM token server URL. 80 | - [BREAKING CHANGE] Nano 7 accepted a callback to the `*AsStream` functions, but 81 | the correct behaviour when using the `request` object from `*AsStream` 82 | functions is to use event handlers. Users of the `*AsStream` functions should 83 | ensure they are using event handlers not callbacks before moving to this 84 | version. 85 | - [UPGRADED] Apache CouchDB Nano to a minimum of version 8 for `*AsStream` 86 | function fixes. 87 | 88 | # 3.0.2 (2019-01-07) 89 | - [FIXED] Remove unnecessary `@types/nano` dependancy. 90 | 91 | # 3.0.1 (2018-11-22) 92 | - [FIXED] Use named import for `request.CoreOptions` type. 93 | 94 | # 3.0.0 (2018-11-20) 95 | - [FIXED] Expose `BasePlugin` type in Cloudant client. 96 | - [FIXED] Set `parseUrl = false` on underlying Nano instance. 97 | - [BREAKING CHANGE] Due to the Nano 7.x upgrade all return types are now a 98 | `Promise` (except for the `...AsStream` functions). _See 99 | [api-migration.md](https://github.com/cloudant/nodejs-cloudant/blob/master/api-migration.md#2x--3x) 100 | for migration details._ 101 | - [REMOVED] Remove nodejs-cloudant TypeScript type definitions for 102 | `db.search`. These definitions are now imported directly from Nano. 103 | - [REMOVED] Removed support for the deprecated Cloudant virtual hosts feature. 104 | - [UPGRADED] Using nano==7.1.1 dependency. 105 | 106 | # 2.4.1 (2018-11-12) 107 | - [FIXED] Don't override `plugins` array when instantiating a new client using VCAP. 108 | 109 | # 2.4.0 (2018-09-19) 110 | - [FIXED] Case where `username` and `password` options were not used if a `url` was supplied. 111 | - [FIXED] Case where `vcapServices` was supplied with a basic-auth `url`. 112 | 113 | # 2.3.0 (2018-06-08) 114 | - [FIXED] Removed addition of `statusCode` to response objects returned by promises. 115 | - [IMPROVED] Added support for IAM API key when initializing client with VCAP_SERVICES environment variable. 116 | 117 | # 2.2.0 (2018-04-30) 118 | - [FIXED] Add missing `maxAttempt` parameter to TypeScript definition. 119 | - [FIXED] Include client initialization callback in TypeScript definition. 120 | - [FIXED] Prevent client executing done callback multiple times. 121 | - [FIXED] Removed test and lint data that bloated npm package size. 122 | - [FIXED] Support Cloudant query when using promises request plugin. 123 | 124 | # 2.1.0 (2018-03-05) 125 | - [NEW] Add TypeScript definitions. 126 | - [NEW] Allow pipes to be defined inside request event handlers. 127 | - [UPGRADED] Using nano==6.4.3 dependancy. 128 | - [NOTE] Update engines in preparation for Node.js 4 “Argon” end-of-life. 129 | 130 | # 2.0.2 (2018-02-14) 131 | - [FIXED] Updated `require` references to use newly scoped package 132 | `@cloudant/cloudant`. 133 | 134 | # 2.0.1 (2018-02-14) 135 | - [NEW] Added API for upcoming IBM Cloud Identity and Access Management support 136 | for Cloudant on IBM Cloud. Note: IAM API key support is not yet enabled in the 137 | service. 138 | - [NEW] Support multiple plugins. 139 | _See [api-migration.md](https://github.com/cloudant/nodejs-cloudant/blob/master/api-migration.md) 140 | for migration details._ 141 | - [NEW] Allow use of a custom service name from the CloudFoundry VCAP_SERVICES 142 | environment variable. 143 | - [FIXED] Fix `get_security`/`set_security` asymmetry. 144 | - [FIXED] Support piping of request payload with plugins. 145 | - [BREAKING CHANGE] Replace `retryAttempts` option with `maxAttempts`. This 146 | defines the maximum number of times the request will be attempted. 147 | - [BREAKING CHANGE] By default the `retry` plugin will retry requests on HTTP 148 | 429 status codes, a subset of 5xx server error status codes and also TCP/IP 149 | errors. 150 | _See [api-migration.md](https://github.com/cloudant/nodejs-cloudant/blob/master/api-migration.md) 151 | for migration details._ 152 | - [BREAKING CHANGE] Changed `promise` plugin to throw new `CloudantError` (not 153 | `string`). 154 | - [REMOVED] Remove global `retryTimeout` option (replaced by plugin specific 155 | configuration). 156 | - [REMOVED] Remove previously deprecated method `set_permissions`. 157 | - [IMPROVED] Updated documentation by replacing deprecated Cloudant links with 158 | the latest bluemix.net links. 159 | 160 | # 1.10.0 (2017-11-01) 161 | - [UPGRADED] Upgrade package: cloudant-nano@6.7.0. 162 | 163 | # 1.9.0 (2017-10-20) 164 | - [NEW] Add 'error' & 'response' events to 429 retry plugin stream. 165 | - [FIXED] `{silent: true}` to dotenv to prevent `.env` warnings. 166 | - [UPGRADED] Upgrade package: cloudant-nano@6.6.0. 167 | - [UPGRADED] Upgrade package: debug@^3.1.0. 168 | - [UPGRADED] Upgrade package: request@^2.81.0. 169 | 170 | # 1.8.0 (2017-05-23) 171 | - [UPGRADED] Using cloudant-nano==6.5.0 dependancy. 172 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | Please [read these guidelines](http://ibm.biz/cdt-issue-guide) before opening an issue. 6 | If you still need to open an issue then we ask that you complete the template as 7 | fully as possible. 8 | 9 | ## Pull requests 10 | 11 | We welcome pull requests, but ask contributors to keep in mind the following: 12 | 13 | * Only PRs with the template completed will be accepted 14 | * We will not accept PRs for user specific functionality 15 | 16 | ### Developer Certificate of Origin 17 | 18 | In order for us to accept pull-requests, the contributor must sign-off a 19 | [Developer Certificate of Origin (DCO)](DCO1.1.txt). This clarifies the 20 | intellectual property license granted with any contribution. It is for your 21 | protection as a Contributor as well as the protection of IBM and its customers; 22 | it does not change your rights to use your own Contributions for any other purpose. 23 | 24 | Please read the agreement and acknowledge it by ticking the appropriate box in the PR 25 | text, for example: 26 | 27 | - [x] Tick to sign-off your agreement to the Developer Certificate of Origin (DCO) 1.1 28 | 29 | ## General information 30 | 31 | ## Requirements 32 | 33 | Node.js and npm, other dependencies will be installed automatically via `npm` 34 | and the `package.json` `dependencies` and `devDependencies`. 35 | 36 | ## Testing 37 | 38 | To run tests: 39 | 40 | ```sh 41 | npm test 42 | ``` 43 | 44 | To run tests with a real, instead of mock, server then use the environment 45 | variable `NOCK_OFF=true`. 46 | 47 | You can add verbose debug messages while running tests by doing: 48 | 49 | ``` 50 | DEBUG=* npm test 51 | ``` 52 | 53 | ### Test configuration 54 | 55 | When testing with a real server (i.e. `NOCK_OFF=true`) these options are 56 | available to set as environment variables: 57 | `cloudant_username` - username 58 | `cloudant_password` - password 59 | `SERVER_URL` - the URL to use (defaults to `https://$cloudant_user.cloudant.com`) 60 | -------------------------------------------------------------------------------- /DCO1.1.txt: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | // Copyright © 2017, 2019 IBM Corp. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | def getEnvForSuite(suiteName) { 17 | // Base environment variables 18 | def envVars = [ 19 | "NVM_DIR=${env.HOME}/.nvm" 20 | ] 21 | 22 | // Add test suite specific environment variables 23 | switch(suiteName) { 24 | case 'test': 25 | envVars.add("NOCK_OFF=true") 26 | envVars.add("SERVER_URL=${env.SDKS_TEST_SERVER_URL}") 27 | envVars.add("cloudant_iam_token_server=${env.SDKS_TEST_IAM_SERVER}") 28 | break 29 | case 'nock-test': 30 | break 31 | default: 32 | error("Unknown test suite environment ${suiteName}") 33 | } 34 | 35 | return envVars 36 | } 37 | 38 | def installAndTest(version, testSuite) { 39 | try { 40 | // Actions: 41 | // 1. Load NVM 42 | // 2. Install/use required Node.js version 43 | // 3. Install mocha-jenkins-reporter so that we can get junit style output 44 | // 4. Run tests 45 | sh """ 46 | [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" 47 | nvm install ${version} 48 | nvm use ${version} 49 | npm install mocha-jenkins-reporter --save-dev 50 | ./node_modules/mocha/bin/mocha --reporter mocha-jenkins-reporter --reporter-options junit_report_path=./${testSuite}/test-results.xml,junit_report_stack=true,junit_report_name=${testSuite} test --grep 'Virtual Hosts' --invert 51 | """ 52 | } finally { 53 | junit '**/test-results.xml' 54 | } 55 | } 56 | 57 | def setupNodeAndTest(version, testSuite='test') { 58 | node { 59 | // Install NVM 60 | sh 'wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash' 61 | // Unstash the built content 62 | unstash name: 'built' 63 | 64 | // Run tests using creds 65 | if(testSuite == 'test') { 66 | withCredentials([usernamePassword(credentialsId: 'testServerLegacy', usernameVariable: 'cloudant_username', passwordVariable: 'cloudant_password'), string(credentialsId: 'testServerIamApiKey', variable: 'cloudant_iam_api_key')]) { 67 | withEnv(getEnvForSuite("${testSuite}")) { 68 | installAndTest(version, testSuite) 69 | } 70 | } 71 | } else { 72 | withEnv(getEnvForSuite("${testSuite}")) { 73 | installAndTest(version, testSuite) 74 | } 75 | } 76 | } 77 | } 78 | 79 | stage('Build') { 80 | // Checkout, build 81 | node { 82 | checkout scm 83 | sh 'npm install' 84 | stash name: 'built' 85 | } 86 | } 87 | 88 | stage('QA') { 89 | parallel([ 90 | Node12x : { 91 | // 12.x LTS 92 | setupNodeAndTest('12') 93 | }, 94 | Node12xWithNock : { 95 | setupNodeAndTest('12', 'nock-test') 96 | }, 97 | Node14x : { 98 | // 14.x LTS 99 | setupNodeAndTest('14') 100 | }, 101 | Node : { 102 | // 16.x 103 | setupNodeAndTest('16') 104 | }, 105 | NodeWithNock : { 106 | // 16.x 107 | setupNodeAndTest('16', 'nock-test') 108 | }, 109 | ]) 110 | } 111 | 112 | // Publish the master branch 113 | stage('Publish') { 114 | if (env.BRANCH_NAME == "master") { 115 | node { 116 | unstash 'built' 117 | 118 | def v = com.ibm.cloudant.integrations.VersionHelper.readVersion(this, 'package.json') 119 | String version = v.version 120 | boolean isReleaseVersion = v.isReleaseVersion 121 | 122 | // Upload using the NPM creds 123 | withCredentials([string(credentialsId: 'npm-mail', variable: 'NPM_EMAIL'), 124 | usernamePassword(credentialsId: 'npm-creds', passwordVariable: 'NPM_TOKEN', usernameVariable: 'NPM_USER')]) { 125 | // Actions: 126 | // 1. create .npmrc file for publishing 127 | // 2. add the build ID to any snapshot version for uniqueness 128 | // 3. publish the build to NPM adding a snapshot tag if pre-release 129 | sh """ 130 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc 131 | ${isReleaseVersion ? '' : ('npm version --no-git-tag-version ' + version + '.' + env.BUILD_ID)} 132 | npm publish ${isReleaseVersion ? '' : '--tag snapshot'} 133 | """ 134 | } 135 | } 136 | } 137 | 138 | // Run the gitTagAndPublish which tags/publishes to github for release builds 139 | gitTagAndPublish { 140 | versionFile='package.json' 141 | releaseApiUrl='https://api.github.com/repos/cloudant/nodejs-cloudant/releases' 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migrating to the `cloudant-node-sdk` library 2 | This document is to assist in migrating from the `nodejs-cloudant` (package: `@cloudant/cloudant`) to the newly supported [`cloudant-node-sdk`](https://github.com/IBM/cloudant-node-sdk) (package: `@ibm-cloud/cloudant`) that compatible with JavaScript and TypeScript. 3 | 4 | ## Initializing the client connection 5 | There are several ways to create a client connection in `cloudant-node-sdk`: 6 | 1. [Environment variables](https://github.com/IBM/cloudant-node-sdk#authentication-with-environment-variables) 7 | 2. [External configuration file](https://github.com/IBM/cloudant-node-sdk#authentication-with-external-configuration) 8 | 3. [Programmatically](https://github.com/IBM/cloudant-node-sdk#programmatic-authentication) 9 | 10 | [See the README](https://github.com/IBM/cloudant-node-sdk#code-examples) for code examples on using environment variables. 11 | 12 | ## Other differences 13 | 1. Using the `dotenv` package to store credentials in a file is not recommended. See the [external file configuration section](https://github.com/IBM/cloudant-node-sdk#authentication-with-external-configuration) in our API docs for handling this feature in our new library. 14 | 1. In `cloudant-node-sdk` all operations are performed from the scope of the client instance and 15 | not associated with any sub-scope like the database. There is no need to instantiate a 16 | database object to interact with documents - the database name is included as part of 17 | document operations. For example, in the case of updating a document you would first call 18 | `getDocument({ db: dbName, docId: docId})` to fetch and then `putDocument({ 19 | db: dbName, docId: docId})` to update. As a result of which the `use` 20 | operation also became obsoleted. 21 | 1. Plugins are not supported, but several of the plugin features exist in the new library e.g. IAM, [automatic retries](https://github.com/IBM/ibm-cloud-sdk-common/#automatic-retries) for failed requests. 22 | 1. Error handling is not transferable from `@cloudant/cloudant` to `@ibm-cloud/cloudant`. For more information go to the [Error handling section](https://cloud.ibm.com/apidocs/cloudant?code=node#error-handling) in our API docs. 23 | 1. Custom HTTP client configurations in `@cloudant/cloudant` can be set differently in 24 | `@ibm-cloud/cloudant`. For more information go to the 25 | [Configuring the HTTP client section](https://github.com/IBM/ibm-cloud-sdk-common/#configuring-the-http-client) 26 | in the IBM Cloud SDK Common README. 27 | 28 | ### Troubleshooting 29 | 1. Authentication errors occur during service instantiation. For example, the code `const 30 | service = CloudantV1.newInstance({ serviceName: 'EXAMPLE' });` will fail with `` At least one 31 | of `iamProfileName` or `iamProfileId` must be specified. `` if required environment variables 32 | prefixed with `EXAMPLE` are not set. 33 | 1. Server errors occur when running a request against the service. We suggest to 34 | check server errors with 35 | [`getServerInformation`](https://cloud.ibm.com/apidocs/cloudant?code=node#getserverinformation) 36 | which is the new alternative of `ping`. 37 | 38 | ## Request mapping 39 | Here's a list of the top 5 most frequently used `nodejs-cloudant` operations and the `cloudant-node-sdk` equivalent API operation documentation link: 40 | 41 | | `nodejs-cloudant` operation | `cloudant-node-sdk` API operation documentation link | 42 | |-----------------------------|---------------------------------| 43 | |`db.get()` |[`getDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#getdocument)| 44 | |`db.view()` |[`postView`](https://cloud.ibm.com/apidocs/cloudant?code=node#postview)| 45 | |`db.find()` |[`postFind`](https://cloud.ibm.com/apidocs/cloudant?code=node#postfind)| 46 | |`db.head()` |[`headDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#headdocument)| 47 | |`db.insert()` |[`putDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#putdocument)| 48 | 49 | [A table](#reference-table) with the whole list of operations is provided at the end of this guide. 50 | 51 | The `cloudant-node-sdk` library is generated from a more complete API spec and provides a significant number of operations that do not exist in `nodejs-cloudant`. See [the IBM Cloud API Documentation](https://cloud.ibm.com/apidocs/cloudant) to review request parameter and body options, code examples, and additional details for every endpoint. 52 | 53 | ## Known Issues 54 | There's an [outline of known issues](https://github.com/IBM/cloudant-node-sdk/blob/master/KNOWN_ISSUES.md) in the `cloudant-node-sdk` repository. 55 | 56 | ## Reference table 57 | The table below contains a list of `nodejs-cloudant` functions and the `cloudant-node-sdk` equivalent API operation documentation link. The `cloudant-node-sdk` operation documentation link will contain the new function in a code sample e.g. `getServerInformation` link will contain a code example with `getServerInformation()`. 58 | 59 | **Note:** There are many API operations included in the new `cloudant-node-sdk` that are not available in the `nodejs-cloudant` library. The [API documentation](https://cloud.ibm.com/apidocs/cloudant?code=node) contains the full list of operations. 60 | 61 | |nodejs-cloudant function | cloudant-node-sdk function reference | 62 | |-------------------------|--------------------------------------| 63 | |`ping()`|[`getServerInformation`](https://cloud.ibm.com/apidocs/cloudant?code=node#getserverinformation)| 64 | |`listDbs()/listDbsAsStream()`|[`getAllDbs`](https://cloud.ibm.com/apidocs/cloudant?code=node#getalldbs)| 65 | |`updates()/followUpdates()`|[`getDbUpdates`](https://cloud.ibm.com/apidocs/cloudant?code=node#getdbupdates)| 66 | |`replicate()`/`replicateDb()`/`enableReplication()`/`replication.enable()`|[`putReplicationDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#putreplicationdocument)| 67 | |`queryReplication()`/`replication.query()`|[`getSchedulerDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#getschedulerdocument)| 68 | |`disableReplication()`/`replication.delete()`|[`deleteReplicationDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#deletereplicationdocument)| 69 | |`session()`|[`getSessionInformation`](https://cloud.ibm.com/apidocs/cloudant?code=node#getsessioninformation)| 70 | |`uuids()`|[`getUuids`](https://cloud.ibm.com/apidocs/cloudant?code=node#getuuids)| 71 | |`db.destroy()`|[`deleteDatabase`](https://cloud.ibm.com/apidocs/cloudant?code=node#deletedatabase)| 72 | |`db.info()`|[`getDatabaseInformation`](https://cloud.ibm.com/apidocs/cloudant?code=node#getdatabaseinformation)| 73 | |`db.insert()`|[`postDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#postdocument)| 74 | |`db.create(db_name)`|[`putDatabase`](https://cloud.ibm.com/apidocs/cloudant?code=node#putdatabase)| 75 | |`db.fetch()/db.list()/db.listAsStream()`|[`postAllDocs`, `postAllDocsAsStream`](https://cloud.ibm.com/apidocs/cloudant?code=node#postalldocs)| 76 | |`db.bulk()`|[`postBulkDocs`](https://cloud.ibm.com/apidocs/cloudant?code=node#postbulkdocs)| 77 | |`db.bulk_get()`|[`postBulkGet`](https://cloud.ibm.com/apidocs/cloudant?code=node#postbulkget)| 78 | |`db.changes()`|[`postChanges`](https://cloud.ibm.com/apidocs/cloudant?code=node#postchanges-databases)| 79 | |`db.destroy() with _design path`|[`deleteDesignDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#deletedesigndocument)| 80 | |`db.get() with _design path`|[`getDesignDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#getdesigndocument)| 81 | |`db.insert() with _design path`|[`putDesignDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#putdesigndocument)| 82 | |`db.search()/db.searchAsStream()`|[`postSearch`, `postSearchAsStream`](https://cloud.ibm.com/apidocs/cloudant?code=node#postsearch)| 83 | |`db.view()`|[`postView`](https://cloud.ibm.com/apidocs/cloudant?code=node#postview)| 84 | |`db.list() (only design documents)`|[`postDesignDocs`](https://cloud.ibm.com/apidocs/cloudant?code=node#postdesigndocs)| 85 | |`db.find()`|[`postFind`](https://cloud.ibm.com/apidocs/cloudant?code=node#postfind)| 86 | |`db.createIndex()`|[`postIndex`](https://cloud.ibm.com/apidocs/cloudant?code=node#postindex)| 87 | |`db.index.del()`|[`deleteIndex`](https://cloud.ibm.com/apidocs/cloudant?code=node#deleteindex)| 88 | |`db.destroy() with _local path`|[`deleteLocalDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#deletelocaldocument)| 89 | |`db.get() with _local path`|[`getLocalDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#getlocaldocument)| 90 | |`db.insert() with _local path`|[`putLocalDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#putlocaldocument)| 91 | |`db.partitionInfo()`|[`getPartitionInformation`](https://cloud.ibm.com/apidocs/cloudant?code=node#getpartitioninformation)| 92 | |`db.partitionedList()/partitionedListAsStream()`|[`postPartitionAllDocs`, `postPartitionAllDocsAsStream`](https://cloud.ibm.com/apidocs/cloudant?code=node#postpartitionalldocs)| 93 | |`db.partitionedSearch()/partitionedSearchAsStream()`|[`postPartitionSearch`, `postPartitionSearchAsStream`](https://cloud.ibm.com/apidocs/cloudant?code=node#postpartitionsearch)| 94 | |`db.partitionedView()/partitionedViewAsStream()`|[`postPartitionView`, `postPartitionViewAsStream`](https://cloud.ibm.com/apidocs/cloudant?code=node#postpartitionview)| 95 | |`db.partitionedFind()/partitionedFindAsStream()`|[`postPartitionFind`, `postPartitionFindAsStream`](https://cloud.ibm.com/apidocs/cloudant?code=node#postpartitionfind-queries)| 96 | |`db.get_security()`|[`getSecurity`](https://cloud.ibm.com/apidocs/cloudant?code=node#getsecurity)| 97 | |`db.set_security()`|[`putSecurity`](https://cloud.ibm.com/apidocs/cloudant?code=node#putsecurity)| 98 | |`db.destroy()`|[`deleteDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#deletedocument)| 99 | |`db.get()`|[`getDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#getdocument)| 100 | |`db.head()`|[`headDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#headdocument)| 101 | |`db.insert()`|[`putDocument`](https://cloud.ibm.com/apidocs/cloudant?code=node#putdocument)| 102 | |`db.attachment.destroy()`|[`deleteAttachment`](https://cloud.ibm.com/apidocs/cloudant?code=node#deleteattachment)| 103 | |`db.attachment.get/getAsStream`|[`getAttachment`](https://cloud.ibm.com/apidocs/cloudant?code=node#getattachment)| 104 | |`db.attachment.insert/insertAsStream`|[`putAttachment`](https://cloud.ibm.com/apidocs/cloudant?code=node#putattachment)| 105 | |`generate_api_key()`|[`postApiKeys`](https://cloud.ibm.com/apidocs/cloudant?code=node#postapikeys)| 106 | |`db.set_security()`|[`putCloudantSecurityConfiguration`](https://cloud.ibm.com/apidocs/cloudant?code=node#putcloudantsecurity)| 107 | |`get_cors()`|[`getCorsInformation`](https://cloud.ibm.com/apidocs/cloudant?code=node#getcorsinformation)| 108 | |`set_cors()`|[`putCorsConfiguration`](https://cloud.ibm.com/apidocs/cloudant?code=node#putcorsconfiguration)| 109 | |`db.geo()`|[`getGeo`](https://cloud.ibm.com/apidocs/cloudant?code=node#getgeo)| 110 | -------------------------------------------------------------------------------- /api-migration.md: -------------------------------------------------------------------------------- 1 | # Migrating to new APIs 2 | 3 | This document covers migrating your code when a nodejs-cloudant major release 4 | has breaking API changes. Each section covers migrating from one major version 5 | to another. The section titles state the versions between which the change was 6 | made. 7 | 8 | ## 3.x → 4.x 9 | 10 | [Apache CouchDB Nano](https://www.npmjs.com/package/nano) version 7 used in 11 | `nodejs-cloudant` 3.x had an API error that allowed specifying a callback when 12 | using `*AsStream` functions. Using a callback with the `request` object caused a 13 | `Buffer` allocation attempt for the entire response size, which would fail for 14 | large streams. The correct approach is to use event listeners on the response 15 | stream. 16 | 17 | To prevent incorrect usage the option of providing a callback was removed from 18 | the Apache CouchDB Nano API in version 8 and consequently nodejs-cloudant 4.x. 19 | Consumers of the `*AsStream` functions using callbacks need to adapt their code 20 | to use event listeners instead. For example: 21 | 22 | ```js 23 | cloudant.db.listAsStream(function(error, response, body) { 24 | if (error) { 25 | console.log('ERROR'); 26 | } else { 27 | console.log('DONE'); 28 | } 29 | }).pipe(process.stdout); 30 | ``` 31 | 32 | may be replaced with: 33 | 34 | ```js 35 | cloudant.db.listAsStream() 36 | .on('error', function(error) { 37 | console.log('ERROR'); 38 | }) 39 | .on('end', function(error) { 40 | console.log('DONE'); 41 | }) 42 | .pipe(process.stdout); 43 | ``` 44 | 45 | ## 2.x → 3.x 46 | 47 | We've upgraded our [nano](https://www.npmjs.com/package/nano) dependency. This 48 | means all return types are now a `Promise` (except for the `...AsStream` 49 | functions). The `promise` plugin is no longer required. It is silently ignored 50 | when specified in the client configuration. 51 | 52 | Example: 53 | ```js 54 | var cloudant = new Cloudant({ url: myUrl, plugins: [ 'retry' ] }); 55 | 56 | // Lists all the databases. 57 | cloudant.db.list().then((dbs) => { 58 | dbs.forEach((db) => { 59 | console.log(db); 60 | }); 61 | }).catch((err) => { console.log(err); }); 62 | ``` 63 | 64 | Nano is responsible for resolving or rejecting all promises. Any errors thrown 65 | are created from within Nano. The old `CloudantError` type no longer exists. 66 | 67 | ## 1.x → 2.x 68 | 69 | This change introduces multiple plugin support by using a request interceptor 70 | pattern. All existing plugins included with this library have been rewritten 71 | to support the new implementation. 72 | 73 | Plugins must be passed via the `plugins` parameter in the Cloudant client 74 | constructor (formerly known as `plugin`). 75 | 76 | The library continues to support legacy plugins. They can be used in conjunction 77 | with new plugins. Your plugins list may contain any number of new plugins but 78 | only ever one legacy plugin. 79 | 80 | If you were using the 429 `retry` plugin in version 1.x then be aware that 81 | the configuration has now changed. The new plugin retries 429 and 5xx HTTP 82 | status codes as well as any request errors (such as connection reset errors). 83 | 84 | Example: 85 | - __Old__ plugin configuration: 86 | ```js 87 | var cloudant = new Cloudant({ url: myUrl, plugin: 'retry', retryAttempts: 5, retryTimeout: 1000 }); 88 | ``` 89 | - __New__ plugin configuration _(to mimic 1.x 429 `retry` behavior)_: 90 | ```js 91 | var cloudant = new Cloudant({ url: myUrl, maxAttempt: 5, plugins: { retry: { retryDelayMultiplier: 1, retryErrors: false, retryInitialDelayMsecs: 1000, retryStatusCodes: [ 429 ] } } }); 92 | ``` 93 | Or simply use the `retry` defaults (specified [here](https://github.com/cloudant/nodejs-cloudant#the-plugins)): 94 | ```js 95 | var cloudant = new Cloudant({ url: myUrl, maxAttempt: 5, plugins: 'retry' }); 96 | ``` 97 | 98 | Cookie authentication is enabled by default if no plugins are specified. 99 | 100 | Note that if you wish to use cookie authentication alongside other plugins you 101 | will need to include `cookieauth` in your plugins list. If you wish to disable 102 | all plugins then pass an empty array to the `plugins` parameter during 103 | construction (see below). 104 | 105 | ```js 106 | var cloudant = new Cloudant({ url: myUrl, plugins: [] }); 107 | ``` 108 | 109 | Finally, the `promise` plugin now throws a `CloudantError` (extended from 110 | `Error`) rather than a `string` which was considered bad practice. 111 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Node.js CRUD example 2 | 3 | ## Install dependencies 4 | 5 | ``` 6 | npm install 7 | ``` 8 | 9 | ## Set credentials 10 | 11 | Create an environment variable with your Cloudant credentials e.g. 12 | 13 | ``` 14 | export CLOUDANT_URL=https://myusername:mypassword@mydomain.cloudant.com 15 | ``` 16 | 17 | ## Run 18 | 19 | ``` 20 | node crud.js 21 | ``` 22 | 23 | This creates a database called `crud`, adds a document `mydoc`, reads it back, updates it, deletes it and then deletes the database. 24 | -------------------------------------------------------------------------------- /example/crud.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, 2017 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | if (!process.env.CLOUDANT_URL) { 17 | console.error("Please put the URL of your Cloudant instance in an environment variable 'CLOUDANT_URL'"); 18 | process.exit(1); 19 | } 20 | 21 | // load the Cloudant library 22 | var async = require('async'); 23 | var Cloudant = require('@cloudant/cloudant'); 24 | var cloudant = Cloudant({url: process.env.CLOUDANT_URL}); 25 | var dbname = 'crud'; 26 | var db = null; 27 | var doc = null; 28 | 29 | // create a database 30 | var createDatabase = function(callback) { 31 | console.log("Creating database '" + dbname + "'"); 32 | cloudant.db.create(dbname, function(err, data) { 33 | console.log('Error:', err); 34 | console.log('Data:', data); 35 | db = cloudant.db.use(dbname); 36 | callback(err, data); 37 | }); 38 | }; 39 | 40 | // create a document 41 | var createDocument = function(callback) { 42 | console.log("Creating document 'mydoc'"); 43 | // we are specifying the id of the document so we can update and delete it later 44 | db.insert({ _id: 'mydoc', a: 1, b: 'two' }, function(err, data) { 45 | console.log('Error:', err); 46 | console.log('Data:', data); 47 | callback(err, data); 48 | }); 49 | }; 50 | 51 | // read a document 52 | var readDocument = function(callback) { 53 | console.log("Reading document 'mydoc'"); 54 | db.get('mydoc', function(err, data) { 55 | console.log('Error:', err); 56 | console.log('Data:', data); 57 | // keep a copy of the doc so we know its revision token 58 | doc = data; 59 | callback(err, data); 60 | }); 61 | }; 62 | 63 | // update a document 64 | var updateDocument = function(callback) { 65 | console.log("Updating document 'mydoc'"); 66 | // make a change to the document, using the copy we kept from reading it back 67 | doc.c = true; 68 | db.insert(doc, function(err, data) { 69 | console.log('Error:', err); 70 | console.log('Data:', data); 71 | // keep the revision of the update so we can delete it 72 | doc._rev = data.rev; 73 | callback(err, data); 74 | }); 75 | }; 76 | 77 | // deleting a document 78 | var deleteDocument = function(callback) { 79 | console.log("Deleting document 'mydoc'"); 80 | // supply the id and revision to be deleted 81 | db.destroy(doc._id, doc._rev, function(err, data) { 82 | console.log('Error:', err); 83 | console.log('Data:', data); 84 | callback(err, data); 85 | }); 86 | }; 87 | 88 | // deleting the database document 89 | var deleteDatabase = function(callback) { 90 | console.log("Deleting database '" + dbname + "'"); 91 | cloudant.db.destroy(dbname, function(err, data) { 92 | console.log('Error:', err); 93 | console.log('Data:', data); 94 | callback(err, data); 95 | }); 96 | }; 97 | 98 | async.series([createDatabase, createDocument, readDocument, updateDocument, deleteDocument, deleteDatabase]); 99 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "async": "^1.5.0", 4 | "@cloudant/cloudant": "^2.0.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2019 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const async = require('async'); 17 | const concat = require('concat-stream'); 18 | const debug = require('debug')('cloudant:client'); 19 | const EventRelay = require('./eventrelay.js'); 20 | const path = require('path'); 21 | const PassThroughDuplex = require('./passthroughduplex.js'); 22 | const pkg = require('../package.json'); 23 | const utils = require('./clientutils.js'); 24 | 25 | const DEFAULTS = { 26 | maxAttempt: 3 27 | }; 28 | 29 | /** 30 | * Create a Cloudant client for managing requests. 31 | * 32 | * @param {Object} cfg - Request client configuration. 33 | */ 34 | class CloudantClient { 35 | constructor(cfg) { 36 | var self = this; 37 | 38 | cfg = cfg || {}; 39 | self._cfg = Object.assign({}, DEFAULTS, cfg); 40 | 41 | var client; 42 | self._plugins = []; 43 | self._pluginIds = []; 44 | self.useLegacyPlugin = false; 45 | 46 | // Build plugin array. 47 | var plugins = []; 48 | 49 | if (self._cfg.creds && self._cfg.creds.iamApiKey) { 50 | // => Found IAM API key in VCAP - Add 'iamauth' plugin. 51 | plugins = [ { iamauth: { iamApiKey: self._cfg.creds.iamApiKey } } ]; 52 | } else if (typeof this._cfg.plugins === 'undefined') { 53 | // => No plugins specified - Add 'cookieauth' plugin. 54 | plugins = [ { cookieauth: { errorOnNoCreds: false } } ]; 55 | } 56 | 57 | // Add user specified plugins. 58 | if (typeof this._cfg.plugins !== 'undefined') { 59 | [].concat(self._cfg.plugins).forEach(function(plugin) { 60 | if (typeof plugin !== 'function' || plugin.pluginVersion >= 2) { 61 | plugins.push(plugin); 62 | } else if (self.useLegacyPlugin) { 63 | throw new Error('Using multiple legacy plugins is not permitted'); 64 | } else { 65 | self.useLegacyPlugin = true; 66 | client = plugin; // use legacy plugin as client 67 | } 68 | }); 69 | } 70 | 71 | // initialize the internal client 72 | self._initClient(client); 73 | 74 | // add plugins 75 | self._addPlugins(plugins); 76 | } 77 | 78 | _addPlugins(plugins) { 79 | var self = this; 80 | 81 | if (!Array.isArray(plugins)) { 82 | plugins = [ plugins ]; 83 | } 84 | 85 | plugins.forEach(function(plugin) { 86 | var cfg, Plugin; 87 | 88 | switch (typeof plugin) { 89 | // 1). Custom plugin 90 | case 'function': 91 | debug(`Found custom plugin: '${plugin.id}'`); 92 | Plugin = plugin; 93 | cfg = {}; 94 | break; 95 | 96 | // 2). Plugin (with configuration): { 'pluginName': { 'configKey1': 'configValue1', ... } } 97 | case 'object': 98 | if (Array.isArray(plugin) || Object.keys(plugin).length !== 1) { 99 | throw new Error(`Invalid plugin configuration: '${plugin}'`); 100 | } 101 | 102 | var pluginName = Object.keys(plugin)[0]; 103 | Plugin = self._importPlugin(pluginName); 104 | 105 | cfg = plugin[pluginName]; 106 | if (typeof cfg !== 'object' || Array.isArray(cfg)) { 107 | throw new Error(`Invalid plugin configuration: '${plugin}'`); 108 | } 109 | break; 110 | 111 | // 3). Plugin (no configuration): 'pluginName' 112 | case 'string': 113 | if (plugin === 'base' || plugin === 'default' || plugin === 'promises') { 114 | return; // noop 115 | } 116 | 117 | Plugin = self._importPlugin(plugin); 118 | cfg = {}; 119 | break; 120 | 121 | // 4). Noop 122 | case 'undefined': 123 | return; 124 | default: 125 | throw new Error(`Invalid plugin configuration: '${plugin}'`); 126 | } 127 | 128 | if (self._pluginIds.indexOf(Plugin.id) !== -1) { 129 | debug(`Not adding duplicate plugin: '${Plugin.id}'`); 130 | } else { 131 | debug(`Adding plugin: '${Plugin.id}'`); 132 | var creds = self._cfg.creds || {}; 133 | self._plugins.push( 134 | // instantiate plugin 135 | new Plugin(self._client, Object.assign({ serverUrl: creds.outUrl }, cfg)) 136 | ); 137 | self._pluginIds.push(Plugin.id); 138 | } 139 | }); 140 | } 141 | 142 | _buildPluginPath(name) { 143 | // Only a plugin name was provided: use plugin directory 144 | if (path.basename(name) === name) { 145 | return '../plugins/' + name; 146 | } 147 | 148 | // An absolute path was provided 149 | if (path.isAbsolute(name)) { 150 | return name; 151 | } 152 | 153 | // A relative path was provided 154 | return path.join(process.cwd(), name); 155 | } 156 | 157 | _importPlugin(pluginName) { 158 | switch (pluginName) { 159 | // Note: All built-in plugins are individually listed here to ensure they 160 | // are included in a webpack bundle. 161 | case 'cookieauth': 162 | return require('../plugins/cookieauth'); 163 | case 'iamauth': 164 | return require('../plugins/iamauth'); 165 | case 'retry': 166 | return require('../plugins/retry'); 167 | default: 168 | // Warning: Custom plugins will not be included in a webpack bundle 169 | // by default because the exact module is not known on compile 170 | // time. 171 | try { 172 | // Use template literal to suppress 'dependency is an expression' 173 | // webpack compilation warning. 174 | return require(`${this._buildPluginPath(pluginName)}`); 175 | } catch (e) { 176 | throw new Error(`Failed to load plugin - ${e.message}`); 177 | } 178 | } 179 | } 180 | 181 | _initClient(client) { 182 | if (typeof client !== 'undefined') { 183 | debug('Using custom client.'); 184 | this._client = client; 185 | return; 186 | } 187 | 188 | var protocol; 189 | if (this._cfg && this._cfg.https === false) { 190 | protocol = require('http'); 191 | } else { 192 | protocol = require('https'); // default to https 193 | } 194 | 195 | var agent = new protocol.Agent({ 196 | keepAlive: true, 197 | keepAliveMsecs: 30000, 198 | maxSockets: 6 199 | }); 200 | var requestDefaults = { 201 | agent: agent, 202 | gzip: true, 203 | headers: { 204 | // set library UA header 205 | 'User-Agent': `nodejs-cloudant/${pkg.version} (Node.js ${process.version})` 206 | }, 207 | jar: false 208 | }; 209 | 210 | if (this._cfg.requestDefaults) { 211 | // allow user to override defaults 212 | requestDefaults = Object.assign({}, requestDefaults, this._cfg.requestDefaults); 213 | } 214 | 215 | debug('Using request options: %j', requestDefaults); 216 | 217 | this.requestDefaults = requestDefaults; // expose request defaults 218 | this._client = require('request').defaults(requestDefaults); 219 | } 220 | 221 | _executeRequest(request, done) { 222 | debug('Submitting request: %j', request.options); 223 | 224 | request.response = this._client( 225 | request.options, utils.wrapCallback(request, done)); 226 | 227 | // define new source on event relay 228 | request.eventRelay.setSource(request.response); 229 | 230 | request.response 231 | .on('response', function(response) { 232 | request.response.pause(); 233 | utils.runHooks('onResponse', request, response, function() { 234 | utils.processState(request, done); // process response hook results 235 | }); 236 | }); 237 | 238 | if (typeof request.clientCallback === 'undefined') { 239 | debug('No client callback specified.'); 240 | request.response 241 | .on('error', function(error) { 242 | utils.runHooks('onError', request, error, function() { 243 | utils.processState(request, done); // process error hook results 244 | }); 245 | }); 246 | } 247 | } 248 | 249 | // public 250 | 251 | /** 252 | * Get a client plugin instance. 253 | * 254 | * @param {string} pluginId 255 | */ 256 | getPlugin(pluginId) { 257 | return this._plugins[this._pluginIds.indexOf(pluginId)]; 258 | } 259 | 260 | /** 261 | * Perform a request using this Cloudant client. 262 | * 263 | * @param {Object} options - HTTP options. 264 | * @param {requestCallback} callback - The callback that handles the response. 265 | */ 266 | request(options, callback) { 267 | var self = this; 268 | 269 | if (typeof options === 'string') { 270 | options = { method: 'GET', url: options }; // default GET 271 | } 272 | 273 | var request = {}; 274 | request.abort = false; 275 | request.clientCallback = callback; 276 | 277 | request.clientStream = new PassThroughDuplex(); 278 | 279 | request.clientStream.on('error', function(err) { 280 | debug(err); 281 | }); 282 | request.clientStream.on('pipe', function() { 283 | debug('Request body is being piped.'); 284 | request.pipedRequest = true; 285 | }); 286 | 287 | request.eventRelay = new EventRelay(request.clientStream); 288 | 289 | request.plugins = self._plugins; 290 | 291 | // init state 292 | request.state = { 293 | attempt: 0, 294 | maxAttempt: self._cfg.maxAttempt, 295 | // following are editable by plugin hooks during execution 296 | abortWithResponse: undefined, 297 | retry: false, 298 | retryDelayMsecs: 0 299 | }; 300 | 301 | // add plugin stash 302 | request.plugin_stash = {}; 303 | request.plugins.forEach(function(plugin) { 304 | // allow plugin hooks to share data via the request state 305 | request.plugin_stash[plugin.id] = {}; 306 | }); 307 | 308 | request.clientStream.abort = function() { 309 | // aborts response during hook execution phase. 310 | // note that once a "good" request is made, this abort function is 311 | // monkey-patched with `request.abort()`. 312 | request.abort = true; 313 | }; 314 | 315 | async.forever(function(done) { 316 | request.doneCallback = done; 317 | request.done = false; 318 | 319 | // Fixes an intermittent bug where the `done` callback is executed 320 | // multiple times. 321 | done = function(error) { 322 | if (request.done) { 323 | debug('Callback was already called.'); 324 | return; 325 | } 326 | request.done = true; 327 | return request.doneCallback(error); 328 | }; 329 | 330 | request.options = Object.assign({}, options); // new copy 331 | request.response = undefined; 332 | 333 | // update state 334 | request.state.attempt++; 335 | request.state.retry = false; 336 | request.state.sending = false; 337 | 338 | debug(`Request attempt: ${request.state.attempt}`); 339 | debug(`Delaying request for ${request.state.retryDelayMsecs} Msecs.`); 340 | 341 | setTimeout(function() { 342 | utils.runHooks('onRequest', request, request.options, function(err) { 343 | utils.processState(request, function(stop) { 344 | if (request.state.retry) { 345 | debug('The onRequest hook issued retry.'); 346 | return done(); 347 | } 348 | if (stop) { 349 | debug(`The onRequest hook issued abort: ${stop}`); 350 | return done(stop); 351 | } 352 | if (request.abort) { 353 | debug('Client issued abort during plugin execution.'); 354 | return done(new Error('Client issued abort')); 355 | } 356 | 357 | request.state.sending = true; // indicates onRequest hooks completed 358 | 359 | if (!request.pipedRequest) { 360 | self._executeRequest(request, done); 361 | } else { 362 | if (typeof request.pipedRequestBuffer !== 'undefined' && request.state.attempt > 1) { 363 | request.options.body = request.pipedRequestBuffer; 364 | self._executeRequest(request, done); 365 | } else { 366 | // copy stream contents to buffer for possible retry 367 | var concatStream = concat({ encoding: 'buffer' }, function(buffer) { 368 | request.options.body = request.pipedRequestBuffer = buffer; 369 | self._executeRequest(request, done); 370 | }); 371 | request.clientStream.passThroughWritable 372 | .on('error', function(error) { 373 | debug(error); 374 | self._executeRequest(request, done); 375 | }) 376 | .pipe(concatStream); 377 | } 378 | } 379 | }); 380 | }); 381 | }, request.state.retryDelayMsecs); 382 | }, function(err) { debug(err.message); }); 383 | 384 | return request.clientStream; // return stream to client 385 | } 386 | } 387 | 388 | module.exports = CloudantClient; 389 | -------------------------------------------------------------------------------- /lib/clientutils.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2021 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const async = require('async'); 17 | const debug = require('debug')('cloudant:clientutils'); 18 | 19 | // send response to the client 20 | var sendResponseToClient = function(response, clientStream, clientCallback) { 21 | debug('An alternative response will be returned to the client'); 22 | // response = [, , ] 23 | if (response[0]) { 24 | clientStream.emit('error', response[0]); 25 | } 26 | if (response[1]) { 27 | clientStream.emit('response', response[1]); 28 | } 29 | if (response[2]) { 30 | clientStream.emit('data', Buffer.from(response[2], 'utf8')); 31 | } 32 | clientStream.emit('end'); 33 | 34 | if (typeof clientCallback === 'function') { 35 | clientCallback.apply(null, response); // execute client callback 36 | } 37 | }; 38 | 39 | // update the state with a new state (from plugin hook) 40 | var updateState = function(state, newState, callback) { 41 | if (newState.abortWithResponse) { 42 | // plugin requested immediate abort 43 | state.retry = false; 44 | state.abortWithResponse = newState.abortWithResponse; 45 | return callback(new Error('Plugin issued abort')); // stop plugin hooks 46 | } 47 | if (newState.retry) { 48 | state.retry = true; // plugin requested a retry 49 | } 50 | if (newState.retryDelayMsecs > state.retryDelayMsecs) { 51 | state.retryDelayMsecs = newState.retryDelayMsecs; // set new retry delay 52 | } 53 | callback(); 54 | }; 55 | 56 | // public 57 | 58 | // process state (following plugin execution) 59 | var processState = function(r, callback) { 60 | var abort = function() { 61 | if (r.response) { 62 | debug('Client issued abort.'); 63 | r.response.abort(); 64 | } 65 | }; 66 | 67 | if (r.abort) { 68 | // [1] => Client has called for the request to be aborted. 69 | abort(); 70 | setImmediate(callback, new Error('Client issued abort')); // no retry 71 | return; 72 | } 73 | 74 | if (r.state.abortWithResponse) { 75 | // [2] => Plugin requested abort and specified alternative response. 76 | abort(); 77 | sendResponseToClient(r.state.abortWithResponse, r.clientStream, r.clientCallback); 78 | var err = new Error('Plugin issued abort'); 79 | err.skipClientCallback = true; 80 | setImmediate(callback, err); // no retry 81 | return; 82 | } 83 | 84 | if (r.state.retry && r.state.attempt < r.state.maxAttempt) { 85 | // [3] => One or more plugins have called for the request to be retried. 86 | abort(); 87 | debug('Plugin issued a retry.'); 88 | setImmediate(callback); // retry 89 | return; 90 | } 91 | 92 | if (r.response) { 93 | // monkey-patch real abort function 94 | r.clientStream.abort = function() { 95 | debug('Client issued abort.'); 96 | r.response.abort(); 97 | }; 98 | } 99 | 100 | if (r.state.retry) { 101 | debug('Failed to retry request. Too many retry attempts.'); 102 | r.state.retry = false; 103 | } 104 | 105 | if (!r.state.sending) { 106 | // [4] => Request has not yet been sent. Still processing 'onRequest' hooks. 107 | setImmediate(callback); // continue 108 | return; 109 | } 110 | 111 | if (r.response) { 112 | // pass response events/data to awaiting client 113 | if (r.eventRelay) { 114 | r.eventRelay.resume(); 115 | } 116 | if (r.clientStream.destinations.length > 0) { 117 | r.response.pipe(r.clientStream.passThroughReadable); 118 | } 119 | r.response.resume(); 120 | } 121 | 122 | // [5] => Return response to awaiting client. 123 | r.state.final = true; // flags that we are terminating the loop 124 | setImmediate(callback, new Error('No retry requested')); // no retry 125 | }; 126 | 127 | // execute a specified hook for all plugins 128 | var runHooks = function(hookName, r, data, end) { 129 | if (r.plugins.length === 0) { 130 | end(); // no plugins 131 | } else { 132 | async.eachSeries(r.plugins, function(plugin, done) { 133 | if (typeof plugin[hookName] !== 'function') { 134 | done(); // no hooks for plugin 135 | } else if (plugin.disabled) { 136 | debug(`Skipping hook ${hookName} for disabled plugin '${plugin.id}'.`); 137 | done(); 138 | } else { 139 | debug(`Running hook ${hookName} for plugin '${plugin.id}'.`); 140 | var oldState = Object.assign({}, r.state); 141 | oldState.stash = r.plugin_stash[plugin.id]; // add stash 142 | plugin[hookName](oldState, data, function(newState) { 143 | updateState(r.state, newState, done); 144 | }); 145 | } 146 | }, end); 147 | } 148 | }; 149 | 150 | // wrap client callback to allow for plugin error hook execution 151 | var wrapCallback = function(r, done) { 152 | if (typeof r.clientCallback === 'undefined') { 153 | return undefined; // noop 154 | } else { 155 | debug('Client callback specified.'); 156 | // return wrapped callback 157 | return function(error, response, body) { 158 | if (error) { 159 | runHooks('onError', r, error, function() { 160 | processState(r, function(stop) { 161 | if ((stop && !stop.skipClientCallback) || r.state.final) { 162 | r.clientCallback(error, response, body); 163 | } 164 | done(stop); 165 | }); 166 | }); 167 | } else { 168 | r.clientCallback(error, response, body); 169 | // execute `done()` in response hook 170 | } 171 | }; 172 | } 173 | }; 174 | 175 | module.exports = { 176 | runHooks: runHooks, 177 | processState: processState, 178 | wrapCallback: wrapCallback 179 | }; 180 | -------------------------------------------------------------------------------- /lib/eventrelay.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const debug = require('debug')('cloudant:eventrelay'); 17 | 18 | /** 19 | * Relay all events from a source emitter to a target emitter. 20 | * 21 | * @param {Object} source - Source event emitter. 22 | * @param {Object} target - Target event emitter. 23 | */ 24 | class EventRelay { 25 | constructor(source, target) { 26 | var self = this; 27 | 28 | if (typeof target === 'undefined') { 29 | self._target = source; 30 | } else { 31 | self._target = target; 32 | self.setSource(source); 33 | } 34 | 35 | self._paused = true; 36 | self._eventsStash = []; 37 | } 38 | 39 | // Clear all stashed events. 40 | clear() { 41 | this._eventsStash = []; 42 | } 43 | 44 | // Pause event relay. 45 | pause() { 46 | this._paused = true; 47 | } 48 | 49 | // Resume event relay and fire all stashed events. 50 | resume() { 51 | var self = this; 52 | 53 | this._paused = false; 54 | 55 | debug('Relaying captured events to target stream.'); 56 | self._eventsStash.forEach(function(event) { 57 | self._target.emit.apply(self._target, event); 58 | }); 59 | } 60 | 61 | // Set a new source event emitter. 62 | setSource(source) { 63 | var self = this; 64 | 65 | self.clear(); 66 | self.pause(); 67 | 68 | debug('Setting new source stream.'); 69 | self._source = source; 70 | 71 | self._oldEmit = self._source.emit; 72 | self._source.emit = function() { 73 | if (self._paused) { 74 | self._eventsStash.push(arguments); 75 | } else { 76 | self._target.emit.apply(self._target, arguments); 77 | } 78 | self._oldEmit.apply(self._source, arguments); 79 | }; 80 | } 81 | } 82 | 83 | module.exports = EventRelay; 84 | -------------------------------------------------------------------------------- /lib/passthroughduplex.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const stream = require('stream'); 17 | 18 | class PassThroughDuplex extends stream.Duplex { 19 | constructor(options) { 20 | super(options); 21 | 22 | this.destinations = []; 23 | 24 | this.passThroughReadable = new stream.PassThrough(); 25 | this.passThroughReadable.on('error', function(error) { 26 | this.emit(error); 27 | }); 28 | 29 | this.passThroughWritable = new stream.PassThrough(); 30 | this.passThroughWritable.on('error', function(error) { 31 | this.emit(error); 32 | }); 33 | } 34 | 35 | // readable 36 | 37 | pipe(destination, options) { 38 | this.destinations.push(destination); 39 | return this.passThroughReadable.pipe(destination, options); 40 | } 41 | 42 | read(size) { 43 | return this.passThroughReadable.read(size); 44 | } 45 | 46 | setEncoding(encoding) { 47 | return this.passThroughReadable.setEncoding(encoding); 48 | } 49 | 50 | // writable 51 | 52 | end(chunk, encoding, callback) { 53 | return this.passThroughWritable.end(chunk, encoding, callback); 54 | } 55 | 56 | write(chunk, encoding, callback) { 57 | return this.passThroughWritable.write(chunk, encoding, callback); 58 | } 59 | 60 | // destroy 61 | 62 | destroy(error) { 63 | this.passThroughWritable.destroy(error); 64 | this.passThroughReadable.destroy(error); 65 | } 66 | 67 | // events 68 | 69 | on(event, listener) { 70 | if (!this.passThroughWritable) { 71 | return super.on(event, listener); 72 | } 73 | 74 | switch (event) { 75 | case 'drain': 76 | case 'finish': 77 | return this.passThroughWritable.on(event, listener); 78 | default: 79 | return super.on(event, listener); 80 | } 81 | } 82 | } 83 | 84 | PassThroughDuplex.addListener = PassThroughDuplex.on; 85 | 86 | module.exports = PassThroughDuplex; 87 | -------------------------------------------------------------------------------- /lib/reconfigure.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | // reconfigure deals with the various ways the credentials can be passed in 17 | // and returns an full URL 18 | // e.g. { account:"myaccount", password: "mypassword"} 19 | // or { key: "mykey", password: "mykey", account:"myaccount"} 20 | // or { key: "mykey", password: "mykey", account:"myaccount"} 21 | // or { account:"myaccount.cloudant.com", password: "mykey"} 22 | // or { account: "myaccount"} 23 | // or { url: "https://mykey:mypassword@myaccount.cloudant.com"} 24 | // or { instanceName: "mycloudantservice", vcapServices: JSON.parse(process.env.VCAP_SERVICES)} 25 | 26 | var url = require('url'); 27 | 28 | module.exports = function(config) { 29 | config = JSON.parse(JSON.stringify(config)); // clone 30 | var outUrl; 31 | var outCreds = {}; 32 | var options; 33 | var username; 34 | var password; 35 | // if a full URL is passed in 36 | if (config.url) { 37 | // parse the URL 38 | var parsed = null; 39 | try { 40 | parsed = url.parse(config.url); 41 | } catch (e) { 42 | parsed = null; 43 | } 44 | if (!config.url || !parsed || !parsed.hostname || !parsed.protocol || !parsed.slashes) { 45 | outCreds.outUrl = null; 46 | return outCreds; 47 | } 48 | 49 | // enforce HTTPS for *cloudant.com domains 50 | if (parsed.hostname.match(/cloudant\.com$/) && parsed.protocol === 'http:') { 51 | console.warn('WARNING: You are sending your password as plaintext over the HTTP; switching to HTTPS'); 52 | 53 | // force HTTPS 54 | parsed.protocol = 'https:'; 55 | 56 | // remove port number and path 57 | parsed.host = parsed.host.replace(/:[0-9]*$/, ''); 58 | delete parsed.port; 59 | delete parsed.pathname; 60 | delete parsed.path; 61 | 62 | // reconstruct the URL 63 | config.url = url.format(parsed); 64 | } else { 65 | options = getOptions(config); 66 | username = options.username; 67 | password = options.password; 68 | if (username && password) { 69 | config.url = parsed.protocol + '//' + encodeURIComponent(username) + ':' + 70 | encodeURIComponent(password) + '@' + parsed.host; 71 | } 72 | } 73 | outUrl = config.url; 74 | } else if (config.vcapServices) { 75 | var cloudantServices; 76 | if (typeof config.vcapServiceName !== 'undefined') { 77 | cloudantServices = config.vcapServices[config.vcapServiceName]; 78 | } else { 79 | cloudantServices = config.vcapServices.cloudantNoSQLDB; 80 | } 81 | 82 | if (!cloudantServices || cloudantServices.length === 0) { 83 | throw new Error('Missing Cloudant service in vcapServices'); 84 | } 85 | 86 | if (typeof config.vcapInstanceName !== 'undefined') { 87 | config.instanceName = config.vcapInstanceName; // alias 88 | } 89 | 90 | for (var i = 0; i < cloudantServices.length; i++) { 91 | if (typeof config.instanceName === 'undefined' || cloudantServices[i].name === config.instanceName) { 92 | var credentials = cloudantServices[i].credentials; 93 | if (credentials && credentials.host) { 94 | outUrl = 'https://' + encodeURIComponent(credentials.host); 95 | if (credentials.apikey) { 96 | outCreds.iamApiKey = credentials.apikey; 97 | } else if (credentials.username && credentials.password) { 98 | outUrl = 'https://' + encodeURIComponent(credentials.username) + ':' + 99 | encodeURIComponent(credentials.password) + '@' + encodeURIComponent(credentials.host); 100 | } 101 | break; 102 | } else { 103 | throw new Error('Invalid Cloudant service in vcapServices'); 104 | } 105 | } 106 | } 107 | 108 | if (!outUrl) { 109 | throw new Error('Missing Cloudant service in vcapServices'); 110 | } 111 | } else { 112 | // An account can be just the username, or the full cloudant URL. 113 | var match = config.account && 114 | config.account.match && 115 | config.account.match(/([^.]+)\.cloudant\.com/); 116 | if (match) { config.account = match[1]; } 117 | options = getOptions(config); 118 | username = options.username; 119 | password = options.password; 120 | 121 | // Configure for Cloudant, either authenticated or anonymous. 122 | if (config.account && password) { 123 | config.url = 'https://' + encodeURIComponent(username) + ':' + 124 | encodeURIComponent(password) + '@' + 125 | encodeURIComponent(config.account) + '.cloudant.com'; 126 | } else if (config.account) { 127 | config.url = 'https://' + encodeURIComponent(config.account) + 128 | '.cloudant.com'; 129 | } 130 | 131 | outUrl = config.url; 132 | } 133 | 134 | // We trim out the trailing `/` because when the URL tracks down to `nano` we have to 135 | // worry that the trailing `/` doubles up depending on how URLs are built, this creates 136 | // "Database does not exist." errors. 137 | // Issue: cloudant/nodejs-cloudant#129 138 | if (outUrl && outUrl.slice(-1) === '/') { 139 | outUrl = outUrl.slice(0, -1); 140 | } 141 | 142 | outCreds.outUrl = (outUrl || null); 143 | return outCreds; 144 | }; 145 | 146 | module.exports.getOptions = getOptions; 147 | function getOptions(config) { 148 | // The username is the account ("foo" for "foo.cloudant.com") 149 | // or the third-party API key. 150 | var result = {password: config.password, username: config.key || config.username || config.account}; 151 | return result; 152 | } 153 | -------------------------------------------------------------------------------- /lib/tokens/CookieTokenManager.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const debug = require('debug')('cloudant:tokens:cookietokenmanager'); 17 | const TokenManager = require('./TokenManager'); 18 | 19 | class CookieTokenManager extends TokenManager { 20 | constructor(client, jar, sessionUrl, username, password) { 21 | super(client, jar, sessionUrl); 22 | 23 | this._username = username; 24 | this._password = password; 25 | } 26 | 27 | _getToken(callback) { 28 | debug('Submitting cookie token request.'); 29 | this._client({ 30 | url: this._sessionUrl, 31 | method: 'POST', 32 | json: true, 33 | body: { 34 | name: this._username, 35 | password: this._password 36 | }, 37 | jar: this._jar 38 | }, (error, response, body) => { 39 | if (error) { 40 | debug(error); 41 | callback(error); 42 | } else if (response.statusCode === 200) { 43 | debug('Successfully renewed session cookie.'); 44 | callback(null, response); 45 | } else { 46 | let msg = `Failed to get cookie. Status code: ${response.statusCode}`; 47 | debug(msg); 48 | callback(new Error(msg), response); 49 | } 50 | }); 51 | } 52 | } 53 | 54 | module.exports = CookieTokenManager; 55 | -------------------------------------------------------------------------------- /lib/tokens/IamTokenManager.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const a = require('async'); 17 | const debug = require('debug')('cloudant:tokens:iamtokenmanager'); 18 | const TokenManager = require('./TokenManager'); 19 | 20 | class IAMTokenManager extends TokenManager { 21 | constructor(client, jar, sessionUrl, iamTokenUrl, iamApiKey, iamClientId, iamClientSecret) { 22 | super(client, jar, sessionUrl); 23 | 24 | this._iamTokenUrl = iamTokenUrl; 25 | this._iamApiKey = iamApiKey; 26 | this._iamClientId = iamClientId; 27 | this._iamClientSecret = iamClientSecret; 28 | } 29 | 30 | _getToken(done) { 31 | var self = this; 32 | 33 | debug('Making IAM session request.'); 34 | let accessToken; 35 | a.series([ 36 | (callback) => { 37 | let accessTokenAuth; 38 | if (self._iamClientId && self._iamClientSecret) { 39 | accessTokenAuth = { user: self._iamClientId, pass: self._iamClientSecret }; 40 | } 41 | debug('Getting access token.'); 42 | self._client({ 43 | url: self._iamTokenUrl, 44 | method: 'POST', 45 | auth: accessTokenAuth, 46 | headers: { 'Accepts': 'application/json' }, 47 | form: { 48 | 'grant_type': 'urn:ibm:params:oauth:grant-type:apikey', 49 | 'response_type': 'cloud_iam', 50 | 'apikey': self._iamApiKey 51 | }, 52 | json: true 53 | }, (error, response, body) => { 54 | if (error) { 55 | callback(error); 56 | } else if (response.statusCode === 200) { 57 | if (body.access_token) { 58 | accessToken = body.access_token; 59 | debug('Retrieved access token from IAM token service.'); 60 | callback(null, response); 61 | } else { 62 | callback(new Error('Invalid response from IAM token service'), response); 63 | } 64 | } else { 65 | let msg = `Failed to acquire access token. Status code: ${response.statusCode}`; 66 | callback(new Error(msg), response); 67 | } 68 | }); 69 | }, 70 | (callback) => { 71 | debug('Perform IAM cookie based user login.'); 72 | self._client({ 73 | url: self._sessionUrl, 74 | method: 'POST', 75 | form: { 'access_token': accessToken }, 76 | jar: self._jar, 77 | json: true 78 | }, (error, response, body) => { 79 | if (error) { 80 | callback(error); 81 | } else if (response.statusCode === 200) { 82 | debug('Successfully renewed IAM session.'); 83 | callback(null, response); 84 | } else { 85 | let msg = `Failed to exchange IAM token with Cloudant. Status code: ${response.statusCode}`; 86 | callback(new Error(msg), response); 87 | } 88 | }); 89 | } 90 | ], (error, responses) => { 91 | done(error, responses[responses.length - 1]); 92 | }); 93 | } 94 | 95 | setIamApiKey(newIamApiKey) { 96 | this._iamApiKey = newIamApiKey; 97 | this.attemptTokenRenewal = true; 98 | } 99 | } 100 | 101 | module.exports = IAMTokenManager; 102 | -------------------------------------------------------------------------------- /lib/tokens/TokenManager.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2019, 2021 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const debug = require('debug')('cloudant:tokens:tokenmanager'); 17 | const cookie = require('cookie'); 18 | const EventEmitter = require('events'); 19 | 20 | class TokenManager { 21 | constructor(client, jar, sessionUrl) { 22 | this._client = client; 23 | this._jar = jar; 24 | this._sessionUrl = sessionUrl; 25 | 26 | this._attemptTokenRenewal = true; 27 | this._isTokenRenewing = false; 28 | 29 | this._tokenExchangeEE = new EventEmitter().setMaxListeners(Infinity); 30 | 31 | // START monkey patch for https://github.com/salesforce/tough-cookie/issues/154 32 | // Use the tough-cookie CookieJar from the RequestJar 33 | const cookieJar = this._jar ? this._jar._jar : false; 34 | // Check if we've already patched the jar 35 | if (cookieJar && !cookieJar.cloudantPatch) { 36 | // Set the patching flag 37 | cookieJar.cloudantPatch = true; 38 | // Replace the store's updateCookie function with one that applies a patch to newCookie 39 | const originalUpdateCookieFn = cookieJar.store.updateCookie; 40 | cookieJar.store.updateCookie = function(oldCookie, newCookie, cb) { 41 | // Add current time as an update timestamp to the newCookie 42 | newCookie.cloudantPatchUpdateTime = new Date(); 43 | // Replace the cookie's expiryTime function with one that uses cloudantPatchUpdateTime 44 | // in place of creation time to check the expiry. 45 | const originalExpiryTimeFn = newCookie.expiryTime; 46 | newCookie.expiryTime = function(now) { 47 | // The original expiryTime check is relative to a time in this order: 48 | // 1. supplied now argument 49 | // 2. this.creation (original cookie creation time) 50 | // 3. current time 51 | // This patch replaces 2 with an expiry check relative to the cloudantPatchUpdateTime if set instead of 52 | // the creation time by passing it as the now argument. 53 | return originalExpiryTimeFn.call( 54 | newCookie, 55 | newCookie.cloudantPatchUpdateTime || now 56 | ); 57 | }; 58 | // Finally delegate back to the original update function or the fallback put (which is set by Cookie 59 | // when an update function is not present on the store). Since we always set an update function for our 60 | // patch we need to also provide that fallback. 61 | if (originalUpdateCookieFn) { 62 | originalUpdateCookieFn.call( 63 | cookieJar.store, 64 | oldCookie, 65 | newCookie, 66 | cb 67 | ); 68 | } else { 69 | cookieJar.store.putCookie(newCookie, cb); 70 | } 71 | }; 72 | } 73 | // END cookie jar monkey patch 74 | } 75 | 76 | _autoRenew(defaultMaxAgeSecs) { 77 | debug('Auto renewing token now...'); 78 | this._renew().then((response) => { 79 | let setCookieHeader = response.headers['set-cookie']; 80 | let headerValue = Array.isArray(setCookieHeader) ? setCookieHeader[0] : setCookieHeader; 81 | let maxAgeSecs = cookie.parse(headerValue)['Max-Age'] || defaultMaxAgeSecs; 82 | let delayMSecs = maxAgeSecs / 2 * 1000; 83 | debug(`Renewing token in ${delayMSecs} milliseconds.`); 84 | setTimeout(this._autoRenew.bind(this, defaultMaxAgeSecs), delayMSecs).unref(); 85 | }).catch((error) => { 86 | debug(`Failed to auto renew token - ${error}. Retrying in 60 seconds.`); 87 | setTimeout(this._autoRenew.bind(this), 60000).unref(); 88 | }); 89 | } 90 | 91 | _getToken(callback) { 92 | // ** Method to be implemented by _all_ subclasses of `TokenManager` ** 93 | throw new Error('Not implemented.'); 94 | } 95 | 96 | // Renew the token. 97 | _renew() { 98 | if (!this._isTokenRenewing) { 99 | this._isTokenRenewing = true; 100 | this._tokenExchangeEE.removeAllListeners(); 101 | debug('Starting token renewal.'); 102 | this._getToken((error, response) => { 103 | if (error) { 104 | this._tokenExchangeEE.emit('error', error, response); 105 | } else { 106 | this._tokenExchangeEE.emit('success', response); 107 | this._attemptTokenRenewal = false; 108 | } 109 | debug('Finished token renewal.'); 110 | this._isTokenRenewing = false; 111 | }); 112 | } 113 | return new Promise((resolve, reject) => { 114 | this._tokenExchangeEE.once('success', resolve); 115 | this._tokenExchangeEE.once('error', (error, response) => { 116 | error.response = response; 117 | reject(error); 118 | }); 119 | }); 120 | } 121 | 122 | // Getter for `attemptTokenRenewal`. 123 | // - `true` A renewal attempt will be made on the next `renew` request. 124 | // - `false` No renewal attempt will be made on the next `renew` 125 | // request. Instead the last good renewal response will be returned 126 | // to the client. 127 | get attemptTokenRenewal() { 128 | return this._attemptTokenRenewal; 129 | } 130 | 131 | // Getter for `isTokenRenewing`. 132 | // - `true` A renewal attempt is in progress. 133 | // - `false` There are no in progress renewal attempts. 134 | get isTokenRenewing() { 135 | return this._isTokenRenewing; 136 | } 137 | 138 | // Settter for `attemptTokenRenewal`. 139 | set attemptTokenRenewal(newAttemptTokenRenewal) { 140 | this._attemptTokenRenewal = newAttemptTokenRenewal; 141 | } 142 | 143 | // Renew the token if `attemptTokenRenewal` is `true`. Otherwise this is a 144 | // no-op so just resolve the promise. 145 | renewIfRequired() { 146 | if (this._attemptTokenRenewal) { 147 | return this._renew(); 148 | } else { 149 | return new Promise((resolve) => { 150 | resolve(); 151 | }); 152 | } 153 | } 154 | 155 | // Start the auto renewal timer. 156 | startAutoRenew(defaultMaxAgeSecs) { 157 | this._autoRenew(defaultMaxAgeSecs || 3600); 158 | } 159 | } 160 | 161 | module.exports = TokenManager; 162 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudant/cloudant", 3 | "description": "Cloudant Node.js client", 4 | "license": "Apache-2.0", 5 | "homepage": "http://github.com/cloudant/nodejs-cloudant", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:cloudant/nodejs-cloudant.git" 9 | }, 10 | "version": "4.5.2-SNAPSHOT", 11 | "author": { 12 | "name": "IBM Cloudant", 13 | "email": "support@cloudant.com" 14 | }, 15 | "contributors": [ 16 | "Glynn Bird " 17 | ], 18 | "keywords": [ 19 | "cloudant", 20 | "couchdb", 21 | "json", 22 | "nosql", 23 | "database" 24 | ], 25 | "dependencies": { 26 | "@types/request": "^2.48.4", 27 | "async": "2.1.2", 28 | "concat-stream": "^1.6.0", 29 | "cookie": "^0.4.0", 30 | "debug": "^3.1.0", 31 | "lockfile": "1.0.3", 32 | "nano": "~8.2.2", 33 | "request": "^2.81.0", 34 | "tmp": "0.0.33" 35 | }, 36 | "devDependencies": { 37 | "dotenv": "2.0.0", 38 | "tslint": "^5", 39 | "eslint": "^4.18.2", 40 | "eslint-config-semistandard": "^11.0.0", 41 | "eslint-config-standard": "^10.2.1", 42 | "eslint-plugin-header": "^1.0.0", 43 | "eslint-plugin-import": "^2.2.0", 44 | "eslint-plugin-node": "^5.1.0", 45 | "eslint-plugin-promise": "^3.5.0", 46 | "eslint-plugin-react": "^7.0.0", 47 | "eslint-plugin-standard": "^3.0.1", 48 | "md5-file": "^3.2.3", 49 | "mocha": "^7.1.2", 50 | "nock": "^13", 51 | "should": "6.0.3", 52 | "typescript": "^2.8.3", 53 | "uuid": "^3.0.1" 54 | }, 55 | "scripts": { 56 | "test": "eslint --ignore-path .eslintignore . && tsc test/typescript/*.ts && mocha && npm run tslint && npm run type-check && npm audit", 57 | "test-verbose": "env DEBUG='*,-mocha:*' npm run test", 58 | "test-live": "NOCK_OFF=true mocha", 59 | "test-live-verbose": "env DEBUG='*,-mocha:*' npm run test-live", 60 | "tslint": "tslint --project types", 61 | "type-check": "tsc --project types/tsconfig.json", 62 | "mocha": "mocha" 63 | }, 64 | "main": "./cloudant.js", 65 | "types": "types", 66 | "engines": { 67 | "node": ">=12" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /plugins/base.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const debug = require('debug')('cloudant:plugins:base'); 17 | const lockFile = require('lockfile'); 18 | const tmp = require('tmp'); 19 | 20 | function noop() {} 21 | 22 | /** 23 | * Cloudant base plugin. 24 | * 25 | * @param {Object} client - HTTP client. 26 | * @param {Object} cfg - Client configuration. 27 | */ 28 | class BasePlugin { 29 | constructor(client, cfg) { 30 | this._client = client; 31 | this._cfg = cfg; 32 | this._disabled = false; 33 | this._lockFile = tmp.tmpNameSync({ postfix: '.lock' }); 34 | } 35 | 36 | get id() { 37 | return this.constructor.id; 38 | } 39 | 40 | get disabled() { 41 | return this._disabled; 42 | } 43 | 44 | // Disable a plugin to prevent all hooks from being executed. 45 | set disabled(disabled) { 46 | this._disabled = disabled; 47 | } 48 | 49 | // NOOP Base Hooks 50 | 51 | onRequest(state, request, callback) { 52 | callback(state); 53 | } 54 | 55 | onResponse(state, response, callback) { 56 | callback(state); 57 | } 58 | 59 | onError(state, error, callback) { 60 | callback(state); 61 | } 62 | 63 | // Helpers 64 | 65 | // Acquire a file lock on the specified path. Release the file lock on 66 | // completion of the callback. 67 | withLock(opts, callback) { 68 | var self = this; 69 | if (typeof opts === 'function') { 70 | callback = opts; 71 | opts = {}; 72 | } 73 | debug('Acquiring lock...'); 74 | lockFile.lock(self._lockFile, opts, function(error) { 75 | if (error) { 76 | callback(error, noop); 77 | } else { 78 | callback(null, function() { 79 | // Close and unlink the file lock. 80 | lockFile.unlock(self._lockFile, function(error) { 81 | if (error) { 82 | debug(`Failed to release lock: ${error}`); 83 | } 84 | }); 85 | }); 86 | } 87 | }); 88 | } 89 | } 90 | 91 | BasePlugin.id = 'base'; 92 | BasePlugin.pluginVersion = 2; 93 | 94 | module.exports = BasePlugin; 95 | -------------------------------------------------------------------------------- /plugins/cookieauth.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, 2021 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const debug = require('debug')('cloudant:plugins:cookieauth'); 17 | const request = require('request'); 18 | const u = require('url'); 19 | 20 | const BasePlugin = require('./base'); 21 | const CookieTokenManager = require('../lib/tokens/CookieTokenManager'); 22 | 23 | /** 24 | * Cookie Authentication plugin. 25 | */ 26 | class CookiePlugin extends BasePlugin { 27 | constructor(client, cfg) { 28 | cfg = Object.assign({ 29 | autoRenew: true, 30 | errorOnNoCreds: true 31 | }, cfg); 32 | 33 | super(client, cfg); 34 | 35 | let sessionUrl = new u.URL(cfg.serverUrl); 36 | sessionUrl.pathname = '/_session'; 37 | if (!sessionUrl.username || !sessionUrl.password) { 38 | if (cfg.errorOnNoCreds) { 39 | throw new Error('Credentials are required for cookie authentication.'); 40 | } 41 | debug('Missing credentials for cookie authentication. Permanently disabling plugin.'); 42 | this.disabled = true; 43 | return; 44 | } 45 | 46 | this._jar = request.jar(); 47 | 48 | this._tokenManager = new CookieTokenManager( 49 | client, 50 | this._jar, 51 | u.format(sessionUrl, {auth: false}), 52 | // Extract creds from URL and decode 53 | decodeURIComponent(sessionUrl.username), 54 | decodeURIComponent(sessionUrl.password) 55 | ); 56 | 57 | if (cfg.autoRenew) { 58 | this._tokenManager.startAutoRenew(cfg.autoRenewDefaultMaxAgeSecs); 59 | } 60 | } 61 | 62 | onRequest(state, req, callback) { 63 | var self = this; 64 | 65 | req.jar = self._jar; 66 | 67 | req.uri = req.uri || req.url; 68 | delete req.url; 69 | req.uri = u.format(new u.URL(req.uri), {auth: false}); 70 | 71 | self._tokenManager.renewIfRequired().catch((error) => { 72 | if (state.attempt < state.maxAttempt) { 73 | state.retry = true; 74 | } else { 75 | state.abortWithResponse = [ error ]; // return error to client 76 | } 77 | }).finally(() => callback(state)); 78 | } 79 | 80 | onResponse(state, response, callback) { 81 | if (response.statusCode === 401) { 82 | debug('Received 401 response. Asking for request retry.'); 83 | state.retry = true; 84 | this._tokenManager.attemptTokenRenewal = true; 85 | } 86 | callback(state); 87 | } 88 | } 89 | 90 | CookiePlugin.id = 'cookieauth'; 91 | 92 | module.exports = CookiePlugin; 93 | -------------------------------------------------------------------------------- /plugins/iamauth.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2021 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const debug = require('debug')('cloudant:plugins:iamauth'); 17 | const request = require('request'); 18 | const u = require('url'); 19 | 20 | const BasePlugin = require('./base.js'); 21 | const IAMTokenManager = require('../lib/tokens/IamTokenManager'); 22 | 23 | /** 24 | * IAM Authentication plugin. 25 | */ 26 | class IAMPlugin extends BasePlugin { 27 | constructor(client, cfg) { 28 | if (typeof cfg.iamApiKey === 'undefined') { 29 | throw new Error('Missing IAM API key from configuration'); 30 | } 31 | 32 | cfg = Object.assign({ 33 | autoRenew: true, 34 | iamTokenUrl: 'https://iam.cloud.ibm.com/identity/token', 35 | retryDelayMsecs: 1000 36 | }, cfg); 37 | 38 | super(client, cfg); 39 | 40 | let sessionUrl = new u.URL(cfg.serverUrl); 41 | sessionUrl.pathname = '/_iam_session'; 42 | 43 | this._jar = request.jar(); 44 | 45 | this._tokenManager = new IAMTokenManager( 46 | client, 47 | this._jar, 48 | u.format(sessionUrl, {auth: false}), 49 | cfg.iamTokenUrl, 50 | cfg.iamApiKey, 51 | cfg.iamClientId, 52 | cfg.iamClientSecret 53 | ); 54 | 55 | if (cfg.autoRenew) { 56 | this._tokenManager.startAutoRenew(); 57 | } 58 | } 59 | 60 | onRequest(state, req, callback) { 61 | var self = this; 62 | 63 | req.jar = self._jar; 64 | 65 | req.uri = req.uri || req.url; 66 | delete req.url; 67 | req.uri = u.format(new u.URL(req.uri), {auth: false}); 68 | 69 | self._tokenManager.renewIfRequired().catch((error) => { 70 | debug(error); 71 | if (state.attempt < state.maxAttempt) { 72 | state.retry = true; 73 | let iamResponse = error.response; 74 | let retryAfterSecs; 75 | if (iamResponse && iamResponse.headers) { 76 | retryAfterSecs = iamResponse.headers['Retry-After']; 77 | } 78 | if (retryAfterSecs) { 79 | state.retryDelayMsecs = retryAfterSecs * 1000; 80 | } else { 81 | state.retryDelayMsecs = self._cfg.retryDelayMsecs; 82 | } 83 | } else { 84 | state.abortWithResponse = [ error ]; // return error to client 85 | } 86 | }).finally(() => callback(state)); 87 | } 88 | 89 | onResponse(state, response, callback) { 90 | if (response.statusCode === 401) { 91 | debug('Received 401 response. Asking for request retry.'); 92 | state.retry = true; 93 | this._tokenManager.attemptTokenRenewal = true; 94 | } 95 | callback(state); 96 | } 97 | 98 | setIamApiKey(newIamApiKey) { 99 | this._tokenManager.setIamApiKey(newIamApiKey); 100 | } 101 | } 102 | 103 | IAMPlugin.id = 'iamauth'; 104 | 105 | module.exports = IAMPlugin; 106 | -------------------------------------------------------------------------------- /plugins/retry.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const BasePlugin = require('./base.js'); 17 | const debug = require('debug')('cloudant:plugins:retry'); 18 | /** 19 | * Retry plugin. 20 | */ 21 | class RetryPlugin extends BasePlugin { 22 | constructor(client, cfg) { 23 | cfg = Object.assign({ 24 | retryDelayMultiplier: 2, 25 | retryErrors: true, 26 | retryInitialDelayMsecs: 500, 27 | retryStatusCodes: [ 28 | 429, // 429 Too Many Requests 29 | 500, // 500 Internal Server Error 30 | 501, // 501 Not Implemented 31 | 502, // 502 Bad Gateway 32 | 503, // 503 Service Unavailable 33 | 504 // 504 Gateway Timeout 34 | ] 35 | }, cfg); 36 | super(client, cfg); 37 | } 38 | 39 | onResponse(state, response, callback) { 40 | if (this._cfg.retryStatusCodes.indexOf(response.statusCode) !== -1) { 41 | debug(`Received status code ${response.statusCode}; setting retry state.`); 42 | state.retry = true; 43 | if (state.attempt === 1) { 44 | state.retryDelayMsecs = this._cfg.retryInitialDelayMsecs; 45 | } else { 46 | state.retryDelayMsecs *= this._cfg.retryDelayMultiplier; 47 | } 48 | debug(`Asking for retry after ${state.retryDelayMsecs}`); 49 | } 50 | callback(state); 51 | } 52 | 53 | onError(state, error, callback) { 54 | if (this._cfg.retryErrors) { 55 | debug(`Received error ${error.code} ${error.message}; setting retry state.`); 56 | state.retry = true; 57 | if (state.attempt === 1) { 58 | state.retryDelayMsecs = this._cfg.retryInitialDelayMsecs; 59 | } else { 60 | state.retryDelayMsecs *= this._cfg.retryDelayMultiplier; 61 | } 62 | debug(`Asking for retry after ${state.retryDelayMsecs}`); 63 | } 64 | callback(state); 65 | } 66 | } 67 | 68 | RetryPlugin.id = 'retry'; 69 | 70 | module.exports = RetryPlugin; 71 | -------------------------------------------------------------------------------- /soak_tests/README.md: -------------------------------------------------------------------------------- 1 | # Soak Tests 2 | These tests are designed to simulate typical production load, over a continuous availability period, to validate system behavior under production use. 3 | 4 | ## Running 5 | The tests run against a Cloudant account, namely `https://$cloudant_username.cloudant.com`. Ensure the `cloudant_username` and `cloudant_password` environment variables have been set. These credentials must have admin privileges as during each test run a new database will be created and subsequently deleted. 6 | 7 | Run the tests from inside the `soak_test` directory, for example: 8 | ```sh 9 | $ export cloudant_username=username 10 | $ export cloudant_password=pa55w0rd01 11 | $ export max_runs=10 12 | $ node soak.js 13 | test:soak Starting test... +2ms 14 | test:soak [TEST RUN] Using database 'nodejs-cloudant-838a0824-a9f3-4fff-825b-e08842ee4a0d'. +10ms 15 | test:soak [TEST RUN] Using database 'nodejs-cloudant-7c49cca6-e916-41b3-a651-ab2a7810f743'. +57s 16 | test:soak [TEST RUN] Using database 'nodejs-cloudant-05a0d613-1de9-4c0e-aca4-a2655695def2'. +52s 17 | test:soak [TEST RUN] Using database 'nodejs-cloudant-31950f8b-a851-4228-bfbf-2cc22bfff888'. +49s 18 | test:soak [TEST RUN] Using database 'nodejs-cloudant-fcfd1378-b594-4552-b26a-d30d2f0457da'. +52s 19 | test:soak [TEST RUN] Using database 'nodejs-cloudant-690a3524-ff37-41e2-858d-577db2fb6d2a'. +54s 20 | test:soak [TEST RUN] Using database 'nodejs-cloudant-d81f9047-cd6f-4ee7-87da-c266182d1cc6'. +51s 21 | test:soak [TEST RUN] Using database 'nodejs-cloudant-b776fc14-b6c6-44ea-a9aa-4dafa4865077'. +53s 22 | test:soak [TEST RUN] Using database 'nodejs-cloudant-4add7454-41d4-4ecf-a5b3-454a823de868'. +51s 23 | test:soak [TEST RUN] Using database 'nodejs-cloudant-0734f2b0-409c-489e-8a17-7745e954d691'. +55s 24 | test:soak All tests passed successfully. +56s 25 | $ echo $? 26 | 0 27 | ``` 28 | Test runs are executed in series by default. You can parallelize execution by setting the `concurrency` environment variable. 29 | By default, a total of 100 test runs will be executed. The `max_runs` environment variable can be set to override this _(shown above)_. 30 | 31 | Each test run _should_ complete in under 60 seconds when executed in series over a "good" network connection (i.e. >20Mb/s download/upload). 32 | 33 | ## The Tests 34 | Each test run performs the following actions: 35 | * `HEAD /` - Ping the root URL. 36 | * `PUT /` - Create a new database. 37 | * `GET /` - Get the database metadata. 38 | * `GET //_security` - Get the database security object. 39 | * `POST //` - Post a new document to the database. 40 | * `GET /` - Get the database metadata. 41 | * `GET //` - Retrieve a document from the database. 42 | * `DELETE //` - Delete a document from the database. 43 | * `POST //_bulk_docs` - Upload multiple documents via the bulk API. 44 | * `GET /` - Get the database metadata. 45 | * `GET //_changes` - Get the database change log and save to a file. 46 | * `GET //` - Retrieve 1,234 documents (using concurrency 10). 47 | * `POST //_bulk_docs` - Upload multiple documents via the bulk API. 48 | * `POST //` - Post 1,234 new documents (using concurrency 10). 49 | * `GET /` - Get the database metadata. 50 | * `DELETE /` - Delete the database. 51 | 52 | Two separate clients are used throughout the test. One uses the builtin `promises` plugin, the other does not. Assertions are made during each test run to ensure all server responses are valid and the database state is as expected. Any assertion failures will cause the tests to terminate early with exit code 1. 53 | -------------------------------------------------------------------------------- /soak_tests/animaldb_bulk_docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": [ 3 | { 4 | "_id": "_design/views101", 5 | "indexes": { 6 | "animals": { 7 | "index": "function(doc){\n index(\"default\", doc._id);\n if(doc.min_length){\n index(\"min_length\", doc.min_length, {\"store\": \"yes\"});\n }\n if(doc.diet){\n index(\"diet\", doc.diet, {\"store\": \"yes\"});\n }\n if (doc.latin_name){\n index(\"latin_name\", doc.latin_name, {\"store\": \"yes\"});\n }\n if (doc['class']){\n index(\"class\", doc['class'], {\"store\": \"yes\"});\n }\n}" 8 | } 9 | }, 10 | "views": { 11 | "latin_name_jssum": { 12 | "map": "function(doc) {\n if(doc.latin_name){\n emit(doc.latin_name, doc.latin_name.length);\n }\n}", 13 | "reduce": "function (key, values, rereduce){\n return sum(values);\n}" 14 | }, 15 | "latin_name": { 16 | "map": "function(doc) {\n if(doc.latin_name){\n emit(doc.latin_name, doc.latin_name.length);\n }\n}" 17 | }, 18 | "diet_sum": { 19 | "map": "function(doc) {\n if(doc.diet){\n emit(doc.diet, 1);\n }\n}", 20 | "reduce": "_sum" 21 | }, 22 | "diet_count": { 23 | "map": "function(doc) {\n if(doc.diet && doc.latin_name){\n emit(doc.diet, doc.latin_name);\n }\n}", 24 | "reduce": "_count" 25 | }, 26 | "complex_count": { 27 | "map": "function(doc){\n if(doc.class && doc.diet){\n emit([doc.class, doc.diet], 1);\n }\n}", 28 | "reduce": "_count" 29 | }, 30 | "diet": { 31 | "map": "function(doc) {\n if(doc.diet){\n emit(doc.diet, 1);\n }\n}" 32 | }, 33 | "complex_latin_name_count": { 34 | "map": "function(doc){\n if(doc.latin_name){\n emit([doc.class, doc.diet, doc.latin_name], doc.latin_name.length)\n }\n}", 35 | "reduce": "_count" 36 | }, 37 | "diet_jscount": { 38 | "map": "function(doc) {\n if(doc.diet){\n emit(doc.diet, 1);\n }\n}", 39 | "reduce": "function (key, values, rereduce){\n return values.length;\n}" 40 | }, 41 | "latin_name_count": { 42 | "map": "function(doc) {\n if(doc.latin_name){\n emit(doc.latin_name, doc.latin_name.length);\n }\n}", 43 | "reduce": "_count" 44 | }, 45 | "latin_name_sum": { 46 | "map": "function(doc) {\n if(doc.latin_name){\n emit(doc.latin_name, doc.latin_name.length);\n }\n}", 47 | "reduce": "_sum" 48 | } 49 | } 50 | }, 51 | { 52 | "min_length": 1, 53 | "min_weight": 40, 54 | "latin_name": "Orycteropus afer", 55 | "diet": "omnivore", 56 | "max_length": 2.2, 57 | "wiki_page": "http://en.wikipedia.org/wiki/Aardvark", 58 | "_id": "aardvark", 59 | "class": "mammal", 60 | "max_weight": 65 61 | }, 62 | { 63 | "min_length": 0.6, 64 | "min_weight": 7, 65 | "latin_name": "Meles meles", 66 | "diet": "omnivore", 67 | "max_length": 0.9, 68 | "wiki_page": "http://en.wikipedia.org/wiki/Badger", 69 | "_id": "badger", 70 | "class": "mammal", 71 | "max_weight": 30 72 | }, 73 | { 74 | "min_length": 3.2, 75 | "min_weight": 4700, 76 | "diet": "herbivore", 77 | "max_length": 4, 78 | "wiki_page": "http://en.wikipedia.org/wiki/African_elephant", 79 | "_id": "elephant", 80 | "class": "mammal", 81 | "max_weight": 6050 82 | }, 83 | { 84 | "min_length": 5, 85 | "min_weight": 830, 86 | "diet": "herbivore", 87 | "max_length": 6, 88 | "wiki_page": "http://en.wikipedia.org/wiki/Giraffe", 89 | "_id": "giraffe", 90 | "class": "mammal", 91 | "max_weight": 1600 92 | }, 93 | { 94 | "min_length": 0.28, 95 | "latin_name": "Dacelo novaeguineae", 96 | "diet": "carnivore", 97 | "max_length": 0.42, 98 | "wiki_page": "http://en.wikipedia.org/wiki/Kookaburra", 99 | "_id": "kookaburra", 100 | "class": "bird" 101 | }, 102 | { 103 | "min_length": 0.95, 104 | "min_weight": 2.2, 105 | "diet": "omnivore", 106 | "max_length": 1.1, 107 | "wiki_page": "http://en.wikipedia.org/wiki/Ring-tailed_lemur", 108 | "_id": "lemur", 109 | "class": "mammal", 110 | "max_weight": 2.2 111 | }, 112 | { 113 | "min_length": 1.7, 114 | "min_weight": 130, 115 | "latin_name": "Lama glama", 116 | "diet": "herbivore", 117 | "max_length": 1.8, 118 | "wiki_page": "http://en.wikipedia.org/wiki/Llama", 119 | "_id": "llama", 120 | "class": "mammal", 121 | "max_weight": 200 122 | }, 123 | { 124 | "min_length": 1.2, 125 | "min_weight": 75, 126 | "diet": "carnivore", 127 | "max_length": 1.8, 128 | "wiki_page": "http://en.wikipedia.org/wiki/Panda", 129 | "_id": "panda", 130 | "class": "mammal", 131 | "max_weight": 115 132 | }, 133 | { 134 | "min_length": 0.25, 135 | "min_weight": 0.08, 136 | "latin_name": "Gallinago gallinago", 137 | "diet": "omnivore", 138 | "max_length": 0.27, 139 | "wiki_page": "http://en.wikipedia.org/wiki/Common_Snipe", 140 | "_id": "snipe", 141 | "class": "bird", 142 | "max_weight": 0.14 143 | }, 144 | { 145 | "min_length": 2, 146 | "min_weight": 175, 147 | "diet": "herbivore", 148 | "max_length": 2.5, 149 | "wiki_page": "http://en.wikipedia.org/wiki/Plains_zebra", 150 | "_id": "zebra", 151 | "class": "mammal", 152 | "max_weight": 387 153 | } 154 | ] 155 | } 156 | -------------------------------------------------------------------------------- /soak_tests/doc.json: -------------------------------------------------------------------------------- 1 | { 2 | "null": null, 3 | "string": "॔욒뼊풉䄊뵽䘎ౙ᧘㵾", 4 | "object": { 5 | "nestedKey": "↟碱䆗跦濥퉤샢즑ﳰ潎菿⁹" 6 | }, 7 | "double": 0.6182668793136843, 8 | "float": 0.16321397, 9 | "long": 7999164547335807000, 10 | "boolean": false, 11 | "loremIpsum": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis a quam id commodo. Morbi vehicula vestibulum est. Donec eu elementum risus. Quisque blandit mauris a risus scelerisque condimentum. In arcu justo, aliquet vitae erat ac, varius consequat enim. Ut facilisis, libero id tristique hendrerit, elit tellus aliquam neque, a molestie dolor elit sit amet nulla. Donec hendrerit justo eget ipsum lobortis, sed luctus tortor malesuada. Quisque tempor pretium odio ac euismod. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vehicula aliquam tempor.\n\nSuspendisse quis diam et enim bibendum rutrum sed ut nisl. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer suscipit mi at orci venenatis pellentesque. Etiam venenatis quam ut ornare molestie. Sed feugiat interdum elementum. Vestibulum finibus risus in risus dignissim, sed venenatis justo maximus. Donec at sem eros. Vestibulum ut tristique lectus. Vestibulum ut ipsum nulla. Duis tristique fermentum rutrum.\n\nVestibulum tortor enim, consequat dapibus odio et, sollicitudin mollis urna. Mauris id sodales elit. Mauris malesuada purus sed sem gravida, tincidunt ornare dui fringilla. Phasellus id augue blandit, viverra enim eu, aliquet nulla. Maecenas sed nisi a elit fermentum scelerisque. Sed a pharetra enim. Cras sit amet rutrum turpis.\n\nUt tempus vehicula quam, non volutpat sapien varius vitae. Maecenas congue suscipit maximus. Proin porttitor, elit ut dapibus rhoncus, ante magna lobortis elit, non pharetra lorem risus ac quam. Maecenas condimentum, ipsum nec viverra condimentum, odio neque ornare est, id convallis dolor enim tempus neque. Ut fringilla erat at accumsan imperdiet. Nullam eget urna metus. Aliquam et leo feugiat, fringilla lacus at, dictum quam. Quisque quam dui, faucibus ac felis quis, sollicitudin ultricies turpis. In et elementum mi. Phasellus sit amet congue nisl, vel placerat nulla.\n\nDonec eu purus tempus, efficitur nibh sed, hendrerit odio. Nulla ut ullamcorper est. Nulla lectus magna, interdum eget vestibulum et, rhoncus a dui. Integer feugiat id massa sed luctus. Cras aliquet turpis a ipsum cursus euismod. Nam tempus massa in dapibus varius. Sed laoreet vestibulum tempus.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis a quam id commodo. Morbi vehicula vestibulum est. Donec eu elementum risus. Quisque blandit mauris a risus scelerisque condimentum. In arcu justo, aliquet vitae erat ac, varius conseq", 12 | "integer": 1951957267, 13 | "array": [ 14 | "Bristol", 15 | "Exeter", 16 | 6 17 | ], 18 | "name": "Chris" 19 | } 20 | -------------------------------------------------------------------------------- /soak_tests/soak.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const async = require('async'); 17 | const Cloudant = require('../cloudant.js'); 18 | const debug = require('debug')('test:soak'); 19 | const fs = require('fs'); 20 | const tmp = require('tmp'); 21 | const uuidv4 = require('uuid/v4'); // random 22 | 23 | const USER = process.env.cloudant_username || 'nodejs'; 24 | const PASSWORD = process.env.cloudant_password || 'sjedon'; 25 | const SERVER = process.env.SERVER_URL || `https://${USER}:${PASSWORD}@${USER}.cloudant.com`; 26 | 27 | const MAX_RUNS = parseInt(process.env.max_runs, 10) || 100; 28 | const CONCURRENCY = parseInt(process.env.concurrency, 10) || 1; 29 | 30 | debug.enabled = true; // enable debugging 31 | 32 | var c1 = new Cloudant({ 33 | url: SERVER, 34 | plugins: [ 'cookieauth', 'promises', 'retry' ] 35 | }); 36 | 37 | var c2 = new Cloudant({ 38 | url: SERVER, 39 | plugins: [ 'cookieauth', 'retry' ] // without 'promises' plugin 40 | }); 41 | 42 | var assert = function(actual, expected, done) { 43 | if (actual !== expected) { 44 | done(new Error(`${actual} is not equal to ${expected}`)); 45 | } 46 | }; 47 | 48 | var runTests = function(done) { 49 | const DBNAME = `nodejs-cloudant-${uuidv4()}`; 50 | const DOCID = `my-doc-${uuidv4()}`; 51 | 52 | debug(`[TEST RUN] Using database '${DBNAME}'.`); 53 | 54 | c1 55 | // HEAD / 56 | .ping() 57 | 58 | .then((d) => { 59 | assert(d.statusCode, 200, done); 60 | 61 | // PUT / 62 | return c1.db.create(DBNAME); 63 | }) 64 | 65 | .then((d) => { 66 | assert(d.statusCode, 201, done); 67 | 68 | // GET / 69 | return c1.db.get(DBNAME); 70 | }) 71 | 72 | .then((d) => { 73 | assert(d.statusCode, 200, done); 74 | assert(d.doc_count, 0, done); 75 | 76 | let db = c1.db.use(DBNAME); 77 | // GET //_security 78 | return db.get_security(); 79 | }) 80 | 81 | .then((d) => { 82 | assert(d.statusCode, 200, done); 83 | assert(Object.keys(d).length, 1, done); 84 | 85 | let db = c1.use(DBNAME); 86 | // POST // 87 | return db.insert({ foo: 'bar' }, DOCID); 88 | }) 89 | 90 | .then((d) => { 91 | assert(d.statusCode, 201, done); 92 | assert(d.id, DOCID, done); 93 | 94 | // GET / 95 | return c1.db.get(DBNAME); 96 | }) 97 | 98 | .then((d) => { 99 | assert(d.statusCode, 200, done); 100 | assert(d.doc_count, 1, done); 101 | 102 | let db = c1.db.use(DBNAME); 103 | // GET // 104 | return db.get(DOCID); 105 | }) 106 | 107 | .then((d) => { 108 | assert(d.statusCode, 200, done); 109 | assert(d._id, DOCID, done); 110 | 111 | let db = c1.db.use(DBNAME); 112 | // DELETE // 113 | return db.destroy(d._id, d._rev); 114 | }) 115 | 116 | .then((d) => { 117 | assert(d.statusCode, 200, done); 118 | assert(d.id, DOCID, done); 119 | 120 | // POST //_bulk_docs 121 | return c1.request({ 122 | path: DBNAME + '/_bulk_docs', 123 | method: 'POST', 124 | headers: { 'Content-Type': 'application/json' }, 125 | body: fs.readFileSync('bulk_docs.json') 126 | }); 127 | }) 128 | 129 | .then((d) => { 130 | assert(d.statusCode, 201, done); 131 | 132 | // GET / 133 | return c1.db.get(DBNAME); 134 | }) 135 | 136 | .then((d) => { 137 | assert(d.statusCode, 200, done); 138 | assert(d.doc_count, 5096, done); 139 | 140 | return new Promise((resolve, reject) => { 141 | async.waterfall([ 142 | (cb) => { 143 | let fName = tmp.fileSync().name; 144 | // GET //_changes 145 | c2.request({ path: DBNAME + '/_changes' }) 146 | .on('error', (e) => { done(e); }) 147 | .pipe(fs.createWriteStream(fName)) 148 | .on('finish', () => { 149 | let results = JSON.parse(fs.readFileSync(fName)).results; 150 | assert(results.length, 5097, done); 151 | cb(null, results); 152 | }); 153 | }, 154 | (results, cb) => { 155 | let db = c2.db.use(DBNAME); 156 | let docIds = []; 157 | results.slice(0, 1234).forEach((result) => { 158 | docIds.push(result.id); 159 | }); 160 | async.eachLimit(docIds, 10, (docId, cb) => { 161 | // GET // 162 | db.get(docId) 163 | .on('error', (e) => { done(e); }) 164 | .on('response', (r) => { 165 | if (docId.startsWith('my-doc-')) { 166 | assert(r.statusCode, 404, done); // doc was deleted 167 | } else { 168 | assert(r.statusCode, 200, done); 169 | } 170 | }) 171 | .on('end', cb); 172 | }, cb); 173 | }, 174 | (cb) => { 175 | fs.createReadStream('animaldb_bulk_docs.json').pipe( 176 | // POST //_bulk_docs 177 | c2.cc.request({ 178 | url: SERVER + '/' + DBNAME + '/_bulk_docs', 179 | method: 'POST', 180 | headers: { 'Content-Type': 'application/json' } 181 | }) 182 | .on('error', (e) => { done(e); }) 183 | .on('response', (r) => { 184 | assert(r.statusCode, 201, done); 185 | }) 186 | .on('end', cb) 187 | ); 188 | }, 189 | (cb) => { 190 | let db = c2.db.use(DBNAME); 191 | let docIds = []; 192 | for (var i = 0; i < 1234; i++) { 193 | docIds.push(`my-new-doc-${i}`); 194 | } 195 | async.eachLimit(docIds, 10, (docId, cb) => { 196 | // POST // 197 | db.insert(fs.readFileSync('doc.json'), docId, (e, b) => { 198 | assert(b.ok, true, done); 199 | cb(); 200 | }); 201 | }, (e) => { cb(e); }); 202 | } 203 | ], 204 | (e) => { 205 | if (e) { 206 | reject(e); 207 | } else { 208 | resolve(); 209 | } 210 | }); 211 | }); 212 | }) 213 | 214 | .then((d) => { 215 | // GET / 216 | return c1.db.get(DBNAME); 217 | }) 218 | 219 | .then((d) => { 220 | assert(d.statusCode, 200, done); 221 | assert(d.doc_count, 6341, done); 222 | 223 | // DELETE / 224 | return c1.db.destroy(DBNAME); 225 | }) 226 | 227 | .then((d) => { 228 | assert(d.statusCode, 200, done); 229 | done(); 230 | }) 231 | 232 | .catch((e) => { done(e); }); 233 | }; 234 | 235 | var tests = []; 236 | for (var i = 0; i < MAX_RUNS; i++) { 237 | tests.push(runTests); 238 | } 239 | 240 | debug('Starting test...'); 241 | 242 | async.parallelLimit(tests, CONCURRENCY, function(e) { 243 | if (e) { 244 | debug(e); 245 | process.exit(1); 246 | } else { 247 | debug('All tests passed successfully.'); 248 | } 249 | }); 250 | -------------------------------------------------------------------------------- /test/clientutils.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global describe it afterEach */ 16 | 'use strict'; 17 | 18 | const assert = require('assert'); 19 | const nock = require('./nock.js'); 20 | const request = require('request'); 21 | const stream = require('stream'); 22 | const testPlugin = require('./fixtures/testplugins.js'); 23 | const utils = require('../lib/clientutils.js'); 24 | 25 | const ME = process.env.cloudant_username || 'nodejs'; 26 | const SERVER = process.env.SERVER_URL || `https://${ME}.cloudant.com`; 27 | 28 | describe('Client Utilities', function() { 29 | afterEach(function() { 30 | if (!process.env.NOCK_OFF) { 31 | nock.cleanAll(); 32 | } 33 | }); 34 | 35 | describe('processState', function() { 36 | it('calls back without error if no response', function(done) { 37 | var r = { clientStream: {}, response: undefined, state: { retry: false } }; 38 | utils.processState(r, function(stop) { 39 | assert.equal(stop, undefined); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('calls back without error if retry', function(done) { 45 | nock(SERVER) 46 | .get('/') 47 | .reply(200, {couchdb: 'Welcome'}); 48 | 49 | var r = { response: request.get(SERVER) }; 50 | r.state = { 51 | abortWithResponse: undefined, 52 | attempt: 1, 53 | maxAttempt: 3, 54 | retry: true, 55 | sending: true 56 | }; 57 | utils.processState(r, function(stop) { 58 | assert.equal(stop, undefined); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('calls back with error if plugin issued abort', function(done) { 64 | var r = { 65 | clientCallback: function(e, r, d) { 66 | assert.equal(e, 'error'); 67 | assert.equal(r, 'response'); 68 | assert.equal(d, 'data'); 69 | }, 70 | clientStream: new stream.PassThrough() 71 | }; 72 | 73 | r.clientStream.on('error', function(e) { 74 | assert.equal(e, 'error'); 75 | }); 76 | r.clientStream.on('response', function(r) { 77 | assert.equal(r, 'response'); 78 | }); 79 | r.clientStream.on('data', function(d) { 80 | assert.equal(d, 'data'); 81 | }); 82 | 83 | r.state = { 84 | abortWithResponse: ['error', 'response', 'data'], 85 | retry: false 86 | }; 87 | utils.processState(r, function(stop) { 88 | assert.equal(stop.message, 'Plugin issued abort'); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('calls back with error if too many retries', function(done) { 94 | nock(SERVER) 95 | .get('/') 96 | .reply(200, {couchdb: 'Welcome'}); 97 | 98 | var r = { abort: false, clientStream: { destinations: [] }, response: request.get(SERVER) }; 99 | r.state = { 100 | abortWithResponse: undefined, 101 | attempt: 3, 102 | cfg: { maxAttempt: 3, retryDelay: 0 }, 103 | retry: true, 104 | sending: true 105 | }; 106 | utils.processState(r, function(stop) { 107 | assert.equal(stop.message, 'No retry requested'); 108 | done(); 109 | }); 110 | }); 111 | 112 | it('calls back with error if no retry', function(done) { 113 | nock(SERVER) 114 | .get('/') 115 | .reply(200, {couchdb: 'Welcome'}); 116 | 117 | var r = { abort: false, clientStream: { destinations: [] }, response: request.get(SERVER) }; 118 | r.state = { 119 | abortWithResponse: undefined, 120 | attempt: 1, 121 | cfg: { maxAttempt: 3, retryDelay: 0 }, 122 | retry: false, 123 | sending: true 124 | }; 125 | utils.processState(r, function(stop) { 126 | assert.equal(stop.message, 'No retry requested'); 127 | done(); 128 | }); 129 | }); 130 | 131 | it('calls back without error if retry and sending false', function(done) { 132 | nock(SERVER) 133 | .get('/') 134 | .reply(200, {couchdb: 'Welcome'}); 135 | 136 | var r = { response: request.get(SERVER) }; 137 | r.state = { 138 | abortWithResponse: undefined, 139 | attempt: 1, 140 | maxAttempt: 3, 141 | retry: true, 142 | sending: false 143 | }; 144 | utils.processState(r, function(stop) { 145 | assert.equal(stop, undefined); 146 | done(); 147 | }); 148 | }); 149 | 150 | it('calls back without error if no retry and sending false', function(done) { 151 | nock(SERVER) 152 | .get('/') 153 | .reply(200, {couchdb: 'Welcome'}); 154 | 155 | var r = { clientStream: {}, response: request.get(SERVER) }; 156 | r.state = { 157 | abortWithResponse: undefined, 158 | attempt: 1, 159 | cfg: { maxAttempt: 3, retryDelay: 0 }, 160 | retry: false, 161 | sending: false 162 | }; 163 | utils.processState(r, function(stop) { 164 | assert.equal(stop, undefined); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('calls back without error if too many retries and sending false', function(done) { 170 | nock(SERVER) 171 | .get('/') 172 | .reply(200, {couchdb: 'Welcome'}); 173 | 174 | var r = { clientStream: {}, response: request.get(SERVER) }; 175 | r.state = { 176 | abortWithResponse: undefined, 177 | attempt: 3, 178 | cfg: { maxAttempt: 3, retryDelay: 0 }, 179 | retry: true, 180 | sending: false 181 | }; 182 | utils.processState(r, function(stop) { 183 | assert.equal(stop, undefined); 184 | assert.equal(r.state.retry, false); 185 | done(); 186 | }); 187 | }); 188 | }); 189 | 190 | describe('runHooks', function() { 191 | it('exits early if no plugins', function(done) { 192 | var r = { plugins: [] }; // no plugins 193 | utils.runHooks('onRequest', r, 'data', function(err) { 194 | done(err); 195 | }); 196 | }); 197 | 198 | it('runs all plugin hooks', function(done) { 199 | var plugin = new testPlugin.NoopPlugin(null, {}); 200 | var r = { plugins: [ plugin ], plugin_stash: { noop: {} } }; 201 | r.state = { 202 | abortWithResponse: undefined, 203 | attempt: 1, 204 | cfg: { maxAttempt: 3, retryDelay: 0 }, 205 | retry: true, 206 | sending: true 207 | }; 208 | utils.runHooks('onRequest', r, 'request', function(err) { 209 | assert.equal(plugin.onRequestCallCount, 1); 210 | assert.equal(plugin.onErrorCallCount, 0); 211 | assert.equal(plugin.onResponseCallCount, 0); 212 | done(err); 213 | }); 214 | }); 215 | 216 | it('noop for missing plugin hook', function(done) { 217 | var plugin = new testPlugin.NoopPlugin(null, {}); 218 | var r = { plugins: [ plugin ] }; 219 | utils.runHooks('someInvalidHookName', r, 'request', function(err) { 220 | assert.equal(plugin.onRequestCallCount, 0); 221 | assert.equal(plugin.onErrorCallCount, 0); 222 | assert.equal(plugin.onResponseCallCount, 0); 223 | done(err); 224 | }); 225 | }); 226 | }); 227 | 228 | describe('wrapCallback', function() { 229 | it('noop for undefined client callback', function() { 230 | var r = { clientCallback: undefined }; // no client callback 231 | assert.equal(utils.wrapCallback(r), undefined); 232 | }); 233 | 234 | it('skips error hooks and executes client callback', function(done) { 235 | nock(SERVER) 236 | .get('/') 237 | .reply(200, {couchdb: 'Welcome'}); 238 | 239 | var plugin = new testPlugin.NoopPlugin(null, {}); 240 | var cb = function(e, r, d) { 241 | assert.equal(e, undefined); 242 | assert.equal(r.statusCode, 200); 243 | assert.ok(d.toString('utf8').indexOf('"couchdb":"Welcome"') > -1); 244 | 245 | assert.equal(plugin.onRequestCallCount, 0); 246 | assert.equal(plugin.onErrorCallCount, 0); 247 | assert.equal(plugin.onResponseCallCount, 0); 248 | 249 | done(); 250 | }; 251 | var r = { 252 | abort: false, 253 | clientCallback: cb, 254 | clientStream: {}, 255 | plugins: [ plugin ], 256 | plugin_stash: { noop: {} } 257 | }; 258 | r.state = { 259 | abortWithResponse: undefined, 260 | attempt: 1, 261 | cfg: { maxAttempt: 3, retryDelay: 0 }, 262 | retry: false, 263 | sending: true 264 | }; 265 | r.response = request.get(SERVER, utils.wrapCallback(r)); 266 | }); 267 | 268 | it('runs plugin error hooks and skips client callback on retry', function(done) { 269 | if (process.env.NOCK_OFF) { 270 | this.skip(); 271 | } 272 | 273 | nock(SERVER) 274 | .get('/') 275 | .replyWithError({code: 'ECONNRESET', message: 'socket hang up'}); 276 | 277 | var plugin = new testPlugin.NoopPlugin(null, {}); 278 | var cb = function(e, r, d) { 279 | assert.fail('Unexpected client callback execution'); 280 | }; 281 | var r = { 282 | clientCallback: cb, 283 | plugins: [ plugin ], 284 | plugin_stash: { noop: {} } 285 | }; 286 | r.state = { 287 | abortWithResponse: undefined, 288 | attempt: 1, 289 | maxAttempt: 3, 290 | retry: true, 291 | sending: true 292 | }; 293 | r.response = request.get(SERVER, utils.wrapCallback(r, function(stop) { 294 | done(stop); 295 | })); 296 | }); 297 | 298 | it('runs plugin error hooks and executes client callback on abort', function(done) { 299 | if (process.env.NOCK_OFF) { 300 | this.skip(); 301 | } 302 | 303 | nock(SERVER) 304 | .get('/') 305 | .replyWithError({code: 'ECONNRESET', message: 'socket hang up'}); 306 | 307 | var plugin = new testPlugin.NoopPlugin(null, {}); 308 | var cb = function(e, r, d) { 309 | assert.equal(e, 'error'); 310 | assert.equal(r, 'response'); 311 | assert.equal(d, 'data'); 312 | 313 | assert.equal(plugin.onRequestCallCount, 0); 314 | assert.equal(plugin.onErrorCallCount, 1); 315 | assert.equal(plugin.onResponseCallCount, 0); 316 | }; 317 | var r = { 318 | clientCallback: cb, 319 | clientStream: new stream.PassThrough(), 320 | plugins: [ plugin ], 321 | plugin_stash: { noop: {} } 322 | }; 323 | 324 | r.clientStream.on('error', function(e) { 325 | assert.equal(e, 'error'); 326 | }); 327 | r.clientStream.on('response', function(r) { 328 | assert.equal(r, 'response'); 329 | }); 330 | r.clientStream.on('data', function(d) { 331 | assert.equal(d, 'data'); 332 | }); 333 | 334 | r.state = { 335 | abortWithResponse: ['error', 'response', 'data'], 336 | retry: false 337 | }; 338 | r.response = request.get(SERVER, utils.wrapCallback(r, function(stop) { 339 | assert.equal(stop.message, 'Plugin issued abort'); 340 | done(); 341 | })); 342 | }); 343 | }); 344 | }); 345 | -------------------------------------------------------------------------------- /test/cloudant.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global describe it before after */ 16 | 'use strict'; 17 | 18 | const assert = require('assert'); 19 | const Client = require('../lib/client.js'); 20 | const Cloudant = require('../cloudant.js'); 21 | const nock = require('./nock.js'); 22 | const uuidv4 = require('uuid/v4'); // random 23 | 24 | const ME = process.env.cloudant_username || 'nodejs'; 25 | const PASSWORD = process.env.cloudant_password || 'sjedon'; 26 | const SERVER = process.env.SERVER_URL || `https://${ME}.cloudant.com`; 27 | const DBNAME = `nodejs-cloudant-${uuidv4()}`; 28 | 29 | const COOKIEAUTH_PLUGIN = [ { cookieauth: { autoRenew: false } } ]; 30 | 31 | describe('Cloudant #db', function() { 32 | before(function(done) { 33 | var mocks = nock(SERVER) 34 | .put(`/${DBNAME}`) 35 | .reply(201, { ok: true }); 36 | 37 | var cloudantClient = new Client({ plugins: 'retry' }); 38 | 39 | var options = { 40 | url: `${SERVER}/${DBNAME}`, 41 | auth: { username: ME, password: PASSWORD }, 42 | method: 'PUT' 43 | }; 44 | cloudantClient.request(options, function(err, resp) { 45 | assert.equal(err, null); 46 | assert.equal(resp.statusCode, 201); 47 | mocks.done(); 48 | done(); 49 | }); 50 | }); 51 | 52 | after(function(done) { 53 | var mocks = nock(SERVER) 54 | .delete(`/${DBNAME}`) 55 | .reply(200, { ok: true }); 56 | 57 | var cloudantClient = new Client({ plugins: 'retry' }); 58 | 59 | var options = { 60 | url: `${SERVER}/${DBNAME}`, 61 | auth: { username: ME, password: PASSWORD }, 62 | method: 'DELETE' 63 | }; 64 | cloudantClient.request(options, function(err, resp) { 65 | assert.equal(err, null); 66 | assert.equal(resp.statusCode, 200); 67 | mocks.done(); 68 | done(); 69 | }); 70 | }); 71 | 72 | describe('set_security', function() { 73 | it('add _reader nobody role', function(done) { 74 | var security = { cloudant: { nobody: [ '_reader' ] } }; 75 | 76 | var mocks = nock(SERVER) 77 | .post('/_session') 78 | .reply(200, { ok: true }) 79 | .put(`/_api/v2/db/${DBNAME}/_security`, security) 80 | .reply(200, { ok: true }) 81 | .get(`/_api/v2/db/${DBNAME}/_security`) 82 | .reply(200, security); 83 | 84 | var cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: COOKIEAUTH_PLUGIN }); 85 | var db = cloudant.db.use(DBNAME); 86 | 87 | db.set_security(security, function(err, result) { 88 | assert.equal(err, null); 89 | assert.ok(result.ok); 90 | 91 | db.get_security(function(err, result) { 92 | assert.equal(err, null); 93 | assert.deepEqual(result, security); 94 | mocks.done(); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | 100 | it('add _writer nobody role (with missing cloudant key)', function(done) { 101 | var role = { nobody: [ '_writer' ] }; // no cloudant key 102 | var security = { cloudant: role }; 103 | 104 | var mocks = nock(SERVER) 105 | .post('/_session') 106 | .reply(200, { ok: true }) 107 | .put(`/_api/v2/db/${DBNAME}/_security`, security) 108 | .reply(200, { ok: true }) 109 | .get(`/_api/v2/db/${DBNAME}/_security`) 110 | .reply(200, security); 111 | 112 | var cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: COOKIEAUTH_PLUGIN }); 113 | var db = cloudant.db.use(DBNAME); 114 | 115 | db.set_security(role, function(err, result) { 116 | assert.equal(err, null); 117 | assert.ok(result.ok); 118 | 119 | db.get_security(function(err, result) { 120 | assert.equal(err, null); 121 | assert.deepEqual(result, security); 122 | mocks.done(); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | 128 | it('set couchdb_auth_only mode', function(done) { 129 | var security = { 130 | couchdb_auth_only: true, 131 | members: { 132 | names: ['member'], 133 | roles: [] 134 | }, 135 | admins: { 136 | names: ['admin'], 137 | roles: [] 138 | } 139 | }; 140 | 141 | var mocks = nock(SERVER) 142 | .post('/_session') 143 | .reply(200, { ok: true }) 144 | .put(`/${DBNAME}/_security`, security) 145 | .reply(200, { ok: true }) 146 | .get(`/_api/v2/db/${DBNAME}/_security`) 147 | .reply(200, security); 148 | 149 | var cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: COOKIEAUTH_PLUGIN }); 150 | var db = cloudant.db.use(DBNAME); 151 | 152 | db.set_security(security, function(err, result) { 153 | assert.equal(err, null); 154 | assert.ok(result.ok); 155 | 156 | db.get_security(function(err, result) { 157 | assert.equal(err, null); 158 | assert.deepEqual(result, security); 159 | mocks.done(); 160 | done(); 161 | }); 162 | }); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/eventrelay.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global describe it */ 16 | 'use strict'; 17 | 18 | const assert = require('assert'); 19 | const events = require('events'); 20 | const stream = require('stream'); 21 | 22 | const EventRelay = require('../lib/eventrelay.js'); 23 | 24 | describe('EventRelay', function() { 25 | it('does not throw errors for an undefined source', function() { 26 | var target = new stream.PassThrough(); 27 | var er = new EventRelay(target); 28 | er.clear(); 29 | er.resume(); 30 | }); 31 | 32 | it('relays all events from source to target', function(done) { 33 | var source = new events.EventEmitter(); 34 | var target = new stream.PassThrough(); 35 | var er = new EventRelay(source, target); 36 | 37 | // send events 38 | var sentEvents = ['one', 'two', 'three']; 39 | sentEvents.forEach(function(e) { 40 | source.emit(e, e); 41 | }); 42 | 43 | // add event handlers 44 | var receivedEvents = []; 45 | target 46 | .on('one', function(x) { 47 | receivedEvents.push(x); 48 | }) 49 | .on('two', function(x) { 50 | receivedEvents.push(x); 51 | }) 52 | .on('three', function(x) { 53 | receivedEvents.push(x); 54 | assert.deepEqual(receivedEvents, sentEvents); 55 | done(); 56 | }); 57 | 58 | er.resume(); // note EventRelay starts in 'paused' mode 59 | }); 60 | 61 | it('allows source to be defined after construction', function(done) { 62 | var source = new events.EventEmitter(); 63 | var target = new stream.PassThrough(); 64 | var er = new EventRelay(target); 65 | 66 | er.setSource(source); 67 | 68 | // send events 69 | var sentEvents = ['one', 'two', 'three']; 70 | sentEvents.forEach(function(e) { 71 | source.emit(e, e); 72 | }); 73 | 74 | // add event handlers 75 | var receivedEvents = []; 76 | target 77 | .on('one', function(x) { 78 | receivedEvents.push(x); 79 | }) 80 | .on('two', function(x) { 81 | receivedEvents.push(x); 82 | }) 83 | .on('three', function(x) { 84 | receivedEvents.push(x); 85 | assert.deepEqual(receivedEvents, sentEvents); 86 | done(); 87 | }); 88 | 89 | er.resume(); // note EventRelay starts in 'paused' mode 90 | }); 91 | 92 | it('clears events and only relays new events from source to target', function(done) { 93 | var source = new events.EventEmitter(); 94 | var target = new stream.PassThrough(); 95 | var er = new EventRelay(source, target); 96 | 97 | // send events 98 | ['one', 'two', 'three'].forEach(function(e) { 99 | source.emit(e, e); 100 | }); 101 | 102 | er.clear(); 103 | 104 | // send more events 105 | var sentEvents = ['four', 'five', 'six']; 106 | sentEvents.forEach(function(e) { 107 | source.emit(e, e); 108 | }); 109 | 110 | // add event handlers 111 | var receivedEvents = []; 112 | target 113 | .on('four', function(x) { 114 | receivedEvents.push(x); 115 | }) 116 | .on('five', function(x) { 117 | receivedEvents.push(x); 118 | }) 119 | .on('six', function(x) { 120 | receivedEvents.push(x); 121 | assert.deepEqual(receivedEvents, sentEvents); 122 | done(); 123 | }); 124 | 125 | er.resume(); // note EventRelay starts in 'paused' mode 126 | }); 127 | 128 | it('relays request events from source to target', function(done) { 129 | var source = new events.EventEmitter(); 130 | var target = new stream.PassThrough(); 131 | var er = new EventRelay(source, target); 132 | 133 | source.emit('request', {url: 'http://localhost:5986'}); 134 | source.emit('socket', {encrypted: true}); 135 | source.emit('response', {statusCode: 123}); 136 | 137 | // data 138 | var sentData = ['foo', 'bar', 'baz']; 139 | sentData.forEach(function(d) { 140 | source.emit('data', d); 141 | }); 142 | 143 | source.emit('end'); 144 | 145 | var data = []; 146 | var seenRequestEvent = false; 147 | var seenSocketEvent = false; 148 | var seenResponseEvent = false; 149 | 150 | // add event handlers 151 | target 152 | .on('request', function(req) { 153 | seenRequestEvent = true; 154 | assert.equal(req.url, 'http://localhost:5986'); 155 | }) 156 | .on('socket', function(socket) { 157 | seenSocketEvent = true; 158 | assert.ok(socket.encrypted); 159 | }) 160 | .on('response', function(resp) { 161 | seenResponseEvent = true; 162 | assert.equal(resp.statusCode, 123); 163 | }) 164 | .on('pipe', function(src) { 165 | assert.fail('Unexpected "pipe" event received.'); 166 | }) 167 | .on('data', function(d) { 168 | data.push(d.toString('utf8')); 169 | }) 170 | .on('end', function() { 171 | assert.ok(seenRequestEvent); 172 | assert.ok(seenSocketEvent); 173 | assert.ok(seenResponseEvent); 174 | assert.deepEqual(data, sentData); 175 | done(); 176 | }); 177 | 178 | er.resume(); // note EventRelay starts in 'paused' mode 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /test/fixtures/testplugin.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const BasePlugin = require('../../plugins/base.js'); 17 | 18 | class TestPlugin extends BasePlugin { 19 | } 20 | 21 | TestPlugin.id = 'test'; 22 | 23 | module.exports = TestPlugin; 24 | -------------------------------------------------------------------------------- /test/fixtures/testplugins.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2017, 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | const assert = require('assert'); 17 | const BasePlugin = require('../../plugins/base.js'); 18 | 19 | const ME = process.env.cloudant_username || 'nodejs'; 20 | const SERVER = process.env.SERVER_URL || `https://${ME}.cloudant.com`; 21 | 22 | // NoopPlugin for testing 23 | 24 | class NoopPlugin extends BasePlugin { 25 | constructor(client, cfg) { 26 | super(client, cfg); 27 | this.onRequestCallCount = 0; 28 | this.onErrorCallCount = 0; 29 | this.onResponseCallCount = 0; 30 | } 31 | 32 | onRequest(state, request, callback) { 33 | this.onRequestCallCount++; 34 | callback(state); // noop 35 | } 36 | 37 | onError(state, error, callback) { 38 | this.onErrorCallCount++; 39 | callback(state); // noop 40 | } 41 | 42 | onResponse(state, response, callback) { 43 | this.onResponseCallCount++; 44 | callback(state); // noop 45 | } 46 | } 47 | 48 | NoopPlugin.id = 'noop'; 49 | 50 | class NoopPlugin1 extends NoopPlugin {} 51 | NoopPlugin1.id = 'noop1'; 52 | 53 | class NoopPlugin2 extends NoopPlugin {} 54 | NoopPlugin2.id = 'noop2'; 55 | 56 | class NoopPlugin3 extends NoopPlugin {} 57 | NoopPlugin3.id = 'noop3'; 58 | 59 | // AlwaysRetry for testing 60 | // - onRequest: noop 61 | // - onError: always retries (with retry delay) 62 | // - onResponse: always retries (with retry delay) 63 | 64 | class AlwaysRetry extends BasePlugin { 65 | constructor(client, cfg) { 66 | super(client, cfg); 67 | this.onRequestCallCount = 0; 68 | this.onErrorCallCount = 0; 69 | this.onResponseCallCount = 0; 70 | } 71 | 72 | onError(state, error, callback) { 73 | this.onErrorCallCount++; 74 | state.retry = true; 75 | if (state.attempt === 1) { 76 | state.retryDelayMsecs = 500; 77 | } else { 78 | state.retryDelayMsecs *= 2; 79 | } 80 | callback(state); 81 | } 82 | 83 | onResponse(state, response, callback) { 84 | this.onResponseCallCount++; 85 | state.retry = true; 86 | if (state.attempt === 1) { 87 | state.retryDelayMsecs = 500; 88 | } else { 89 | state.retryDelayMsecs *= 2; 90 | } 91 | callback(state); 92 | } 93 | } 94 | 95 | AlwaysRetry.id = 'alwaysretry'; 96 | 97 | // ComplexPlugin1 for testing 98 | // - onRequest: sets method to 'PUT' 99 | // adds 'ComplexPlugin1: foo' header 100 | // - onError: always retries 101 | // - onResponse: always retries with retry delay 102 | 103 | class ComplexPlugin1 extends BasePlugin { 104 | constructor(client, cfg) { 105 | super(client, cfg); 106 | this.onRequestCallCount = 0; 107 | this.onErrorCallCount = 0; 108 | this.onResponseCallCount = 0; 109 | } 110 | 111 | onRequest(state, request, callback) { 112 | this.onRequestCallCount++; 113 | request.method = 'PUT'; 114 | request.headers = { 'ComplexPlugin1': 'foo' }; 115 | callback(state); 116 | } 117 | 118 | onError(state, error, callback) { 119 | this.onErrorCallCount++; 120 | state.retry = true; 121 | callback(state); 122 | } 123 | 124 | onResponse(state, response, callback) { 125 | this.onResponseCallCount++; 126 | state.retry = true; 127 | if (state.attempt === 1) { 128 | state.retryDelayMsecs = 10; 129 | } else { 130 | state.retryDelayMsecs *= 2; 131 | } 132 | callback(state); 133 | } 134 | } 135 | 136 | ComplexPlugin1.id = 'complexplugin1'; 137 | 138 | // ComplexPlugin2 for testing 139 | // - onRequest: sets method to 'GET' 140 | // adds 'ComplexPlugin2: bar' header 141 | // - onError: submits GET /bar and returns response 142 | // - onResponse: retries 401 responses once 143 | 144 | class ComplexPlugin2 extends BasePlugin { 145 | constructor(client, cfg) { 146 | super(client, cfg); 147 | this.onRequestCallCount = 0; 148 | this.onErrorCallCount = 0; 149 | this.onResponseCallCount = 0; 150 | } 151 | 152 | onRequest(state, request, callback) { 153 | this.onRequestCallCount++; 154 | request.method = 'GET'; 155 | request.headers = { 'ComplexPlugin2': 'bar' }; 156 | callback(state); 157 | } 158 | 159 | onError(state, error, callback) { 160 | this.onErrorCallCount++; 161 | this._client(SERVER + '/bar', function(error, response, body) { 162 | state.abortWithResponse = [error, response, body]; 163 | callback(state); 164 | }); 165 | } 166 | 167 | onResponse(state, response, callback) { 168 | this.onResponseCallCount++; 169 | if (state.attempt < state.maxAttempt) { 170 | if (state.attempt === 1 && response.statusCode === 401) { 171 | state.retry = true; 172 | } 173 | } 174 | callback(state); 175 | } 176 | } 177 | 178 | ComplexPlugin2.id = 'complexplugin2'; 179 | 180 | // ComplexPlugin3 for testing 181 | // - onResponse: retries 5xx responses once, submitting a DELETE /bar on failure 182 | 183 | class ComplexPlugin3 extends BasePlugin { 184 | constructor(client, cfg) { 185 | super(client, cfg); 186 | this.onResponseCallCount = 0; 187 | } 188 | 189 | onResponse(state, response, callback) { 190 | this.onResponseCallCount++; 191 | if (response.statusCode < 500) { 192 | return callback(state); 193 | } 194 | if (state.attempt === 1) { 195 | state.retry = true; 196 | callback(state); 197 | } else { 198 | var options = { url: SERVER + '/bar', method: 'DELETE' }; 199 | this._client(options, function(error, response, body) { 200 | state.abortWithResponse = [error, response, body]; 201 | callback(state); 202 | }); 203 | } 204 | } 205 | } 206 | 207 | ComplexPlugin3.id = 'complexplugin3'; 208 | 209 | // PluginA for testing 210 | // - attempt 1 211 | // * retryDelayMsecs set to 10 212 | // - attempt 2 213 | // * retryDelayMsecs set to 100 214 | // - attempt 3 215 | // * retryDelayMsecs set to 1000 216 | 217 | class PluginA extends BasePlugin { 218 | _hook(state, callback) { 219 | switch (state.attempt) { 220 | case 1: 221 | assert.equal(state.retry, false); 222 | assert.equal(state.retryDelayMsecs, 0); 223 | assert.equal(typeof state.stash.foo, 'undefined'); 224 | state.retry = true; 225 | state.retryDelayMsecs = 10; 226 | state.stash.foo = 'pluginA -- this hook has been called once'; 227 | break; 228 | case 2: 229 | assert.equal(state.retry, false); 230 | assert.equal(state.retryDelayMsecs, 40); 231 | assert.equal(state.stash.foo, 'pluginA -- this hook has been called once'); 232 | state.retryDelayMsecs = 100; 233 | state.stash.foo = 'pluginA -- this hook has been called twice'; 234 | break; 235 | case 3: 236 | assert.equal(state.retry, false); 237 | assert.equal(state.retryDelayMsecs, 400); 238 | assert.equal(state.stash.foo, 'pluginA -- this hook has been called twice'); 239 | state.retryDelayMsecs = 1000; 240 | break; 241 | default: 242 | assert.fail('Too many attempts'); 243 | } 244 | callback(state); 245 | } 246 | 247 | onError(state, error, callback) { 248 | this._hook(state, callback); 249 | } 250 | 251 | onResponse(state, response, callback) { 252 | this._hook(state, callback); 253 | } 254 | } 255 | 256 | PluginA.id = 'pluginA'; 257 | 258 | // PluginB for testing 259 | // - attempt 1 260 | // * retryDelayMsecs set to 20 261 | // - attempt 2 262 | // * retryDelayMsecs set to 200 263 | // - attempt 3 264 | // * retryDelayMsecs set to 2000 265 | 266 | class PluginB extends BasePlugin { 267 | _hook(state, callback) { 268 | switch (state.attempt) { 269 | case 1: 270 | assert.equal(state.retry, true); 271 | assert.equal(state.retryDelayMsecs, 10); 272 | assert.equal(typeof state.stash.foo, 'undefined'); 273 | state.retryDelayMsecs = 20; 274 | state.stash.foo = 'pluginB -- this hook has been called once'; 275 | break; 276 | case 2: 277 | assert.equal(state.retry, false); 278 | assert.equal(state.retryDelayMsecs, 100); 279 | assert.equal(state.stash.foo, 'pluginB -- this hook has been called once'); 280 | state.retry = true; 281 | state.retryDelayMsecs = 200; 282 | state.stash.foo = 'pluginB -- this hook has been called twice'; 283 | break; 284 | case 3: 285 | assert.equal(state.retry, false); 286 | assert.equal(state.retryDelayMsecs, 1000); 287 | assert.equal(state.stash.foo, 'pluginB -- this hook has been called twice'); 288 | state.retryDelayMsecs = 2000; 289 | break; 290 | default: 291 | assert.fail('Too many attempts'); 292 | } 293 | callback(state); 294 | } 295 | 296 | onError(state, error, callback) { 297 | this._hook(state, callback); 298 | } 299 | 300 | onResponse(state, response, callback) { 301 | this._hook(state, callback); 302 | } 303 | } 304 | 305 | PluginB.id = 'pluginB'; 306 | 307 | // PluginC for testing 308 | // - attempt 1 309 | // * retryDelayMsecs set to 30 310 | // - attempt 2 311 | // * retryDelayMsecs set to 300 312 | // - attempt 3 313 | // * retryDelayMsecs set to 3000 314 | 315 | class PluginC extends BasePlugin { 316 | _hook(state, callback) { 317 | switch (state.attempt) { 318 | case 1: 319 | assert.equal(state.retry, true); 320 | assert.equal(state.retryDelayMsecs, 20); 321 | assert.equal(typeof state.stash.foo, 'undefined'); 322 | state.retryDelayMsecs = 30; 323 | state.stash.foo = 'pluginC -- this hook has been called once'; 324 | break; 325 | case 2: 326 | assert.equal(state.retry, true); 327 | assert.equal(state.retryDelayMsecs, 200); 328 | assert.equal(state.stash.foo, 'pluginC -- this hook has been called once'); 329 | state.retryDelayMsecs = 300; 330 | state.stash.foo = 'pluginC -- this hook has been called twice'; 331 | break; 332 | case 3: 333 | assert.equal(state.retry, false); 334 | assert.equal(state.retryDelayMsecs, 2000); 335 | assert.equal(state.stash.foo, 'pluginC -- this hook has been called twice'); 336 | state.retry = true; 337 | state.retryDelayMsecs = 3000; 338 | break; 339 | default: 340 | assert.fail('Too many attempts'); 341 | } 342 | callback(state); 343 | } 344 | 345 | onError(state, error, callback) { 346 | this._hook(state, callback); 347 | } 348 | 349 | onResponse(state, response, callback) { 350 | this._hook(state, callback); 351 | } 352 | } 353 | 354 | PluginC.id = 'pluginC'; 355 | 356 | // PluginD for testing 357 | // - attempt 1 358 | // * retryDelayMsecs set to 40 359 | // - attempt 2 360 | // * retryDelayMsecs set to 400 361 | // - attempt 3 362 | // * 363 | 364 | class PluginD extends BasePlugin { 365 | _hook(state, callback) { 366 | switch (state.attempt) { 367 | case 1: 368 | assert.equal(state.retry, true); 369 | assert.equal(state.retryDelayMsecs, 30); 370 | assert.equal(typeof state.stash.foo, 'undefined'); 371 | state.retryDelayMsecs = 40; 372 | state.stash.foo = 'pluginD -- this hook has been called once'; 373 | break; 374 | case 2: 375 | assert.equal(state.retry, true); 376 | assert.equal(state.retryDelayMsecs, 300); 377 | assert.equal(state.stash.foo, 'pluginD -- this hook has been called once'); 378 | state.retryDelayMsecs = 400; 379 | state.stash.foo = 'pluginD -- this hook has been called twice'; 380 | break; 381 | case 3: 382 | assert.equal(state.retry, true); 383 | assert.equal(state.retryDelayMsecs, 3000); 384 | assert.equal(state.stash.foo, 'pluginD -- this hook has been called twice'); 385 | break; 386 | default: 387 | assert.fail('Too many attempts'); 388 | } 389 | callback(state); 390 | } 391 | 392 | onError(state, error, callback) { 393 | this._hook(state, callback); 394 | } 395 | 396 | onResponse(state, response, callback) { 397 | this._hook(state, callback); 398 | } 399 | } 400 | 401 | PluginD.id = 'pluginD'; 402 | 403 | module.exports = { 404 | NoopPlugin: NoopPlugin, 405 | NoopPlugin1: NoopPlugin1, 406 | NoopPlugin2: NoopPlugin2, 407 | NoopPlugin3: NoopPlugin3, 408 | AlwaysRetry: AlwaysRetry, 409 | ComplexPlugin1: ComplexPlugin1, 410 | ComplexPlugin2: ComplexPlugin2, 411 | ComplexPlugin3: ComplexPlugin3, 412 | PluginA: PluginA, 413 | PluginB: PluginB, 414 | PluginC: PluginC, 415 | PluginD: PluginD 416 | }; 417 | -------------------------------------------------------------------------------- /test/issues/292.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global describe it before after */ 16 | 'use strict'; 17 | 18 | const assert = require('assert'); 19 | const Client = require('../../lib/client.js'); 20 | const Cloudant = require('../../cloudant.js'); 21 | const nock = require('../nock.js'); 22 | const uuidv4 = require('uuid/v4'); // random 23 | 24 | const ME = process.env.cloudant_username || 'nodejs'; 25 | const PASSWORD = process.env.cloudant_password || 'sjedon'; 26 | const SERVER = process.env.SERVER_URL || `https://${ME}.cloudant.com`; 27 | const DBNAME = `nodejs-cloudant-${uuidv4()}`; 28 | 29 | const COOKIEAUTH_PLUGIN = [ { cookieauth: { autoRenew: false } } ]; 30 | 31 | describe('#db Issue #292', function() { 32 | before(function(done) { 33 | var mocks = nock(SERVER) 34 | .put(`/${DBNAME}`) 35 | .reply(201, { ok: true }); 36 | 37 | var cloudantClient = new Client({ plugins: 'retry' }); 38 | 39 | var options = { 40 | url: `${SERVER}/${DBNAME}`, 41 | auth: { username: ME, password: PASSWORD }, 42 | method: 'PUT' 43 | }; 44 | cloudantClient.request(options, function(err, resp) { 45 | assert.equal(err, null); 46 | assert.equal(resp.statusCode, 201); 47 | mocks.done(); 48 | done(); 49 | }); 50 | }); 51 | 52 | after(function(done) { 53 | var mocks = nock(SERVER) 54 | .delete(`/${DBNAME}`) 55 | .reply(200, { ok: true }); 56 | 57 | var cloudantClient = new Client({ plugins: 'retry' }); 58 | 59 | var options = { 60 | url: `${SERVER}/${DBNAME}`, 61 | auth: { username: ME, password: PASSWORD }, 62 | method: 'DELETE' 63 | }; 64 | cloudantClient.request(options, function(err, resp) { 65 | assert.equal(err, null); 66 | assert.equal(resp.statusCode, 200); 67 | mocks.done(); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('lists all query indices', function(done) { 73 | var mocks = nock(SERVER) 74 | .post('/_session') 75 | .reply(200, { ok: true }) 76 | .get(`/${DBNAME}/_index`) 77 | .reply(200, { total_rows: 1, indexes: [ { name: '_all_docs' } ] }); 78 | 79 | var cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: COOKIEAUTH_PLUGIN }); 80 | var db = cloudant.db.use(DBNAME); 81 | 82 | db.index().then((d) => { 83 | assert.equal(d.total_rows, 1); 84 | mocks.done(); 85 | done(); 86 | }) 87 | .catch((err) => { assert.fail(`Unexpected error: ${err}`); }); 88 | }); 89 | 90 | it('creates new query index', function(done) { 91 | var definition = { 92 | index: { fields: [ 'foo' ] }, 93 | name: 'foo-index', 94 | type: 'json' 95 | }; 96 | 97 | var mocks = nock(SERVER) 98 | .post('/_session') 99 | .reply(200, { ok: true }) 100 | .post(`/${DBNAME}/_index`, definition) 101 | .reply(200, { result: 'created' }); 102 | 103 | var cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: COOKIEAUTH_PLUGIN }); 104 | var db = cloudant.db.use(DBNAME); 105 | 106 | db.index(definition).then((d) => { 107 | assert.equal(d.result, 'created'); 108 | mocks.done(); 109 | done(); 110 | }) 111 | .catch((err) => { assert.fail(`Unexpected error: ${err}`); }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /test/legacy/plugin.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, 2019 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global describe it before after */ 16 | 'use strict'; 17 | 18 | // Cloudant client API tests 19 | require('dotenv').config({silent: true}); 20 | 21 | var fs = require('fs'); 22 | var should = require('should'); 23 | var assert = require('assert'); 24 | var uuid = require('uuid/v4'); 25 | 26 | var nock = require('../nock.js'); 27 | var Cloudant = require('../../cloudant.js'); 28 | var request = require('request'); 29 | var PassThroughDuplex = require('../../lib/passthroughduplex.js'); 30 | 31 | // These globals may potentially be parameterized. 32 | var ME = process.env.cloudant_username || 'nodejs'; 33 | var PASSWORD = process.env.cloudant_password || 'sjedon'; 34 | var SERVER = process.env.SERVER_URL || 'https://' + ME + '.cloudant.com'; 35 | var SERVER_NO_PROTOCOL = SERVER.replace(/^https?:\/\//, ''); 36 | var SERVER_WITH_CREDS = `https://${ME}:${PASSWORD}@${SERVER_NO_PROTOCOL}`; 37 | 38 | const COOKIEAUTH_PLUGIN = [ { cookieauth: { autoRenew: false } } ]; 39 | 40 | var dbName; 41 | var mydb = null; 42 | var cc = null; 43 | var ddoc = null; 44 | var viewname = null; 45 | 46 | // hooks 47 | 48 | var onBefore = function(done) { 49 | const unique = uuid(); 50 | dbName = 'nodejs_cloudant_test_' + unique; 51 | var mocks = nock(SERVER) 52 | .put('/' + dbName).reply(200, { 'ok': true }) 53 | .put('/' + dbName + '/mydoc').reply(200, { id: 'mydoc', rev: '1-1' }); 54 | 55 | cc = Cloudant({url: SERVER, username: ME, password: PASSWORD, plugins: 'retry'}); 56 | cc.db.create(dbName, function(er, d) { 57 | should(er).equal(null); 58 | d.should.be.an.Object; 59 | d.should.have.a.property('ok'); 60 | d.ok.should.be.equal(true); 61 | mydb = cc.db.use(dbName); 62 | 63 | // add a doc 64 | mydb.insert({ foo: true }, 'mydoc', function(er, d) { 65 | should(er).equal(null); 66 | d.should.be.an.Object; 67 | d.should.have.a.property('id'); 68 | d.should.have.a.property('rev'); 69 | mocks.done(); 70 | done(); 71 | }); 72 | }); 73 | }; 74 | 75 | var onAfter = function(done) { 76 | var mocks = nock(SERVER) 77 | .delete('/' + dbName).reply(200, { 'ok': true }); 78 | 79 | cc.db.destroy(dbName, function(er, d) { 80 | should(er).equal(null); 81 | d.should.be.an.Object; 82 | d.should.have.a.property('ok'); 83 | d.ok.should.be.equal(true); 84 | mydb = null; 85 | cc = null; 86 | 87 | mocks.done(); 88 | done(); 89 | }); 90 | }; 91 | 92 | describe('retry-on-429 plugin #db', function() { 93 | before(onBefore); 94 | after(onAfter); 95 | 96 | it('behave normally too', function(done) { 97 | var mocks = nock(SERVER) 98 | if (typeof(process.env.NOCK_OFF) === 'undefined') { 99 | mocks.persist().get('/' + dbName).reply(200, {}); 100 | } 101 | var cloudant = Cloudant({plugins: 'retry', url: SERVER, username: ME, password: PASSWORD}); 102 | cloudant.cc._addPlugins('retry'); // retry socket hang up errors 103 | var db = cloudant.db.use(dbName); 104 | this.timeout(10000); 105 | db.info(function(err, data) { 106 | assert.equal(err, null); 107 | data.should.be.an.Object; 108 | done(); 109 | }); 110 | }); 111 | 112 | it('allow no callback', function(done) { 113 | var mocks = nock(SERVER).get('/' + dbName).reply(200, {}); 114 | var cloudant = Cloudant({plugins: 'retry', url: SERVER, username: ME, password: PASSWORD}); 115 | var db = cloudant.db.use(dbName); 116 | this.timeout(10000); 117 | db.info(); 118 | setTimeout(done, 1000); 119 | }); 120 | 121 | it('should return a stream', function(done) { 122 | var mocks = nock(SERVER) 123 | .get('/_all_dbs').reply(200, ['_replicator','_users']); 124 | var cloudant = Cloudant({plugins: 'retry', url: SERVER, username: ME, password: PASSWORD}); 125 | var dbs = cloudant.db.listAsStream() 126 | .once('end', function() { 127 | done() 128 | }); 129 | assert.equal(dbs instanceof PassThroughDuplex, true); 130 | }); 131 | 132 | it('should return a promise', function(done) { 133 | var mocks = nock(SERVER) 134 | .get('/' + dbName).reply(200, { ok: true }); 135 | var cloudant = Cloudant({plugins: 'retry', url: SERVER, username: ME, password: PASSWORD}); 136 | var db = cloudant.db.use(dbName); 137 | var p = db.info().then(function() { 138 | done(); 139 | }); 140 | assert.equal(p instanceof Promise, true); 141 | }); 142 | }); 143 | 144 | describe('promises #db', function() { 145 | before(onBefore); 146 | after(onAfter); 147 | 148 | it('should return a promise', function(done) { 149 | var mocks = nock(SERVER) 150 | .get('/' + dbName).reply(200, { ok: true }); 151 | var cloudant = Cloudant({url: SERVER, username: ME, password: PASSWORD, plugins: []}); 152 | var db = cloudant.db.use(dbName); 153 | var p = db.info().then(function(data) { 154 | data.should.be.an.Object; 155 | done(); 156 | }); 157 | assert.equal(p instanceof Promise, true); 158 | }); 159 | 160 | it('should return an error status code', function(done) { 161 | var mocks = nock(SERVER) 162 | .get('/somedbthatdoesntexist').reply(404, { ok: false }); 163 | var cloudant = Cloudant({url: SERVER, username: ME, password: PASSWORD, plugins: []}); 164 | var db = cloudant.db.use('somedbthatdoesntexist'); 165 | var p = db.info().then(function(data) { 166 | assert(false); 167 | }).catch(function(e) { 168 | e.should.be.an.Object; 169 | e.should.have.property.statusCode; 170 | e.statusCode.should.be.a.Number; 171 | e.statusCode.should.equal(404); 172 | done(); 173 | }); 174 | assert.equal(p instanceof Promise, true); 175 | }); 176 | }); 177 | 178 | describe('custom plugin #db', function() { 179 | before(onBefore); 180 | after(onAfter); 181 | 182 | var defaultPlugin = function(opts, callback) { 183 | return request(opts, callback); 184 | }; 185 | 186 | it('should allow custom plugins', function(done) { 187 | var mocks = nock(SERVER) 188 | .get('/') 189 | .reply(200, { couchdb: 'Welcome', version: '1.0.2', cloudant_build: '2488' }); 190 | 191 | var cloudant = Cloudant({ plugins: defaultPlugin, url: SERVER, username: ME, password: PASSWORD }); 192 | cloudant.ping(function(err, data) { 193 | assert.equal(err, null); 194 | assert.equal(data.couchdb, 'Welcome'); 195 | mocks.done(); 196 | done(); 197 | }); 198 | }); 199 | 200 | var defaultPlugin2 = function(opts, callback) { 201 | return request(opts, callback); 202 | }; 203 | 204 | it('errors if multiple custom plugins are specified', function() { 205 | assert.throws( 206 | () => { 207 | Cloudant({ plugins: [ defaultPlugin, defaultPlugin2 ], account: ME, password: PASSWORD }); 208 | }, 209 | /Using multiple legacy plugins is not permitted/, 210 | 'did not throw with expected message' 211 | ); 212 | }); 213 | 214 | it('should allow custom plugins using asynchronous instantiation', function(done) { 215 | var mocks = nock(SERVER) 216 | .post('/_session', { name: ME, password: PASSWORD }) 217 | .reply(200, { ok: true }) 218 | .get('/') 219 | .reply(200, { couchdb: 'Welcome' }); 220 | 221 | Cloudant({ 222 | creds: { outUrl: SERVER_WITH_CREDS }, 223 | plugins: defaultPlugin, 224 | url: SERVER, 225 | username: ME, 226 | password: PASSWORD 227 | }, function(err, nano, pong) { 228 | assert.equal(err, null); 229 | assert.notEqual(nano, null); 230 | assert.equal(pong.couchdb, 'Welcome'); 231 | mocks.done(); 232 | done(); 233 | }); 234 | }); 235 | }); 236 | 237 | describe('cookieauth plugin #db', function() { 238 | before(onBefore); 239 | after(onAfter); 240 | 241 | it('should return a promise', function(done) { 242 | var mocks = nock(SERVER) 243 | .post('/_session').reply(200, { ok: true }) 244 | .get('/' + dbName).reply(200, { ok: true }); 245 | var cloudant = Cloudant({plugins: COOKIEAUTH_PLUGIN, url: SERVER, username: ME, password: PASSWORD}); 246 | var db = cloudant.db.use(dbName); 247 | var p = db.info().then(function(data) { 248 | data.should.be.an.Object; 249 | // check that we use all the nocked API calls 250 | mocks.done(); 251 | done(); 252 | }); 253 | assert.equal(p instanceof Promise, true); 254 | }); 255 | 256 | it('should authenticate before attempting API call', function(done) { 257 | var mocks = nock(SERVER) 258 | .post('/_session', {name: ME, password: PASSWORD}).reply(200, { ok: true, info: {}, userCtx: { name: ME, roles: ['_admin'] } }) 259 | .get('/' + dbName + '/mydoc').reply(200, { _id: 'mydoc', _rev: '1-123', ok: true }); 260 | var cloudant = Cloudant({plugins: COOKIEAUTH_PLUGIN, url: SERVER, username: ME, password: PASSWORD}); 261 | var db = cloudant.db.use(dbName); 262 | var p = db.get('mydoc', function(err, data) { 263 | assert.equal(err, null); 264 | data.should.be.an.Object; 265 | data.should.have.property._id; 266 | data.should.have.property._rev; 267 | data.should.have.property.ok; 268 | 269 | // check that we use all the nocked API calls 270 | mocks.done(); 271 | done(); 272 | }); 273 | }); 274 | 275 | it('should only authenticate once', function(done) { 276 | var mocks = nock(SERVER) 277 | .post('/_session', {name: ME, password: PASSWORD}).reply(200, { ok: true, info: {}, userCtx: { name: ME, roles: ['_admin'] } }, { 'Set-Cookie': 'AuthSession=xyz; Version=1; Path=/; HttpOnly' }) 278 | .get('/' + dbName + '/mydoc').reply(200, { _id: 'mydoc', _rev: '1-123', ok: true }) 279 | .get('/' + dbName + '/mydoc').reply(200, { _id: 'mydoc', _rev: '1-123', ok: true }); 280 | var cloudant = Cloudant({plugins: COOKIEAUTH_PLUGIN, url: SERVER, username: ME, password: PASSWORD}); 281 | var db = cloudant.db.use(dbName); 282 | var p = db.get('mydoc', function(err, data) { 283 | assert.equal(err, null); 284 | data.should.be.an.Object; 285 | data.should.have.property._id; 286 | data.should.have.property._rev; 287 | data.should.have.property.ok; 288 | 289 | db.get('mydoc', function(err, data) { 290 | assert.equal(err, null); 291 | data.should.be.an.Object; 292 | data.should.have.property._id; 293 | data.should.have.property._rev; 294 | data.should.have.property.ok; 295 | 296 | // check that we use all the nocked API calls 297 | mocks.done(); 298 | done(); 299 | }); 300 | }); 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /test/legacy/y.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | -webkit-text-size-adjust: 100%; 4 | -ms-text-size-adjust: 100%; 5 | } 6 | body { 7 | margin: 0; 8 | } 9 | article, 10 | aside, 11 | details, 12 | figcaption, 13 | figure, 14 | footer, 15 | header, 16 | hgroup, 17 | main, 18 | menu, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | audio, 25 | canvas, 26 | progress, 27 | video { 28 | display: inline-block; 29 | vertical-align: baseline; 30 | } 31 | audio:not([controls]) { 32 | display: none; 33 | height: 0; 34 | } 35 | [hidden], 36 | template { 37 | display: none; 38 | } 39 | a { 40 | background-color: transparent; 41 | } 42 | a:active, 43 | a:hover { 44 | outline: 0; 45 | } 46 | abbr[title] { 47 | border-bottom: 1px dotted; 48 | } 49 | b, 50 | strong { 51 | font-weight: bold; 52 | } 53 | dfn { 54 | font-style: italic; 55 | } 56 | h1 { 57 | margin: .67em 0; 58 | font-size: 2em; 59 | } 60 | mark { 61 | color: #000; 62 | background: #ff0; 63 | } 64 | small { 65 | font-size: 80%; 66 | } 67 | sub, 68 | sup { 69 | position: relative; 70 | font-size: 75%; 71 | line-height: 0; 72 | vertical-align: baseline; 73 | } 74 | sup { 75 | top: -.5em; 76 | } 77 | sub { 78 | bottom: -.25em; 79 | } 80 | img { 81 | border: 0; 82 | } 83 | svg:not(:root) { 84 | overflow: hidden; 85 | } 86 | figure { 87 | margin: 1em 40px; 88 | } 89 | hr { 90 | height: 0; 91 | -webkit-box-sizing: content-box; 92 | -moz-box-sizing: content-box; 93 | box-sizing: content-box; 94 | } 95 | 96 | -------------------------------------------------------------------------------- /test/legacy/y.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudant/nodejs-cloudant/22c37b922f36d5dde4f7efa20b80f388823b7de9/test/legacy/y.css.gz -------------------------------------------------------------------------------- /test/nock.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, 2017 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | function noop() { 17 | return this; 18 | } 19 | 20 | function nockNoop() { 21 | // Return a completely inert nock-compatible object. 22 | return { 23 | 'delete': noop, 24 | done: noop, 25 | filteringPath: noop, 26 | get: noop, 27 | head: noop, 28 | post: noop, 29 | put: noop, 30 | query: noop, 31 | reply: noop, 32 | replyWithFile: noop 33 | }; 34 | } 35 | 36 | var nock; 37 | if (process.env.NOCK_OFF) { 38 | nock = nockNoop; 39 | } else { 40 | nock = require('nock'); 41 | } 42 | 43 | module.exports = nock; 44 | -------------------------------------------------------------------------------- /test/partitioned_databases.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global describe it before after */ 16 | 'use strict'; 17 | 18 | const assert = require('assert'); 19 | const async = require('async'); 20 | const Cloudant = require('../cloudant.js'); 21 | const nock = require('./nock.js'); 22 | const uuidv4 = require('uuid/v4'); // random 23 | 24 | const ME = process.env.cloudant_username || 'nodejs'; 25 | const PASSWORD = process.env.cloudant_password || 'sjedon'; 26 | const SERVER = process.env.SERVER_URL || `https://${ME}.cloudant.com`; 27 | const DBNAME = `nodejs-cloudant-${uuidv4()}`; 28 | 29 | describe('Partitioned Databases #db', () => { 30 | const partitionKeys = Array.apply(null, {length: 10}) 31 | .map(() => { return uuidv4(); }); 32 | 33 | before(() => { 34 | var mocks = nock(SERVER) 35 | .put(`/${DBNAME}`) 36 | .query({ partitioned: true }) 37 | .reply(201, { ok: true }); 38 | 39 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 40 | return cloudant.db.create(DBNAME, { partitioned: true }).then((body) => { 41 | assert.ok(body.ok); 42 | mocks.done(); 43 | }); 44 | }); 45 | 46 | after(() => { 47 | var mocks = nock(SERVER) 48 | .delete(`/${DBNAME}`) 49 | .reply(200, { ok: true }); 50 | 51 | var cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 52 | return cloudant.db.destroy(DBNAME).then((body) => { 53 | assert.ok(body.ok); 54 | mocks.done(); 55 | }); 56 | }); 57 | 58 | it('created a partitioned database', () => { 59 | var mocks = nock(SERVER) 60 | .get(`/${DBNAME}`) 61 | .reply(200, { props: { partitioned: true } }); 62 | 63 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 64 | return cloudant.db.get(DBNAME).then((body) => { 65 | assert.ok(body.props.partitioned); 66 | mocks.done(); 67 | }); 68 | }); 69 | 70 | it('create some partitioned documents', function(done) { 71 | if (!process.env.NOCK_OFF) { 72 | this.skip(); 73 | } 74 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 75 | const db = cloudant.db.use(DBNAME); 76 | 77 | var q = async.queue(function(task, callback) { 78 | db.bulk({ 'docs': task.docs }).then(callback).catch(done); 79 | }, 10); 80 | q.drain = done; 81 | 82 | for (let i in partitionKeys) { 83 | let docs = []; 84 | for (let j = 0; j < 10; j++) { 85 | docs.push({ _id: `${partitionKeys[i]}:doc${j}`, foo: 'bar' }); 86 | } 87 | q.push({ 'docs': docs }); 88 | } 89 | }); 90 | 91 | it('get partition information', () => { 92 | const pKey = partitionKeys[0]; 93 | 94 | var mocks = nock(SERVER) 95 | .get(`/${DBNAME}/_partition/${pKey}`) 96 | .reply(200, { partition: pKey, doc_count: 10 }); 97 | 98 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 99 | const db = cloudant.db.use(DBNAME); 100 | return db.partitionInfo(pKey).then((body) => { 101 | assert.equal(body.partition, pKey); 102 | assert.equal(body.doc_count, 10); 103 | mocks.done(); 104 | }); 105 | }); 106 | 107 | it('get all documents in a partition', () => { 108 | const pKey = partitionKeys[0]; 109 | 110 | var mocks = nock(SERVER) 111 | .get(`/${DBNAME}/_partition/${pKey}/_all_docs`) 112 | .reply(200, { rows: new Array(10) }); 113 | 114 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 115 | const db = cloudant.db.use(DBNAME); 116 | return db.partitionedList(pKey).then((body) => { 117 | assert.equal(body.rows.length, 10); 118 | mocks.done(); 119 | }); 120 | }); 121 | 122 | describe('Partitioned Query', () => { 123 | before(() => { 124 | if (!process.env.NOCK_OFF) { 125 | return; 126 | } 127 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 128 | const db = cloudant.db.use(DBNAME); 129 | return db.createIndex({ index: { fields: ['foo'] } }).then((body) => { 130 | assert.equal(body.result, 'created'); 131 | }); 132 | }); 133 | 134 | it('query a partitioned query', () => { 135 | const pKey = partitionKeys[0]; 136 | const selector = { selector: { foo: { $eq: 'bar' } } }; 137 | 138 | var mocks = nock(SERVER) 139 | .post(`/${DBNAME}/_partition/${pKey}/_find`, selector) 140 | .reply(200, { docs: new Array(10) }); 141 | 142 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 143 | const db = cloudant.db.use(DBNAME); 144 | return db.partitionedFind(pKey, selector).then((body) => { 145 | assert(body.docs.length, 10); 146 | mocks.done(); 147 | }); 148 | }); 149 | }); 150 | 151 | describe('Partitioned Search', () => { 152 | before(() => { 153 | if (!process.env.NOCK_OFF) { 154 | return; 155 | } 156 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 157 | const db = cloudant.db.use(DBNAME); 158 | return db.insert({ 159 | _id: '_design/mysearch', 160 | options: { partitioned: true }, 161 | indexes: { 162 | search1: { 163 | index: 'function(doc) { index("id", doc._id, {"store": true}); }' 164 | } 165 | } 166 | }).then((body) => { 167 | assert.ok(body.ok); 168 | }); 169 | }); 170 | 171 | it('query a partitioned search', () => { 172 | const pKey = partitionKeys[0]; 173 | 174 | var mocks = nock(SERVER) 175 | .post(`/${DBNAME}/_partition/${pKey}/_design/mysearch/_search/search1`, 176 | { q: '*:*' }) 177 | .reply(200, { rows: new Array(10) }); 178 | 179 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 180 | const db = cloudant.db.use(DBNAME); 181 | return db.partitionedSearch(pKey, 'mysearch', 'search1', { q: '*:*' }).then((body) => { 182 | assert(body.rows.length, 10); 183 | mocks.done(); 184 | }); 185 | }); 186 | }); 187 | 188 | describe('Partitioned View', () => { 189 | before(() => { 190 | if (!process.env.NOCK_OFF) { 191 | return; 192 | } 193 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 194 | const db = cloudant.db.use(DBNAME); 195 | return db.insert({ 196 | _id: '_design/myview', 197 | options: { partitioned: true }, 198 | views: { view1: { map: 'function(doc) { emit(doc._id, 1); }' } } 199 | }).then((body) => { 200 | assert.ok(body.ok); 201 | }); 202 | }); 203 | 204 | it('query a partitioned view', () => { 205 | const pKey = partitionKeys[0]; 206 | 207 | var mocks = nock(SERVER) 208 | .get(`/${DBNAME}/_partition/${pKey}/_design/myview/_view/view1`) 209 | .reply(200, { rows: new Array(10) }); 210 | 211 | const cloudant = Cloudant({ url: SERVER, username: ME, password: PASSWORD, plugins: [] }); 212 | const db = cloudant.db.use(DBNAME); 213 | return db.partitionedView(pKey, 'myview', 'view1').then((body) => { 214 | assert(body.rows.length, 10); 215 | mocks.done(); 216 | }); 217 | }); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /test/passthroughduplex.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global describe it */ 16 | 'use strict'; 17 | 18 | const assert = require('assert'); 19 | const stream = require('stream'); 20 | 21 | const PassThroughDuplex = require('../lib/passthroughduplex.js'); 22 | 23 | describe('Pass Through Duplex', function() { 24 | it('can write to internal writable', function(done) { 25 | var duplex = new PassThroughDuplex(); 26 | var readable = new stream.Readable(); 27 | readable._read = function noop() {}; 28 | 29 | readable.pipe(duplex); 30 | 31 | var data = ['data', 'some more data', 'even more data']; 32 | data.forEach(function(d) { 33 | readable.push(d); 34 | }); 35 | readable.push(null); 36 | 37 | var chunks = []; 38 | duplex.passThroughWritable.on('data', function(chunk) { 39 | chunks.push(chunk.toString()); 40 | }); 41 | 42 | duplex.passThroughWritable.on('end', function() { 43 | assert.deepEqual(chunks, data); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('can read from internal readable', function(done) { 49 | var duplex = new PassThroughDuplex(); 50 | var readable = new stream.Readable(); 51 | readable._read = function noop() {}; 52 | 53 | readable.pipe(duplex.passThroughReadable); 54 | 55 | var data = ['data', 'some more data', 'even more data']; 56 | data.forEach(function(d) { 57 | readable.push(d); 58 | }); 59 | readable.push(null); 60 | 61 | var chunks = []; 62 | duplex.passThroughReadable.on('data', function(chunk) { 63 | chunks.push(chunk.toString()); 64 | }); 65 | 66 | duplex.passThroughReadable.on('end', function() { 67 | assert.deepEqual(chunks, data); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('captures pipe event', function(done) { 73 | var duplex = new PassThroughDuplex(); 74 | var readable = new stream.Readable(); 75 | 76 | readable._read = function noop() { 77 | this.push(null); 78 | }; 79 | 80 | readable.pipe(duplex.passThroughReadable); 81 | 82 | duplex.on('pipe', function() { 83 | done(); 84 | }); 85 | 86 | readable.pipe(duplex); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/stream.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global describe it before after */ 16 | 'use strict'; 17 | 18 | const assert = require('assert'); 19 | const Client = require('../lib/client.js'); 20 | const fs = require('fs'); 21 | const md5File = require('md5-file'); 22 | const nock = require('./nock.js'); 23 | const stream = require('stream'); 24 | const uuidv4 = require('uuid/v4'); // random 25 | 26 | const ME = process.env.cloudant_username || 'nodejs'; 27 | const PASSWORD = process.env.cloudant_password || 'sjedon'; 28 | const SERVER = process.env.SERVER_URL || `https://${ME}.cloudant.com`; 29 | const DBNAME = `/nodejs-cloudant-${uuidv4()}`; 30 | 31 | describe('#db Stream', function() { 32 | before(function(done) { 33 | var mocks = nock(SERVER) 34 | .put(DBNAME) 35 | .reply(201, {ok: true}); 36 | 37 | var cloudantClient = new Client({ plugins: 'retry' }); 38 | 39 | var options = { 40 | url: SERVER + DBNAME, 41 | auth: { username: ME, password: PASSWORD }, 42 | method: 'PUT' 43 | }; 44 | cloudantClient.request(options, function(err, resp) { 45 | assert.equal(err, null); 46 | assert.equal(resp.statusCode, 201); 47 | mocks.done(); 48 | done(); 49 | }); 50 | }); 51 | 52 | after(function(done) { 53 | var mocks = nock(SERVER) 54 | .delete(DBNAME) 55 | .reply(200, {ok: true}); 56 | 57 | var cloudantClient = new Client({ plugins: 'retry' }); 58 | 59 | var options = { 60 | url: SERVER + DBNAME, 61 | auth: { username: ME, password: PASSWORD }, 62 | method: 'DELETE' 63 | }; 64 | cloudantClient.request(options, function(err, resp) { 65 | assert.equal(err, null); 66 | assert.equal(resp.statusCode, 200); 67 | mocks.done(); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('bulk documents to database', function(done) { 73 | var mocks = nock(SERVER) 74 | .post(DBNAME + '/_bulk_docs') 75 | .reply(201, { ok: true }) 76 | .get(DBNAME) 77 | .reply(200, { doc_count: 5096 }); 78 | 79 | var cloudantClient = new Client({ plugins: 'retry' }); 80 | 81 | var options = { 82 | url: SERVER + DBNAME + '/_bulk_docs', 83 | auth: { username: ME, password: PASSWORD }, 84 | method: 'POST', 85 | headers: { 'Content-Type': 'application/json' } 86 | }; 87 | var req = cloudantClient.request(options, function(err, resp, data) { 88 | assert.equal(err, null); 89 | assert.equal(resp.statusCode, 201); 90 | 91 | var options = { 92 | url: SERVER + DBNAME, 93 | auth: { username: ME, password: PASSWORD }, 94 | method: 'GET' 95 | }; 96 | cloudantClient.request(options, function(err, resp, data) { 97 | assert.equal(err, null); 98 | assert.equal(resp.statusCode, 200); 99 | assert.ok(data.indexOf('"doc_count":5096') > -1); 100 | 101 | mocks.done(); 102 | done(); 103 | }); 104 | }); 105 | 106 | fs.createReadStream('test/fixtures/bulk_docs.json') 107 | .pipe(new stream.PassThrough({ highWaterMark: 1000 })).pipe(req); 108 | }); 109 | 110 | it('all documents to file', function(done) { 111 | var mocks = nock(SERVER) 112 | .get(DBNAME + '/_all_docs') 113 | .query({ include_docs: true }) 114 | .reply(200, fs.readFileSync('test/fixtures/all_docs_include_docs.json')); 115 | 116 | var cloudantClient = new Client({ plugins: 'retry' }); 117 | 118 | var options = { 119 | url: SERVER + DBNAME + '/_all_docs', 120 | qs: { include_docs: true }, 121 | auth: { username: ME, password: PASSWORD }, 122 | method: 'GET' 123 | }; 124 | var req = cloudantClient.request(options, function(err, resp, data) { 125 | assert.equal(err, null); 126 | assert.equal(resp.statusCode, 200); 127 | }); 128 | 129 | var results = fs.createWriteStream('data.json'); 130 | req.pipe(new stream.PassThrough({ highWaterMark: 1000 })).pipe(results) 131 | .on('finish', function() { 132 | assert.equal(md5File.sync('data.json'), md5File.sync('test/fixtures/all_docs_include_docs.json')); 133 | fs.unlinkSync('data.json'); 134 | mocks.done(); 135 | done(); 136 | }); 137 | }); 138 | 139 | it('all documents to file (when piping inside response handler)', function(done) { 140 | var mocks = nock(SERVER) 141 | .get(DBNAME + '/_all_docs') 142 | .query({ include_docs: true }) 143 | .reply(200, fs.readFileSync('test/fixtures/all_docs_include_docs.json')); 144 | 145 | var cloudantClient = new Client({ plugins: 'retry' }); 146 | 147 | var options = { 148 | url: SERVER + DBNAME + '/_all_docs', 149 | qs: { include_docs: true }, 150 | auth: { username: ME, password: PASSWORD }, 151 | method: 'GET' 152 | }; 153 | var req = cloudantClient.request(options, function(err, resp, data) { 154 | assert.equal(err, null); 155 | assert.equal(resp.statusCode, 200); 156 | }) 157 | .on('response', function(resp) { 158 | if (resp.statusCode !== 200) { 159 | assert.fail(`Failed to GET /_all_docs. Status code: ${resp.statusCode}`); 160 | } else { 161 | req 162 | .pipe(new stream.PassThrough({ highWaterMark: 1000 })) 163 | .pipe(fs.createWriteStream('data.json')) 164 | .on('finish', function() { 165 | assert.equal(md5File.sync('data.json'), md5File.sync('test/fixtures/all_docs_include_docs.json')); 166 | fs.unlinkSync('data.json'); 167 | mocks.done(); 168 | done(); 169 | }); 170 | } 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /test/tokens/TokenManager.js: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global describe it */ 16 | 'use strict'; 17 | 18 | const assert = require('assert'); 19 | 20 | const TokenManager = require('../../lib/tokens/TokenManager'); 21 | 22 | class TokenManagerRenewSuccess extends TokenManager { 23 | constructor() { 24 | super(); 25 | this._getTokenCallCount = 0; 26 | this._cookieHeader = 'Max-Age=1'; 27 | } 28 | 29 | _getToken(callback) { 30 | this._getTokenCallCount += 1; 31 | setTimeout(() => { 32 | callback(null, { headers: { 'set-cookie': [ this._cookieHeader ] } }); 33 | }, 100); 34 | } 35 | 36 | // mock successful token renewal 37 | get getTokenCallCount() { 38 | return this._getTokenCallCount; 39 | } 40 | 41 | get cookieHeader() { 42 | return this._cookieHeader; 43 | } 44 | 45 | set cookieHeader(cookieHeader) { 46 | this._cookieHeader = cookieHeader; 47 | } 48 | } 49 | 50 | class TokenManagerRenewFailure extends TokenManager { 51 | constructor() { 52 | super(); 53 | this._getTokenCallCount = 0; 54 | } 55 | 56 | // mock failed token renewal 57 | _getToken(callback) { 58 | this._getTokenCallCount += 1; 59 | setTimeout(() => { 60 | callback(new Error('err'), {ok: false}); 61 | }, 100); 62 | } 63 | 64 | get getTokenCallCount() { 65 | return this._getTokenCallCount; 66 | } 67 | } 68 | 69 | describe('Token Manger', (done) => { 70 | it('renews the token successfully', (done) => { 71 | let t = new TokenManagerRenewSuccess(); 72 | t.renewIfRequired().then(() => { 73 | assert.equal(t.getTokenCallCount, 1); 74 | done(); 75 | }).catch(done); 76 | assert.ok(t.isTokenRenewing); 77 | }); 78 | 79 | it('handles a token renewal failure', (done) => { 80 | let t = new TokenManagerRenewFailure(); 81 | t.renewIfRequired().then(() => { 82 | assert.fail('Unexpected success.'); 83 | }).catch((error) => { 84 | assert.equal(t.getTokenCallCount, 1); 85 | assert.equal(error.message, 'err'); 86 | assert.equal(error.response.ok, false); 87 | done(); 88 | }); 89 | assert.ok(t.isTokenRenewing); 90 | }); 91 | 92 | it('correctly auto renews token', (done) => { 93 | let t = new TokenManagerRenewSuccess(); 94 | t.startAutoRenew(); 95 | setTimeout(() => { 96 | // one renew every 0.5 seconds 97 | assert.equal(t.getTokenCallCount, 4); 98 | done(); 99 | }, 2000); 100 | }); 101 | 102 | it('correctly auto renews token in the absence of a cookie Max-Age', (done) => { 103 | let t = new TokenManagerRenewSuccess(); 104 | t.cookieHeader = ''; 105 | t.startAutoRenew(2); 106 | setTimeout(() => { 107 | // one renew every 1 seconds 108 | assert.equal(t.getTokenCallCount, 2); 109 | done(); 110 | }, 2000); 111 | }); 112 | 113 | it('only makes one renewal request', (done) => { 114 | let t = new TokenManagerRenewSuccess(); 115 | let renewalCount = 0; 116 | let lim = 10000; 117 | 118 | for (let i = 1; i < lim + 1; i++) { 119 | if (i === lim) { 120 | t.renewIfRequired().then(() => { 121 | renewalCount += 1; 122 | assert.equal(renewalCount, lim); 123 | assert.equal(t.getTokenCallCount, 1); 124 | done(); 125 | }).catch(done); 126 | } else { 127 | t.renewIfRequired().then(() => { 128 | renewalCount += 1; 129 | }).catch(done); 130 | } 131 | } 132 | assert.ok(t.isTokenRenewing); 133 | }); 134 | 135 | it('makes another renewal only after setting force renew', (done) => { 136 | let t = new TokenManagerRenewSuccess(); 137 | // renew 1 - make request 138 | t.renewIfRequired().then(() => { 139 | assert.equal(t.getTokenCallCount, 1); 140 | // renew 2 - return last good response 141 | t.renewIfRequired().then(() => { 142 | assert.equal(t.getTokenCallCount, 1); 143 | t.attemptTokenRenewal = true; 144 | // renew 3 - make request 145 | t.renewIfRequired().then(() => { 146 | assert.equal(t.getTokenCallCount, 2); 147 | done(); 148 | }); 149 | }); 150 | }).catch(done); 151 | assert.ok(t.isTokenRenewing); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /test/typescript/cloudant.ts: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Ensure Cloudant type declaration files can be imported without error. 16 | 17 | export import cloudant = require('../../types/index'); 18 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright © 2018, 2019 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as nano from 'nano'; 16 | import { CoreOptions, Request } from 'request'; 17 | 18 | declare function cloudant( 19 | config: cloudant.Configuration | string, 20 | callback?: (error: any, client?: cloudant.ServerScope, pong?: any) => void 21 | ): cloudant.ServerScope; 22 | 23 | declare namespace cloudant { 24 | type Callback = (error: any, response: R, headers?: any) => void; 25 | 26 | interface ApiKey { 27 | key: string; 28 | password: string; 29 | } 30 | 31 | interface Configuration { 32 | account?: string; 33 | password?: string; 34 | vcapInstanceName?: string; 35 | vcapServices?: string; 36 | vcapServiceName?: string; 37 | url?: string; 38 | cookie?: string; 39 | requestDefaults?: CoreOptions; 40 | log?(id: string, args: any): void; 41 | parseUrl?: boolean; 42 | request?(params: any): void; 43 | plugins?: any; 44 | maxAttempt?: number; 45 | } 46 | 47 | interface CORS { 48 | enable_cors: boolean; 49 | allow_credentials: boolean; 50 | origins: string[]; 51 | } 52 | 53 | interface DatabasePartitionInfo { 54 | db_name: string; 55 | sizes: { 56 | active: number; 57 | external: number; 58 | }; 59 | partition: string; 60 | doc_count: number; 61 | doc_del_count: number; 62 | } 63 | 64 | interface GeoParams { 65 | include_docs?: boolean; 66 | bookmark?: string; 67 | format?: string; 68 | limit?: number; 69 | skip?: number; 70 | stale?: string; 71 | bbox?: number[]; 72 | lat?: number; 73 | lon?: number; 74 | rangex?: number; 75 | rangey?: number; 76 | radius?: number; 77 | g?: any; 78 | relation?: string; 79 | nearest?: boolean; 80 | } 81 | 82 | interface GeoResult { 83 | bookmark: string; 84 | features: any[]; 85 | row: any[]; 86 | type: string; 87 | } 88 | 89 | interface Query { 90 | // https://console.bluemix.net/docs/services/Cloudant/api/cloudant_query.html#query 91 | (definition?: any, callback?: Callback): Promise; 92 | 93 | // https://console.bluemix.net/docs/services/Cloudant/api/cloudant_query.html#deleting-an-index 94 | del(spec: QueryDeleteSpec, callback?: Callback): Promise; 95 | } 96 | 97 | interface QueryDeleteSpec { 98 | ddoc: string; 99 | name: string; 100 | } 101 | 102 | interface Security { 103 | [key: string]: any; 104 | } 105 | 106 | // Server Scope 107 | // ============ 108 | 109 | interface ServerScope extends nano.ServerScope { 110 | db: nano.DatabaseScope; 111 | use(db: string): DocumentScope; 112 | scope(db: string): DocumentScope; 113 | 114 | // https://console.bluemix.net/docs/services/Cloudant/api/authorization.html#api-keys 115 | generate_api_key(callback?: Callback): Promise; 116 | 117 | // https://console.bluemix.net/docs/services/Cloudant/api/cors.html#reading-the-cors-configuration 118 | get_cors(callback?: Callback): Promise; 119 | 120 | // https://console.bluemix.net/docs/services/Cloudant/api/account.html#ping 121 | ping(callback?: Callback): Promise; 122 | 123 | // https://console.bluemix.net/docs/services/Cloudant/api/cors.html#setting-the-cors-configuration 124 | set_cors(cors: CORS, callback?: Callback): Promise; 125 | } 126 | 127 | // Document Scope 128 | // ============== 129 | 130 | interface DocumentScope extends nano.DocumentScope { 131 | // https://console.bluemix.net/docs/services/Cloudant/api/cloudant_query.html 132 | index: Query; 133 | 134 | // https://console.bluemix.net/docs/services/Cloudant/api/document.html#the-_bulk_get-endpoint 135 | bulk_get(options: nano.BulkModifyDocsWrapper, callback?: Callback): Promise; 136 | 137 | // https://console.bluemix.net/docs/services/Cloudant/api/cloudant-geo.html#cloudant-geospatial 138 | geo( 139 | designname: string, 140 | docname: string, 141 | params: GeoParams, 142 | callback?: Callback 143 | ): Promise; 144 | 145 | // https://console.bluemix.net/docs/services/Cloudant/api/authorization.html#viewing-permissions 146 | get_security(callback?: Callback): Promise; 147 | 148 | // https://console.bluemix.net/docs/services/Cloudant/api/authorization.html#modifying-permissions 149 | set_security(Security: Security, callback?: Callback): Promise; 150 | 151 | 152 | // Partitioned Databases 153 | // --------------------- 154 | 155 | // https://cloud.ibm.com/docs/services/Cloudant/guides?topic=cloudant-database-partitioning 156 | 157 | partitionInfo( 158 | partitionKey: string, 159 | callback?: Callback 160 | ): Promise; 161 | 162 | partitionedFind( 163 | partitionKey: string, 164 | selector: nano.MangoQuery, 165 | callback?: Callback> 166 | ): Promise>; 167 | 168 | partitionedFindAsStream( 169 | partitionKey: string, 170 | selector: nano.MangoQuery 171 | ): Request; 172 | 173 | partitionedList( 174 | partitionKey: string, 175 | params: nano.DocumentFetchParams, 176 | callback?: Callback> 177 | ): Promise>; 178 | 179 | partitionedListAsStream( 180 | partitionKey: string, 181 | params: nano.DocumentFetchParams 182 | ): Request; 183 | 184 | partitionedSearch( 185 | partitionKey: string, 186 | designname: string, 187 | searchname: string, 188 | params: nano.DocumentSearchParams, 189 | callback?: Callback> 190 | ): Promise>; 191 | 192 | partitionedSearchAsStream( 193 | partitionKey: string, 194 | designname: string, 195 | searchname: string, 196 | params: nano.DocumentSearchParams 197 | ): Request; 198 | 199 | partitionedView( 200 | partitionKey: string, 201 | designname: string, 202 | viewname: string, 203 | params: nano.DocumentViewParams, 204 | callback?: Callback> 205 | ): Promise>; 206 | 207 | partitionedViewAsStream( 208 | partitionKey: string, 209 | designname: string, 210 | viewname: string, 211 | params: nano.DocumentViewParams 212 | ): Request; 213 | } 214 | 215 | // types for plugins 216 | 217 | interface PluginConfig {} 218 | interface PluginState { 219 | maxAttempt: number; 220 | } 221 | 222 | type PluginCallbackFunction = (state: PluginState) => void; 223 | 224 | interface PluginRequest { 225 | method: 'get' | 'post' | 'options'; 226 | headers: {}, 227 | uri: string; 228 | } 229 | 230 | interface PluginPostRequest extends PluginRequest { 231 | method: 'post'; 232 | body?: string | {}; 233 | } 234 | 235 | interface PluginResponse { 236 | request: PluginRequest; 237 | headers: {}; 238 | statusCode: number; 239 | } 240 | 241 | export class BasePlugin { 242 | public static pluginVersion: number; 243 | public static id: string 244 | public disabled: boolean; 245 | public _cfg: PluginConfig; 246 | public constructor(client: nano.DocumentScope<{}>, config?: PluginConfig) 247 | public onRequest(state: PluginState, request: PluginRequest, callback: PluginCallbackFunction): void 248 | public onResponse(state: PluginState, response: PluginResponse, callback: PluginCallbackFunction): void 249 | public onError(state: PluginState, error: Error, callback: PluginCallbackFunction): void 250 | } 251 | } 252 | 253 | export = cloudant; 254 | -------------------------------------------------------------------------------- /types/tests.ts: -------------------------------------------------------------------------------- 1 | // Copyright © 2018, 2019 IBM Corp. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* tslint:disable:no-empty */ 16 | import cloudant = require('@cloudant/cloudant'); 17 | import nano = require('nano'); 18 | 19 | const { BasePlugin } = cloudant; 20 | 21 | interface ICustomPluginConfig extends cloudant.PluginConfig { 22 | customLoggingEnabled?: boolean; 23 | } 24 | 25 | class CustomPlugin extends BasePlugin { 26 | public static id = 'custom'; 27 | 28 | constructor(client: nano.DocumentScope<{}>, configuration: ICustomPluginConfig) { 29 | const cfg = Object.assign({ 30 | customLoggingEnabled: true 31 | }, configuration); 32 | 33 | super(client, cfg); 34 | } 35 | 36 | // tslint:disable-next-line:max-line-length 37 | public onRequest(state: cloudant.PluginState, request: cloudant.PluginRequest, callback: cloudant.PluginCallbackFunction) { 38 | const { customLoggingEnabled } = this._cfg as ICustomPluginConfig; 39 | 40 | if (customLoggingEnabled) { 41 | // tslint:disable-next-line:no-console 42 | console.log('%s request made to %s with headers %o', request.method, request.uri, request.headers); 43 | } 44 | 45 | callback(state); 46 | } 47 | 48 | // tslint:disable-next-line:max-line-length 49 | public onResponse(state: cloudant.PluginState, response: cloudant.PluginResponse, callback: cloudant.PluginCallbackFunction) { 50 | const { customLoggingEnabled } = this._cfg as ICustomPluginConfig; 51 | 52 | if (customLoggingEnabled) { 53 | // tslint:disable-next-line 54 | console.log('%d response for %s request made to %s', response.statusCode, response.request.method, response.request.uri); 55 | } 56 | 57 | callback(state); 58 | } 59 | 60 | // tslint:disable-next-line:max-line-length 61 | public onError(state: cloudant.PluginState, error: Error, callback: cloudant.PluginCallbackFunction) { 62 | const { customLoggingEnabled } = this._cfg as ICustomPluginConfig; 63 | 64 | if (customLoggingEnabled) { 65 | // tslint:disable-next-line:no-console 66 | console.error(error); 67 | } 68 | 69 | callback(state); 70 | } 71 | } 72 | 73 | /* 74 | * Instantiate with configuration object 75 | */ 76 | const config: cloudant.Configuration = { 77 | account: 'my-cloudant-account', 78 | maxAttempt: 3, 79 | password: 'my-password', 80 | plugins: [CustomPlugin, 'retry'] 81 | }; 82 | 83 | const cfgInstance = cloudant(config); 84 | 85 | /* 86 | * Instantiate with VCAP configuration object 87 | */ 88 | 89 | const vcapConfig: cloudant.Configuration = { 90 | vcapInstanceName: 'foo', 91 | vcapServiceName: 'cloudantXYZ', 92 | vcapServices: JSON.parse(process.env.VCAP_SERVICES || '{}') 93 | }; 94 | 95 | const vcapInstance = cloudant(vcapConfig); 96 | 97 | /* 98 | * Run Initialization Callback 99 | */ 100 | cloudant(config, (error, client, pong) => { 101 | if (error) { 102 | return; 103 | } else if (client) { 104 | client.db.list((err, allDbs) => {}); 105 | } 106 | }); 107 | 108 | /* 109 | * Server Scope 110 | */ 111 | const instance = cloudant('http://localhost:5984'); 112 | 113 | instance.ping((pong) => {}); 114 | instance.ping().then((pong) => {}); 115 | 116 | instance.generate_api_key((error, key) => {}); 117 | instance.generate_api_key().then((key) => {}); 118 | 119 | const cors: cloudant.CORS = { 120 | allow_credentials: true, 121 | enable_cors: true, 122 | origins: ['*'] 123 | }; 124 | 125 | instance.set_cors(cors, (error, data) => {}); 126 | instance.set_cors(cors).then((data) => {}); 127 | 128 | instance.get_cors((error, data) => {}); 129 | instance.get_cors().then((data) => {}); 130 | 131 | /* 132 | * Document Scope 133 | */ 134 | 135 | const mydb: cloudant.DocumentScope<{}> = instance.use('mydb'); 136 | 137 | const docs: nano.BulkModifyDocsWrapper = { 138 | docs: [ { id: 'doc1' }, { id: 'doc2' } ] 139 | }; 140 | 141 | mydb.bulk_get(docs, (results) => {}); 142 | mydb.bulk_get(docs).then((results) => {}); 143 | 144 | const security: cloudant.Security = { 145 | nobody: [], 146 | nodejs : [ '_reader', '_writer', '_admin', '_replicator' ] 147 | }; 148 | 149 | mydb.set_security(security, (error, resp) => {}); 150 | mydb.set_security(security).then((resp) => {}); 151 | 152 | mydb.get_security((err, resp) => {}); 153 | mydb.get_security().then((resp) => {}); 154 | 155 | const params: nano.DocumentSearchParams = { 156 | limit: 10, 157 | q: 'bird:*' 158 | }; 159 | 160 | mydb.search('design', 'doc', params, (err, resp) => {}); 161 | mydb.search('design', 'doc', params).then((resp) => {}); 162 | 163 | const geoParams: cloudant.GeoParams = { 164 | include_docs: true, 165 | lat: 27.000, 166 | lon: 28.00 167 | }; 168 | 169 | mydb.geo('design', 'docname', geoParams, (err, result) => {}); 170 | mydb.geo('design', 'docname', geoParams).then((result) => {}); 171 | 172 | const myIndex = { 173 | index: { fields: [ 'foo' ] }, 174 | name: 'foo-index', 175 | type: 'json' 176 | }; 177 | 178 | // Create an index. 179 | mydb.index(myIndex, (err, resp) => {}); 180 | mydb.index(myIndex).then((resp) => {}); 181 | 182 | // See all indexes. 183 | mydb.index((err: any, resp: any) => {}); 184 | mydb.index().then((resp) => {}); 185 | 186 | const myDeleteSpec: cloudant.QueryDeleteSpec = { 187 | ddoc: '_design/1f003ce73056238720c2e8f7da545390a8ea1dc5', 188 | name: 'foo-index' 189 | }; 190 | 191 | // Delete an index. 192 | mydb.index.del(myDeleteSpec, (err, resp) => {}); 193 | mydb.index.del(myDeleteSpec).then((resp) => {}); 194 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "noEmit": true, 9 | "strictFunctionTypes": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@cloudant/cloudant": ["."] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "quotemark": [ 5 | true, 6 | "single", 7 | "avoid-escape" 8 | ], 9 | "no-implicit-dependencies": false, 10 | "trailing-comma": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------