├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Feature_request.md │ ├── Question.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── .gitignore ├── .npmrc ├── CHANGES.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── doc ├── advanced-queries.md └── multiple-db-instances.md ├── index.js ├── intl ├── MSG.json ├── cs │ └── messages.json ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fr │ └── messages.json ├── it │ └── messages.json ├── ja │ └── messages.json ├── ko │ └── messages.json ├── nl │ └── messages.json ├── pl │ └── messages.json ├── pt │ └── messages.json ├── ru │ └── messages.json ├── tr │ └── messages.json ├── zh-Hans │ └── messages.json ├── zh-Hant │ └── messages.json └── zz │ ├── messages.json │ └── messages_inverted.json ├── lib ├── couchdb.js ├── discovery.js ├── migrate.js └── view.js ├── package.json ├── setup.sh ├── test.js └── test ├── automigrate.test.js ├── autoupdate.test.js ├── connection.test.js ├── couchdb.test.js ├── count.test.js ├── create.test.js ├── find.test.js ├── findById.test.js ├── imported.test.js ├── index.test.js ├── init.js ├── lib └── test-util.js ├── maxrows.test.js ├── regexp.test.js ├── regextopcre.test.js ├── replace.test.js ├── update.test.js └── view.test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback", 3 | "rules": { 4 | "max-len": ["error", 120, 4, { 5 | "ignoreComments": true, 6 | "ignoreUrls": true, 7 | "ignorePattern": "^\\s*var\\s.=\\s*(require\\s*\\()|(/)" 8 | }], 9 | "no-unused-expressions": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | 6 | --- 7 | 8 | 18 | 19 | ## Steps to reproduce 20 | 21 | 22 | 23 | ## Current Behavior 24 | 25 | 26 | 27 | ## Expected Behavior 28 | 29 | 30 | 31 | ## Link to reproduction sandbox 32 | 33 | 37 | 38 | ## Additional information 39 | 40 | 45 | 46 | ## Related Issues 47 | 48 | 49 | 50 | _See [Reporting Issues](http://loopback.io/doc/en/contrib/Reporting-issues.html) for more tips on writing good issues_ 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: feature 5 | 6 | --- 7 | 8 | ## Suggestion 9 | 10 | 11 | 12 | ## Use Cases 13 | 14 | 18 | 19 | ## Examples 20 | 21 | 22 | 23 | ## Acceptance criteria 24 | 25 | TBD - will be filled by the team. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: The issue tracker is not for questions. Please use Stack Overflow or other resources for help. 4 | labels: question 5 | 6 | --- 7 | 8 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Report a security vulnerability 4 | url: https://loopback.io/doc/en/contrib/Reporting-issues.html#security-issues 5 | about: Do not report security vulnerabilities using GitHub issues. Please send an email to `security@loopback.io` instead. 6 | - name: Get help on StackOverflow 7 | url: https://stackoverflow.com/tags/loopbackjs 8 | about: Please ask and answer questions on StackOverflow. 9 | - name: Join our mailing list 10 | url: https://groups.google.com/forum/#!forum/loopbackjs 11 | about: You can also post your question to our mailing list. 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## Checklist 12 | 13 | - [ ] DCO (Developer Certificate of Origin) [signed in all commits](https://loopback.io/doc/en/contrib/code-contrib.html) 14 | - [ ] `npm test` passes on your machine 15 | - [ ] New tests added or existing tests modified to cover all changes 16 | - [ ] Code conforms with the [style guide](https://loopback.io/doc/en/contrib/style-guide-es6.html) 17 | - [ ] Commit messages are following our [guidelines](https://loopback.io/doc/en/contrib/git-commit-messages.html) 18 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - critical 10 | - p1 11 | - major 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: > 21 | This issue has been closed due to continued inactivity. Thank you for your understanding. 22 | If you believe this to be in error, please contact one of the code owners, 23 | listed in the `CODEOWNERS` file at the top-level of this repository. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2019-11-26, Version 1.5.3 2 | ========================= 3 | 4 | * feat: add partition field to model (#72) (Janny) 5 | 6 | * chore: improve issue and PR templates (Nora) 7 | 8 | 9 | 2019-11-19, Version 1.5.2 10 | ========================= 11 | 12 | * refactor: add options for search (#71) (Janny) 13 | 14 | 15 | 2019-11-14, Version 1.5.1 16 | ========================= 17 | 18 | * refactor: migrate functions (#69) (Janny) 19 | 20 | 21 | 2019-09-19, Version 1.5.0 22 | ========================= 23 | 24 | * drop support for node 6 (Nora) 25 | 26 | 27 | 2019-05-07, Version 1.4.0 28 | ========================= 29 | 30 | * chore: update strong-globalize version (Diana Lau) 31 | 32 | * chore: update copyrights years (Diana Lau) 33 | 34 | * fix: update lodash (#58) (Janny) 35 | 36 | * fix: honor fields in the filter (#59) (Janny) 37 | 38 | * fix: eslint (#56) (Janny) 39 | 40 | 41 | 2018-07-09, Version 1.3.0 42 | ========================= 43 | 44 | * fix: lint (#55) (Janny) 45 | 46 | * chore: fix linting and add .npmrc (virkt25) 47 | 48 | * PCRE Regular Expressions (#52) (Dan Jarvis) 49 | 50 | * [WebFM] cs/pl/ru translation (#50) (tangyinb) 51 | 52 | * remove node 4 (jannyHou) 53 | 54 | * correct the connector name (jannyHou) 55 | 56 | 57 | 2018-04-27, Version 1.2.1 58 | ========================= 59 | 60 | * fix: add model view in index (#47) (Janny) 61 | 62 | 63 | 2018-01-19, Version 1.2.0 64 | ========================= 65 | 66 | * Add global limit (#42) (Janny) 67 | 68 | * Fix lint (#38) (Janny) 69 | 70 | 71 | 2017-10-18, Version 1.1.0 72 | ========================= 73 | 74 | * update dependencies (Diana Lau) 75 | 76 | 77 | 2017-09-26, Version 1.0.3 78 | ========================= 79 | 80 | * Update nano version (#36) (Loay) 81 | 82 | * remove event listener (#33) (Janny) 83 | 84 | * Add stalebot configuration (Kevin Delisle) 85 | 86 | 87 | 2017-08-22, Version 1.0.2 88 | ========================= 89 | 90 | * Recover skipped test (ssh24) 91 | 92 | 93 | 2017-08-21, Version 1.0.1 94 | ========================= 95 | 96 | * getMoSettings (#30) (Janny) 97 | 98 | * Make getDriver extendable (#29) (Janny) 99 | 100 | * Fix readme (ssh24) 101 | 102 | 103 | 2017-08-17, Version 1.0.0 104 | ========================= 105 | 106 | * Remove beta warning (Kevin Delisle) 107 | 108 | * Using strong-mocha-interfaces instead of lb-bdd (#25) (Taranveer Virk) 109 | 110 | * Implement indexes feature (ssh24) 111 | 112 | * Create Issue and PR Templates (#24) (Sakib Hasan) 113 | 114 | * Fix npm mocha command (ssh24) 115 | 116 | * Use Mocha Interface to skip tests (Taranveer Virk) 117 | 118 | * Update eslint (ssh24) 119 | 120 | * Add test folder (#17) (Janny) 121 | 122 | * Return data without model name prop (ssh24) 123 | 124 | * Add CODEOWNER file (Diana Lau) 125 | 126 | 127 | 2017-08-07, Version 0.9.0 128 | ========================= 129 | 130 | * First release! 131 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precedence. 4 | 5 | # Alumni maintainers 6 | # @ssh24 @virkt25 @loay @kjdelisle @b-admike 7 | 8 | # Core team members from IBM 9 | * @jannyHou @dhmlau 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | LoopBack, as member project of the OpenJS Foundation, use 4 | [Contributor Covenant v2.0](https://contributor-covenant.org/version/2/0/code_of_conduct) 5 | as their code of conduct. The full text is included 6 | [below](#contributor-covenant-code-of-conduct-v2.0) in English, and translations 7 | are available from the Contributor Covenant organisation: 8 | 9 | - [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations) 10 | - [github.com/ContributorCovenant](https://github.com/ContributorCovenant/contributor_covenant/tree/release/content/version/2/0) 11 | 12 | Refer to the sections on reporting and escalation in this document for the 13 | specific emails that can be used to report and escalate issues. 14 | 15 | ## Reporting 16 | 17 | ### Project Spaces 18 | 19 | For reporting issues in spaces related to LoopBack, please use the email 20 | `tsc@loopback.io`. The LoopBack Technical Steering Committee (TSC) handles CoC issues related to the spaces that it 21 | maintains. The project TSC commits to: 22 | 23 | - maintain the confidentiality with regard to the reporter of an incident 24 | - to participate in the path for escalation as outlined in the section on 25 | Escalation when required. 26 | 27 | ### Foundation Spaces 28 | 29 | For reporting issues in spaces managed by the OpenJS Foundation, for example, 30 | repositories within the OpenJS organization, use the email 31 | `report@lists.openjsf.org`. The Cross Project Council (CPC) is responsible for 32 | managing these reports and commits to: 33 | 34 | - maintain the confidentiality with regard to the reporter of an incident 35 | - to participate in the path for escalation as outlined in the section on 36 | Escalation when required. 37 | 38 | ## Escalation 39 | 40 | The OpenJS Foundation maintains a Code of Conduct Panel (CoCP). This is a 41 | foundation-wide team established to manage escalation when a reporter believes 42 | that a report to a member project or the CPC has not been properly handled. In 43 | order to escalate to the CoCP send an email to 44 | `coc-escalation@lists.openjsf.org`. 45 | 46 | For more information, refer to the full 47 | [Code of Conduct governance document](https://github.com/openjs-foundation/cross-project-council/blob/HEAD/CODE_OF_CONDUCT.md). 48 | 49 | --- 50 | 51 | ## Contributor Covenant Code of Conduct v2.0 52 | 53 | ## Our Pledge 54 | 55 | We as members, contributors, and leaders pledge to make participation in our 56 | community a harassment-free experience for everyone, regardless of age, body 57 | size, visible or invisible disability, ethnicity, sex characteristics, gender 58 | identity and expression, level of experience, education, socio-economic status, 59 | nationality, personal appearance, race, religion, or sexual identity and 60 | orientation. 61 | 62 | We pledge to act and interact in ways that contribute to an open, welcoming, 63 | diverse, inclusive, and healthy community. 64 | 65 | ## Our Standards 66 | 67 | Examples of behavior that contributes to a positive environment for our 68 | community include: 69 | 70 | - Demonstrating empathy and kindness toward other people 71 | - Being respectful of differing opinions, viewpoints, and experiences 72 | - Giving and gracefully accepting constructive feedback 73 | - Accepting responsibility and apologizing to those affected by our mistakes, 74 | and learning from the experience 75 | - Focusing on what is best not just for us as individuals, but for the overall 76 | community 77 | 78 | Examples of unacceptable behavior include: 79 | 80 | - The use of sexualized language or imagery, and sexual attention or advances of 81 | any kind 82 | - Trolling, insulting or derogatory comments, and personal or political attacks 83 | - Public or private harassment 84 | - Publishing others' private information, such as a physical or email address, 85 | without their explicit permission 86 | - Other conduct which could reasonably be considered inappropriate in a 87 | professional setting 88 | 89 | ## Enforcement Responsibilities 90 | 91 | Community leaders are responsible for clarifying and enforcing our standards of 92 | acceptable behavior and will take appropriate and fair corrective action in 93 | response to any behavior that they deem inappropriate, threatening, offensive, 94 | or harmful. 95 | 96 | Community leaders have the right and responsibility to remove, edit, or reject 97 | comments, commits, code, wiki edits, issues, and other contributions that are 98 | not aligned to this Code of Conduct, and will communicate reasons for moderation 99 | decisions when appropriate. 100 | 101 | ## Scope 102 | 103 | This Code of Conduct applies within all community spaces, and also applies when 104 | an individual is officially representing the community in public spaces. 105 | Examples of representing our community include using an official e-mail address, 106 | posting via an official social media account, or acting as an appointed 107 | representative at an online or offline event. 108 | 109 | ## Enforcement 110 | 111 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 112 | reported to the community leaders responsible for enforcement at 113 | [tsc@loopback.io](mailto:tsc@loopback.io). All complaints will be reviewed and 114 | investigated promptly and fairly. 115 | 116 | All community leaders are obligated to respect the privacy and security of the 117 | reporter of any incident. 118 | 119 | ## Enforcement Guidelines 120 | 121 | Community leaders will follow these Community Impact Guidelines in determining 122 | the consequences for any action they deem in violation of this Code of Conduct: 123 | 124 | ### 1. Correction 125 | 126 | **Community Impact**: Use of inappropriate language or other behavior deemed 127 | unprofessional or unwelcome in the community. 128 | 129 | **Consequence**: A private, written warning from community leaders, providing 130 | clarity around the nature of the violation and an explanation of why the 131 | behavior was inappropriate. A public apology may be requested. 132 | 133 | ### 2. Warning 134 | 135 | **Community Impact**: A violation through a single incident or series of 136 | actions. 137 | 138 | **Consequence**: A warning with consequences for continued behavior. No 139 | interaction with the people involved, including unsolicited interaction with 140 | those enforcing the Code of Conduct, for a specified period of time. This 141 | includes avoiding interactions in community spaces as well as external channels 142 | like social media. Violating these terms may lead to a temporary or permanent 143 | ban. 144 | 145 | ### 3. Temporary Ban 146 | 147 | **Community Impact**: A serious violation of community standards, including 148 | sustained inappropriate behavior. 149 | 150 | **Consequence**: A temporary ban from any sort of interaction or public 151 | communication with the community for a specified period of time. No public or 152 | private interaction with the people involved, including unsolicited interaction 153 | with those enforcing the Code of Conduct, is allowed during this period. 154 | Violating these terms may lead to a permanent ban. 155 | 156 | ### 4. Permanent Ban 157 | 158 | **Community Impact**: Demonstrating a pattern of violation of community 159 | standards, including sustained inappropriate behavior, harassment of an 160 | individual, or aggression toward or disparagement of classes of individuals. 161 | 162 | **Consequence**: A permanent ban from any sort of public interaction within the 163 | community. 164 | 165 | ## Attribution 166 | 167 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 168 | version 2.0, available at 169 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 170 | 171 | Community Impact Guidelines were inspired by 172 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 173 | 174 | [homepage]: https://www.contributor-covenant.org 175 | 176 | For answers to common questions about this code of conduct, see the FAQ at 177 | https://www.contributor-covenant.org/faq. Translations are available at 178 | https://www.contributor-covenant.org/translations. 179 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing 2 | 3 | Thank you for your interest in `loopback-connector-couchdb2`, an open source project 4 | administered by IBM. 5 | 6 | Contributing to `loopback-connector-couchdb2` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * [Sign](https://loopback.io/doc/en/contrib/code-contrib.html) all commits with DCO. 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Developer Certificate of Origin 23 | 24 | This project uses [DCO](https://developercertificate.org/). Be sure to sign off 25 | your commits using the `-s` flag or adding `Signed-off-By: Name` in the 26 | commit message. 27 | 28 | **Example** 29 | 30 | ``` 31 | git commit -s -m "feat: my commit message" 32 | ``` 33 | 34 | Also see the [Contributing to LoopBack](https://loopback.io/doc/en/contrib/code-contrib.html) to get you started. 35 | 36 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 37 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2017. All Rights Reserved. 2 | Node module: loopback-connector-couchdb2 3 | 4 | -------- 5 | Copyright 2017 IBM Corp. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-connector-couchdb2 2 | 3 | The `loopback-connector-couchdb2` module is the CouchDB 2.x connector for the 4 | LoopBack framework that supports the advanced functionality originally found 5 | only in Cloudant but that is now available in CouchDB. 6 | 7 | ## Installation 8 | 9 | In your application root directory, enter this command to install the connector: 10 | 11 | ```sh 12 | npm install loopback-connector-couchdb2 --save 13 | ``` 14 | 15 | This installs the module from npm and adds it as a dependency to the 16 | application's `package.json` file. 17 | 18 | If you create a CouchDB 2.x data source using the data source generator as described 19 | below, you don't have to do this, since the generator will run `npm install` for 20 | you. 21 | 22 | ## Getting Started - Model 23 | 24 | ### Map Between Model And Document 25 | 26 | Similar to Cloudant, Couchdb doesn't have a concept as 'table' or 'collection', and to support ad-hoc query which is an important loopback feature, by default the connector uses all_fields index for query, and doesn't create design document for a loopback model. 27 | 28 | In a real application, all_fields index doesn't do any optimization and slow down the performance a lot, for details about how to create index for properties, please refer to [property index](https://github.com/strongloop/loopback-connector-couchdb2#property-index) 29 | 30 | A loopback model instance is stored as a document in Couchdb. It has a model index property to specify the model name, the connector also adds it to Couchdb query's selector when doing model level queries. For example, a User model instance is stored as 31 | 32 | ``` 33 | "loopback__model__name": "User", 34 | "username": "Foo", 35 | "password": "bar" 36 | ``` 37 | 38 | To create a model instance, the connector creates a document with value of property 'loopback__model__name' equals to `User`, and adds `loopback__model__name: 'User'` to query when fetches `User` instances. 39 | 40 | By default, `modelIndex` is 'loopback__model__name', and `modelSelector` is {[modelIndex]: modelName}. User can customize `modelSelector` or `modelIndex` in model's json file. For details please check [model-specific configuration](https://github.com/strongloop/loopback-connector-couchdb2#model-specific-configuration) 41 | 42 | ### Model-specific Configuration 43 | 44 | You can specify configurations per model for database selection and to 45 | map a model to a different document: 46 | 47 | *common/models/_model-name_.json* 48 | 49 | ``` 50 | { 51 | "name": "User", 52 | "base": "PersistedModel", 53 | "idInjection": true, 54 | ... 55 | "couchdb": { 56 | "modelIndex": "custom_model_index_name", 57 | "modelSelector": { "custom_selector": "user" }, 58 | "database": "test2" 59 | }, 60 | ... 61 | ``` 62 | 63 | Model-specific configuration settings: 64 | 65 | Property        | Type | Description 66 | ----------| -----| -------- 67 | database | String | Database name 68 | modelIndex | String | Specify the model name to document mapping, defaults to `loopback__model__name`. 69 | modelSelector | JSON | Use the Couchdb Query selector syntax to associate models to existing data. NOTE: modelSelector and modelIndex are mutually exclusive. modelSelector will override modelIndex when building query. 70 | 71 | ### _rev Property 72 | 73 | In a document, property `_rev` is the latest doc revision and must be provided when modifying the doc. 74 | 75 | Our connector allows the user to retrieve back the `_rev` property upon all CRUD operations, however does not add it to the model definition. 76 | 77 | If you would like to have a `_rev` property on your model, as an end user, the onus is on you to add the property in the model definition. 78 | 79 | **Note:** All CRUD operations require `_rev` (except create) and __it is up to the user to specify them__. The connector does not handle such cases due to possibilities of race condition when two users try to update the same document. 80 | 81 | #### Example CRUD operations with `_rev` 82 | 83 | `model.json` 84 | ``` json 85 | { 86 | ... 87 | "properties": { 88 | "_rev": { 89 | "type": "string" 90 | }, 91 | "name": { 92 | "type": "string" 93 | } 94 | }, 95 | ... 96 | } 97 | ``` 98 | 99 | - Create 100 | 101 | ```javascript 102 | Model.create([{ 103 | name: 'Foo', 104 | }, { 105 | name: 'Bar', 106 | }], function(err, result) { 107 | if (err) throw err; 108 | console.log('Created instance: ' + JSON.stringify(result)); 109 | }); 110 | ``` 111 | 112 | **Note:** Couchdb does not allow customized `_rev` value, hence creating an instance with a `_rev` value will not give the expected result (i.e Couchdb's CREATE operation ignores the `_rev` value when provided and generates a random unique one). The onus is on the user if they fail to comply to this rule. 113 | 114 | Let's say we have an instance in the database: 115 | ```json 116 | { 117 | "id":"2", 118 | "_rev":"2-abcedf", 119 | "name":"Bar" 120 | } 121 | ``` 122 | 123 | - Find 124 | 125 | - find 126 | 127 | ```javascript 128 | Model.find(function(err, result) { 129 | if (err) throw err; 130 | console.log('Found all instances: ' + JSON.stringify(result)); 131 | }); 132 | ``` 133 | 134 | - findById 135 | 136 | ```javascript 137 | Model.findById('2', function(err, result) { 138 | if (err) throw err; 139 | console.log('Found instance with id: ' + JSON.stringify(result)); 140 | }); 141 | ``` 142 | 143 | - Replace 144 | 145 | - replaceOrCreate 146 | 147 | ```javascript 148 | Model.replaceOrCreate({ 149 | id:'2', 150 | _rev:'2-abcedf', 151 | name:'Bar2' 152 | }, function(err, result) { 153 | if (err) throw err; 154 | console.log('Replace an existing instance: ' + JSON.stringify(result)); 155 | }); 156 | ``` 157 | 158 | - replaceById 159 | 160 | ```javascript 161 | Model.replaceById('2', { 162 | _rev:'2-abcedf', 163 | name:'Bar3' 164 | }, function(err, result) { 165 | if (err) throw err; 166 | console.log('Replace an existing instance with id: ' + JSON.stringify(result)); 167 | }); 168 | ``` 169 | 170 | - Update 171 | 172 | - updateOrCreate 173 | 174 | ```javascript 175 | Model.updateOrCreate({ 176 | id:'2', 177 | _rev:'2-abcedf', 178 | name:'Bar4' 179 | }, function(err, result) { 180 | if (err) throw err; 181 | console.log('Update an existing instance: ' + JSON.stringify(result)); 182 | }); 183 | ``` 184 | 185 | - update/updateAll 186 | 187 | - with `_rev` property 188 | ```javascript 189 | Model.updateAll({ 190 | _rev:'2-abcedf', 191 | name:'Bar4' 192 | }, {name: 'Bar4-updated', _rev: '2-abcedf'}, function(err, result) { 193 | if (err) throw err; 194 | console.log('Update an existing instance: ' + JSON.stringify(result)); 195 | }); 196 | ``` 197 | 198 | - without `_rev` property 199 | ```javascript 200 | Model.updateAll({ 201 | name:'Bar4' 202 | }, {name: 'Bar4-updated'}, function(err, result) { 203 | if (err) throw err; 204 | console.log('Update an existing instance: ' + JSON.stringify(result)); 205 | }); 206 | ``` 207 | 208 | # Setup Couchdb Instance 209 | 210 | For users that don't have a Couchdb server to develop or test, here are some suggestions can help you quickly set one up. 211 | 212 | For development use, a docker container is easy to setup. Users can also download the on-prem Couchdb 2.x from http://couchdb.apache.org/ 213 | 214 | # Installation 215 | 216 | Enter the following in the top-level directory of your LoopBack application: 217 | 218 | ``` 219 | $ npm install loopback-connector-couchdb2 --save 220 | ``` 221 | 222 | The `--save` option adds the dependency to the application’s `package.json` file. 223 | 224 | # Configuration 225 | 226 | ## Generate Datasource 227 | 228 | Use the [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html) to add the Couchdb data source to your application. The entry in the applications `/server/datasources.json` will 229 | look something like this: 230 | 231 | ``` 232 | "mydb": { 233 | "name": "mydb", 234 | "connector": "couchdb2", 235 | "url": "https://:@" 236 | "database": "test" 237 | } 238 | ``` 239 | 240 | ## Datasource Config 241 | The connector passes all configurations to nano driver, please check couchdb-nano's document for details: 242 | https://github.com/apache/couchdb-nano#configuration 243 | 244 | ## Example Usage 245 | 246 | */server/script.js* 247 | ```javascript 248 | var util = require('util'); 249 | 250 | // Here we create datasource dynamically. 251 | var DataSource = require ('loopback-datasource-juggler').DataSource, 252 | Couchdb = require ('loopback-connector-couchdb2'); 253 | 254 | var config = { 255 | url: 'your_couchdb_url' 256 | database: 'your_couchdb_database' 257 | }; 258 | 259 | var db = new DataSource (Couchdb, config); 260 | 261 | Test = db.define ('Test', { 262 | name: { type: String }, 263 | }); 264 | 265 | Test.create({ 266 | name: "Tony", 267 | }).then(function(test) { 268 | console.log('create instance ' + util.inspect(test, 4)); 269 | return Test.find({ where: { name: "Tony" }}); 270 | }).then(function(test) { 271 | console.log('find instance: ' + util.inspect(test, 4)); 272 | return Test.destroyAll(); 273 | }).then(function(test) { 274 | console.log('destroy instance!'); 275 | }).catch(err); 276 | 277 | }); 278 | ``` 279 | 280 | - Use different DB instances per model definition. Refer to https://github.com/strongloop/loopback-connector-couchdb2/blob/master/doc/multiple-db-instances.md 281 | 282 | # CRUD 283 | 284 | User can find most loopback CRUD operation apis documented in https://loopback.io/doc/en/lb3/Built-in-models-REST-API.html 285 | 286 | Due to the `_rev` property, Couchdb connector handles CRUD functions a little differently, for details and examples please refer to [_rev-property](https://github.com/strongloop/loopback-connector-couchdb2/blob/master/doc/_rev-property.md) 287 | 288 | # Migration 289 | 290 | For a model connected to Couchdb database, migration means create/update a design document with proper indexes provided by the model. There is a section called [property index](https://github.com/strongloop/loopback-connector-couchdb2#property-index) that talks about how to define indexes. 291 | 292 | After attaching a model to a Couchdb datasource, either statically with `model.json` file or dynamically in boot script code, user need to run `automigrate` or `autoupdate` to migrate models to database. Couchdb connector does NOT automatically migrate them. 293 | 294 | The following migration functions take either an array of multiple model's name, or a string of a single model's name. The example code will show how to do it. 295 | 296 | ## autoupdate vs automigrate 297 | 298 | `autoupdate` does not destroy existing model instances if model already defined in database. It only creates design document for new models. 299 | Under the hood Couchdb allows creating same design doc multiple times, it doesn't return error, but returns `existed` as result to tell is it a new design doc or existing one. 300 | 301 | `automigrate` destroys existing model instances if model already defined in database. Please make sure you do want to clean up data before running `automigrate`. Then it does same thing as `autoupdate` 302 | 303 | ## isActual 304 | 305 | User can call this function to check if model exists in database. 306 | We need to discuss do we still want to create a design doc for a model if no index provided: 307 | - if yes: keep this function 308 | - if no: isActual doesn't make any sense then 309 | 310 | ## property index 311 | 312 | TBD. Briefly: 313 | - By default we use all_fields index with no optimization for performance 314 | - If user define indexable properties or composite index, we create them in one design document 315 | - It's upon user's choice to specify the index they want to use in a query. 316 | 317 | ## Example Code 318 | 319 | Should be adjusted according to the decision we made for isActual 320 | 321 | */server/script.js* 322 | ```javascript 323 | module.export = function migrateData(app) { 324 | // Suppose you already define a datasource called `cloudantDS` 325 | // in server/datasources.json 326 | var ds = app.datasources.cloudantDS; 327 | 328 | // static model created with model.json file 329 | var StaticModel = app.models.StaticModel; 330 | // dynamic model created in boot script 331 | var DynamicModel = ds.define('DynamicModel', { 332 | name: {type: String}, 333 | description: {type: String}, 334 | }); 335 | 336 | // Write the three examples in parallel just to avoid dup code, 337 | // please try ONLY ONE of them at one time. 338 | ds.once('connected', function() { 339 | // try autoupdate example - multiple models 340 | ds.autoupdate(['StaticModel', 'DynamicModel'], function(err) {}); 341 | // OR 342 | // try automigrate example - single model 343 | ds.automigrate('StaticModel', function(err) {}); 344 | // OR 345 | // try isActual example - if any model exist, run autoupdate, otherwise automigrate 346 | ds.isActual(['StaticModel', 'DynamicModel'], function(err, exist) { 347 | if (exist) { 348 | ds.autoupdate(['StaticModel', 'DynamicModel'], function(err){}) 349 | } else { 350 | ds.automigate(['StaticModel', 'DynamicModel'], function(err){}); 351 | } 352 | }); 353 | }); 354 | } 355 | ``` 356 | 357 | # Discovery 358 | 359 | Not implemented yet in this connector. 360 | 361 | # Query 362 | 363 | - Couchdb doesn't support sorting with a property that's not indexable. 364 | - [LoopBack query](http://loopback.io/doc/en/lb3/Querying-data.html) support for: fields, limit, order, skip and where filters. 365 | - Please check [Advanced Queries](https://github.com/strongloop/loopback-connector-couchdb/blob/master/doc/advanced-queries.md) for details about regex filter, nested filter and order. 366 | 367 | # View 368 | 369 | Given a design doc name and the view name in it, user can use a connector level function `viewDocs` to query the view. 370 | 371 | Since `viewDocs` is a specific api for Couchdb/Cloudant connector only, it is not attached to the dataSource Object defined in loopback-datasource-juggler, which means the correct way to call it is `ds.connector.viewDocs`: 372 | 373 | */server/script.js* 374 | ```javascript 375 | module.exports = function(server) { 376 | // Get Couchdb dataSource as `ds` 377 | // 'couchdbDS' is the name of Couchdb datasource created in 378 | // 'server/datasources.json' file 379 | var ds = server.datasources.couchdbDS; 380 | 381 | // 1. Please note `ds.connector.viewDocs()` is the correct way to call it, 382 | // NOT `ds.viewDocs()` 383 | // 2. This api matches the Couchdb endpoint: 384 | // GET /db/_design//_view/ 385 | ds.connector.viewDocs('design_doc', 'view_name', function(err, results) { 386 | // `results` would be the data returned by quering that view 387 | }); 388 | 389 | // Alternatively user can also specify the filter for view query 390 | ds.connector.viewDocs('design_doc', 'view_name', {key: 'filter'}, 391 | function(err, results) {}); 392 | }; 393 | ``` 394 | 395 | # Bulk replace 396 | 397 | Given an array of data to be updated, Couchdb supports the idea of performing bulk replace on a model instance. Please note, unlike other CRUD operations, bulk replace does not invoke any operation hooks. 398 | 399 | **Note:** To perform bulk replace, each data in the array data set needs to have the `id` and `_rev` property corresponding to the documents `id` and `_rev` property in the database. 400 | 401 | Example: 402 | 403 | *server/boot/script.js* 404 | 405 | ```javascript 406 | var dataToCreate = [ 407 | {id: 1, name: 'Foo', age: 1}, 408 | {id: 2, name: 'Bar', age: 1}, 409 | {id: 3, name: 'Baz', age: 2}, 410 | {id: 4, name: 'A', age: 4}, 411 | {id: 5, name: 'B', age: 5}, 412 | {id: 6, name: 'C', age: 6}, 413 | {id: 7, name: 'D', age: 7}, 414 | {id: 8, name: 'E', age: 8}, 415 | ]; 416 | var dataToUpdate = [ 417 | {id: 1, name: 'Foo-change', age: 11}, 418 | {id: 5, name: 'B-change', age: 51}, 419 | {id: 8, name: 'E-change', age: 91} 420 | ]; 421 | 422 | module.exports = function(app) { 423 | var db = app.dataSources.couchdbDS; 424 | var Employee = app.models.Employee; 425 | 426 | db.automigrate(function(err) { 427 | if (err) throw err; 428 | 429 | Employee.create(dataToCreate, function(err, result) { 430 | if (err) throw err; 431 | console.log('\nCreated instance: ' + JSON.stringify(result)); 432 | 433 | dataToUpdate[0].id = result[0].id; 434 | dataToUpdate[0]._rev = result[0]._rev; 435 | dataToUpdate[1].id = result[4].id; 436 | dataToUpdate[1]._rev = result[4]._rev; 437 | dataToUpdate[2].id = result[7].id; 438 | dataToUpdate[2]._rev = result[7]._rev; 439 | 440 | // note: it is called `db.connector.bulkReplace` 441 | // rather than `Employee.bulkReplace` 442 | db.connector.bulkReplace('Employee', dataToUpdate, function(err, result) { 443 | if (err) throw err; 444 | 445 | console.log('\nBulk replace performed: ' + JSON.stringify(result)); 446 | 447 | Employee.find(function(err, result) { 448 | if (err) throw err; 449 | 450 | console.log('\nFound all instances: ' + JSON.stringify(result)); 451 | }); 452 | }); 453 | }); 454 | }); 455 | }; 456 | ``` 457 | 458 | # Testing 459 | 460 | ## Docker 461 | - Assuming you have [Docker](https://docs.docker.com/engine/installation/) installed, run the following script which would spawn a Couch instance on your local: 462 | ```bash 463 | source setup.sh 464 | ``` 465 | where ``, ``, ``, `` and `` are optional parameters. The default values are `localhost`, `5984`, `admin`, `pass` and `testdb` respectively. 466 | - Run the test: 467 | ```bash 468 | npm run mocha 469 | ``` 470 | 471 | # More Info 472 | For more detailed information regarding connector-specific functions and behaviour, 473 | see the [docs section](https://github.com/strongloop/loopback-connector-couchdb2/tree/master/doc). 474 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security advisories 4 | 5 | Security advisories can be found on the 6 | [LoopBack website](https://loopback.io/doc/en/sec/index.html). 7 | 8 | ## Reporting a vulnerability 9 | 10 | If you think you have discovered a new security issue with any LoopBack package, 11 | **please do not report it on GitHub**. Instead, send an email to 12 | [security@loopback.io](mailto:security@loopback.io) with the following details: 13 | 14 | - Full description of the vulnerability. 15 | - Steps to reproduce the issue. 16 | - Possible solutions. 17 | 18 | If you are sending us any logs as part of the report, then make sure to redact 19 | any sensitive data from them. -------------------------------------------------------------------------------- /doc/advanced-queries.md: -------------------------------------------------------------------------------- 1 | # Advanced Queries 2 | 3 | This document is currently a work-in-progress. If you see something missing, 4 | please bring it to our attention, or open a Pull Request. 5 | 6 | Some queries using the Couchdb connector require steps that differ from 7 | those typically used within Loopback. 8 | 9 | ## Filtering 10 | 11 | ### Regex Filtering 12 | 13 | LoopBack filter uses `regexp` as the regular expression field name, the connector converts it to a Couchdb syntax name, which is `$regex`, then sends the corresponding query to database. Therefore, please provide `regexp` instead of `$regex` or `regex` in the filter of a LoopBack api, for example: 14 | 15 | ``` 16 | MyModel.find({where: { afieldname: {regexp: '^A'}}}, cb); 17 | ``` 18 | 19 | More details of the LoopBack syntax regexp filter, refer to [document of where filter](https://loopback.io/doc/en/lb2/Where-filter.html#regular-expressions) 20 | 21 | When filtering by `$regex` in Couchdb, you must provide at least one target field that can be filtered with an equality operator. 22 | 23 | See the [Couchdb Query Docs](http://docs.couchdb.org/en/2.0.0/api/database/find.html#selector-basics) 24 | for more information. 25 | 26 | The following equality operators are valid: 27 | `$gt`, `$lt`, `$eq`, `$gte`, and `$lte` 28 | 29 | If you do not provide this as a part of the query, the connector will automatically 30 | add a filter to your query using the `_id` field. 31 | Example: 32 | ``` 33 | // This query... 34 | { 35 | "selector": { 36 | "afieldname": { 37 | "$regex": "^A" 38 | } 39 | } 40 | } 41 | 42 | // ...will be transformed into this query: 43 | { 44 | "selector": { 45 | "_id": { 46 | "$gt": null 47 | }, 48 | "afieldname": { 49 | "$regex": "^A" 50 | } 51 | } 52 | } 53 | ``` 54 | ### Nested Filtering 55 | 56 | Couchdb connector accepts the nested property as a single property of fields joined by `.`. 57 | 58 | Example: a `Customer` model has a nested property `address.tags.tag`, the correct filter would be: 59 | ```js 60 | // Correct 61 | {where: {address.tags.tag: 'home'}} 62 | ``` 63 | not 64 | ```js 65 | // Wrong 66 | {where: {address: {tags.tag: 'home'}}} 67 | ``` 68 | or 69 | ```js 70 | // Wrong 71 | {where: {address: {tags: {tag: 'home'}}}} 72 | ``` 73 | 74 | #### Filtering array type property 75 | 76 | When a field in a nested property is an array, for example: 77 | 78 | In `field1.field2.field3`, `field2` is an array, Couchdb requires an operator `$elemMatch` after it to make the query work: `field1.field2.$elemMatch.field3`. 79 | 80 | [Loopback filtering nested properties](http://loopback.io/doc/en/lb3/Querying-data.html#filtering-nested-properties) explains how to define nested properties in a model. 81 | 82 | To make it consistent with other connectors, user don't need to add `$elemMatch` if they define the type of each nested property in the model properly. Couchdb connector detects their data type then inserts an `$elemMatch` for array property. So take the example above, `field1.field2.field3` will work. 83 | 84 | In case any of the nested property's type is not detectable, user still have the flexibility to provide a completed query that matches Couchdb's criteria: `field1.field2.$elemMatch.field3`. The connector will send that original query to the database. For details, refer to [Couchdb Query Combination Operator](http://docs.couchdb.org/en/2.0.0/api/database/find.html#combination-operators) 85 | 86 | ### Ordering 87 | 88 | Couchdb only supports sort by indexable field, which means when user provide a Loopback filter with `{order: 'afield'}`, they must make sure `afield` is defined as an indexable property in the model definition. 89 | 90 | For more details about the Couchdb sort syntax, please check [Couchdb Query Sort Syntax](http://docs.couchdb.org/en/2.0.0/api/database/find.html#sort-syntax) -------------------------------------------------------------------------------- /doc/multiple-db-instances.md: -------------------------------------------------------------------------------- 1 | # Multiple database instance 2 | 3 | Whenever the connector calls a driver method inside a model level function, it first detects the datasource that model attached to, then gets the driver instance in that datasource, instead of just calling `this.methodName`. 4 | 5 | For example, in function `Couchdb.prototype.destroy`, we call driver function by [`mo.db.destroy`](https://github.com/strongloop/loopback-connector-couchdb2/blob/cbd3ecb70f9ebf0445ee8dd4caf95bfe1df6882a/lib/couchdb.js#L372), `mo` is the model. 6 | 7 | More code example & test case to demo/verify this feature are in progress. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const SG = require('strong-globalize'); 9 | SG.SetRootDir(__dirname); 10 | 11 | module.exports = require('./lib/couchdb'); 12 | -------------------------------------------------------------------------------- /intl/MSG.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "38dd36a9798bf63308e55de0beec2b86": "CouchDB {{regex}} syntax does not support global", 4 | "3cde8cc9bca22c67278b202ab0720106": "No instance with id {0} found for {1}", 5 | "934c33143a59fd286ff6f7d0b8fb6875": "Invalid settings: \"url\" OR \"username\" AND \"password\" required" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /intl/cs/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "Syntaxe CouchDB {{regex}} nepodporuje globální", 3 | "3cde8cc9bca22c67278b202ab0720106": "Žádná instance s ID {0} nebyla nalezena pro {1}", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "Neplatná nastavení: \"url\" NEBO \"username\" A \"password\" je povinné" 5 | } 6 | 7 | -------------------------------------------------------------------------------- /intl/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "CouchDB {{regex}} Syntax nicht unterstützungsglobal", 3 | "3cde8cc9bca22c67278b202ab0720106": "Kein Fall mit ID, den {0} {1} fand", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "Ungültige Einstellungen: \"url\" OR \"Benutzername\" und \"Kennwort\", die erforderlich sind" 5 | } 6 | -------------------------------------------------------------------------------- /intl/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "CouchDB {{regex}} syntax does not support global", 3 | "3cde8cc9bca22c67278b202ab0720106": "No instance with id {0} found for {1}", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "Invalid settings: \"url\" OR \"username\" AND \"password\" required" 5 | } 6 | -------------------------------------------------------------------------------- /intl/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "La sintaxis de CouchDB {{regex}} no hace soporte global", 3 | "3cde8cc9bca22c67278b202ab0720106": "Ningún ejemplo con identificador que {0} le buscó a {1}", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "Posiciones erróneas: \"URL\" O \"nombre de usuario\" Y \"contraseña\" necesitados" 5 | } 6 | -------------------------------------------------------------------------------- /intl/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "La syntaxe CouchDB {{regex}} ne fait pas le support global", 3 | "3cde8cc9bca22c67278b202ab0720106": "Aucune instance avec id que {0} a trouvée à {1}", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "Paramètres incorrects: \"Nom utilisateur\" ET \"mot de passe\" d'OR \"url\" demandés" 5 | } 6 | -------------------------------------------------------------------------------- /intl/it/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "La sintassi CouchDB {{regex}} fa non sostegno globale", 3 | "3cde8cc9bca22c67278b202ab0720106": "Nessun esempio con identificativo {0} ha trovato per {1}", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "Impostazioni non valide: \"url\" O \"nome utente\" E \"password\" richiesti" 5 | } 6 | -------------------------------------------------------------------------------- /intl/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "CouchDB{{regex}}シンタックスが支えません世界的です", 3 | "3cde8cc9bca22c67278b202ab0720106": "idをもつどんな例{0}見つけますのために{1}", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "無効な設定:「ユーザー名」と「パスワード」が必要とした「url」OR" 5 | } 6 | -------------------------------------------------------------------------------- /intl/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "CouchDB {{regex}} 구문은 지원 세계적이게 하지 않는다.", 3 | "3cde8cc9bca22c67278b202ab0720106": "{0}이 {1}을위해 찾아 준 id이 있는 실례이지 않기", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "효력이 없는 세팅: \"url와\" \"사용자 ID와\" 필요로 한\" \"패스워드\"" 5 | } 6 | -------------------------------------------------------------------------------- /intl/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "f93ce20d178e2d16f45ed0a1a117b990": "voorbeeld voltooid", 3 | "5bdcd0e36db5b1100819f9aa25e9bdf0": "Syntaxis van Cloudant-{{regex}} biedt geen ondersteuning voor global" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/pl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "Składnia {{regex}} programu CouchDB nie obsługuje atrybutu global", 3 | "3cde8cc9bca22c67278b202ab0720106": "Nie znaleziono instancji o identyfikatorze {0} dla {1}", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "Niepoprawne ustawienia: wymagane ustawienie \"url\" LUB \"username\" ORAZ \"password\"" 5 | } 6 | 7 | -------------------------------------------------------------------------------- /intl/pt/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "A sintaxe CouchDB {{regex}} faz não apoio global", 3 | "3cde8cc9bca22c67278b202ab0720106": "Nenhum exemplo com id {0} trovare para {1}", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "Delineamentos nulos: \"url\" OU \"nome usuário\" E \"senha\" requeridos" 5 | } 6 | -------------------------------------------------------------------------------- /intl/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "Синтаксис {{regex}} CouchDB не поддерживает глобальные параметры", 3 | "3cde8cc9bca22c67278b202ab0720106": "Не найден экземпляр с ИД {0} для {1}", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "Недопустимые параметры: требуются \"url\" ИЛИ \"username\" И \"password\"" 5 | } 6 | 7 | -------------------------------------------------------------------------------- /intl/tr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "f93ce20d178e2d16f45ed0a1a117b990": "örnek tamamlandı", 3 | "5bdcd0e36db5b1100819f9aa25e9bdf0": "Cloudant {{regex}} sözdizimi şunu desteklemiyor: genel" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/zh-Hans/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "CouchDB{{regex}}句法不做技术支持全球性", 3 | "3cde8cc9bca22c67278b202ab0720106": "{0}为{1}找到能乐有伊德的事例的", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "无效设置:“需要的“url”或者”username“\"与\"”密码" 5 | } 6 | -------------------------------------------------------------------------------- /intl/zh-Hant/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": "CouchDB{{regex}}句法不做技術支持全球性", 3 | "3cde8cc9bca22c67278b202ab0720106": "{0}為{1}找到能樂有以德的事例的", 4 | "934c33143a59fd286ff6f7d0b8fb6875": "無效設置:“需要的“url』或者』username“\"與\"』密碼" 5 | } 6 | -------------------------------------------------------------------------------- /intl/zz/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "38dd36a9798bf63308e55de0beec2b86": [ 3 | "g.warn:lib/couchdb.js:988" 4 | ], 5 | "3cde8cc9bca22c67278b202ab0720106": [ 6 | "g.f:lib/couchdb.js:232" 7 | ], 8 | "934c33143a59fd286ff6f7d0b8fb6875": [ 9 | "g.f:lib/couchdb.js:54" 10 | ], 11 | "$elemMatch": [ 12 | "newFields.push:lib/couchdb.js:1061" 13 | ], 14 | "CouchDB constructor settings: %j": [ 15 | "debug:lib/couchdb.js:48" 16 | ], 17 | "CouchDB.prototype.all %j %j %j": [ 18 | "debug:lib/couchdb.js:263" 19 | ], 20 | "CouchDB.prototype.all (findRecursive) results: %j %j": [ 21 | "debug:lib/couchdb.js:1091" 22 | ], 23 | "CouchDB.prototype.automigrate %j": [ 24 | "debug:lib/couchdb.js:797" 25 | ], 26 | "CouchDB.prototype.automigrate models %j": [ 27 | "debug:lib/couchdb.js:779" 28 | ], 29 | "CouchDB.prototype.autoupdate %j": [ 30 | "debug:lib/couchdb.js:732" 31 | ], 32 | "CouchDB.prototype.buildSort %j": [ 33 | "debug:lib/couchdb.js:308" 34 | ], 35 | "CouchDB.prototype.buildSort order: %j sort: %j": [ 36 | "debug:lib/couchdb.js:329" 37 | ], 38 | "CouchDB.prototype.bulkReplace %j %j": [ 39 | "debug:lib/couchdb.js:566" 40 | ], 41 | "CouchDB.prototype.connect": [ 42 | "debug:lib/couchdb.js:78" 43 | ], 44 | "CouchDB.prototype.count %j %j %j": [ 45 | "debug:lib/couchdb.js:410" 46 | ], 47 | "CouchDB.prototype.create %j %j %j ": [ 48 | "debug:lib/couchdb.js:189" 49 | ], 50 | "CouchDB.prototype.destroy %j %j %j": [ 51 | "debug:lib/couchdb.js:337" 52 | ], 53 | "CouchDB.prototype.destroy db.destroy %j %j": [ 54 | "debug:lib/couchdb.js:362" 55 | ], 56 | "CouchDB.prototype.destroyAll %j %j %j": [ 57 | "debug:lib/couchdb.js:381" 58 | ], 59 | "CouchDB.prototype.destroyAll db.destroy %j %j": [ 60 | "debug:lib/couchdb.js:391" 61 | ], 62 | "CouchDB.prototype.exists %j %j %j": [ 63 | "debug:lib/couchdb.js:426" 64 | ], 65 | "CouchDB.prototype.find %j %j %j": [ 66 | "debug:lib/couchdb.js:446" 67 | ], 68 | "CouchDB.prototype.insert %j %j": [ 69 | "debug:lib/couchdb.js:170" 70 | ], 71 | "CouchDB.prototype.ping": [ 72 | "debug:lib/couchdb.js:593" 73 | ], 74 | "CouchDB.prototype.ping results %j %j": [ 75 | "debug:lib/couchdb.js:595" 76 | ], 77 | "CouchDB.prototype.replaceById %j %j %j": [ 78 | "debug:lib/couchdb.js:653" 79 | ], 80 | "CouchDB.prototype.replaceOrCreate %j %j": [ 81 | "debug:lib/couchdb.js:611" 82 | ], 83 | "CouchDB.prototype.save %j %j %j": [ 84 | "debug:lib/couchdb.js:203" 85 | ], 86 | "CouchDB.prototype.selectModel use %j": [ 87 | "debug:lib/couchdb.js:704" 88 | ], 89 | "CouchDB.prototype.updateAll %j %j %j %j": [ 90 | "debug:lib/couchdb.js:529" 91 | ], 92 | "CouchDB.prototype.updateAttributes %j %j %j": [ 93 | "debug:lib/couchdb.js:465" 94 | ], 95 | "CouchDB.prototype.updateIndex -- modelName %s, idx %s": [ 96 | "debug:lib/couchdb.js:848" 97 | ], 98 | "CouchDB.prototype.updateIndex index %j %j": [ 99 | "debug:lib/couchdb.js:854" 100 | ], 101 | "CouchDB.prototype.updateOrCreate %j %j": [ 102 | "debug:lib/couchdb.js:490" 103 | ], 104 | "No documents returned for query: %s": [ 105 | "util.format:lib/couchdb.js:1105" 106 | ], 107 | "Unable to update 1 or more ": [ 108 | "util.format:lib/couchdb.js:547", 109 | "util.format:lib/couchdb.js:579" 110 | ], 111 | "_id:number": [ 112 | "f.hasOwnProperty:lib/couchdb.js:1149" 113 | ], 114 | "automigrate iterate props, propertyName %s value %s": [ 115 | "debug:lib/couchdb.js:745" 116 | ], 117 | "could not find matching item in database!": [ 118 | "Error:lib/couchdb.js:367" 119 | ], 120 | "createIndex: ddocName %s, indexName %s, fields %s": [ 121 | "debug:lib/couchdb.js:1239" 122 | ], 123 | "document(s): %s": [ 124 | "util.format:lib/couchdb.js:548", 125 | "util.format:lib/couchdb.js:580" 126 | ], 127 | "findRecursive query: %s": [ 128 | "debug:lib/couchdb.js:1104" 129 | ], 130 | "instance method destroy tries to delete more than one item!": [ 131 | "Error:lib/couchdb.js:359" 132 | ], 133 | "ping failed": [ 134 | "cb:lib/couchdb.js:596" 135 | ], 136 | "CouchDB2.prototype.discoverModelDefinitions %j": [ 137 | "debug:lib/discovery.js:20" 138 | ], 139 | "CouchDB2.prototype.discoverModelDefinitions %j %j": [ 140 | "debug:lib/discovery.js:27" 141 | ], 142 | "CouchDB2.prototype.discoverSchemas %j %j": [ 143 | "debug:lib/discovery.js:39" 144 | ], 145 | "CouchDB2.prototype.view ddocName %s viewName %s options %s": [ 146 | "debug:lib/view.js:40" 147 | ], 148 | "/_all_dbs": [ 149 | "waitFor:test.js:38" 150 | ], 151 | "2s": [ 152 | "ms:test.js:36" 153 | ], 154 | "5s": [ 155 | "ms:test.js:30" 156 | ], 157 | "cleaning up container: %s": [ 158 | "console.log:test.js:203" 159 | ], 160 | "creating db: %j": [ 161 | "console.log:test.js:170" 162 | ], 163 | "end": [ 164 | "res.on:test.js:146", 165 | "res.on:test.js:174" 166 | ], 167 | "env:": [ 168 | "console.log:test.js:114" 169 | ], 170 | "error": [ 171 | "res.on:test.js:145", 172 | "res.on:test.js:173" 173 | ], 174 | "error cleaning up:": [ 175 | "console.error:test.js:44" 176 | ], 177 | "error running tests:": [ 178 | "console.error:test.js:47" 179 | ], 180 | "failed to contact CouchDB": [ 181 | "Error:test.js:141" 182 | ], 183 | "klaemo/couchdb:2.0.0": [ 184 | "dockerStart:test.js:35" 185 | ], 186 | "mocha exited with code: %j, sig: %j": [ 187 | "fmt:test.js:192" 188 | ], 189 | "mocha/bin/_mocha": [ 190 | "require.resolve:test.js:17" 191 | ], 192 | "ping (%d/%d)": [ 193 | "console.log:test.js:139" 194 | ], 195 | "pulling image: %s": [ 196 | "console.log:test.js:68" 197 | ], 198 | "recording container for later cleanup: ": [ 199 | "console.log:test.js:85" 200 | ], 201 | "running mocha...": [ 202 | "console.log:test.js:185" 203 | ], 204 | "starting container from image: %s": [ 205 | "console.log:test.js:74" 206 | ], 207 | "test-db": [ 208 | "createDB:test.js:39" 209 | ], 210 | "waiting for instance to respond": [ 211 | "console.log:test.js:135" 212 | ] 213 | } 214 | -------------------------------------------------------------------------------- /intl/zz/messages_inverted.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib/couchdb.js": { 3 | "48": [ 4 | "debug('CouchDB constructor settings: %j', ... )" 5 | ], 6 | "54": [ 7 | "g.f('934c33143a59fd286ff6f7d0b8fb6875')" 8 | ], 9 | "78": [ 10 | "debug('CouchDB.prototype.connect')" 11 | ], 12 | "170": [ 13 | "debug('CouchDB.prototype.insert %j %j', ... )" 14 | ], 15 | "189": [ 16 | "debug('CouchDB.prototype.create %j %j %j ', ... )" 17 | ], 18 | "203": [ 19 | "debug('CouchDB.prototype.save %j %j %j', ... )" 20 | ], 21 | "232": [ 22 | "g.f('3cde8cc9bca22c67278b202ab0720106')" 23 | ], 24 | "263": [ 25 | "debug('CouchDB.prototype.all %j %j %j', ... )" 26 | ], 27 | "308": [ 28 | "debug('CouchDB.prototype.buildSort %j', ... )" 29 | ], 30 | "329": [ 31 | "debug('CouchDB.prototype.buildSort order: %j sort: %j', ... )" 32 | ], 33 | "337": [ 34 | "debug('CouchDB.prototype.destroy %j %j %j', ... )" 35 | ], 36 | "359": [ 37 | "Error('instance method destroy tries to delete more than one item!')" 38 | ], 39 | "362": [ 40 | "debug('CouchDB.prototype.destroy db.destroy %j %j', ... )" 41 | ], 42 | "367": [ 43 | "Error('could not find matching item in database!')" 44 | ], 45 | "381": [ 46 | "debug('CouchDB.prototype.destroyAll %j %j %j', ... )" 47 | ], 48 | "391": [ 49 | "debug('CouchDB.prototype.destroyAll db.destroy %j %j', ... )" 50 | ], 51 | "410": [ 52 | "debug('CouchDB.prototype.count %j %j %j', ... )" 53 | ], 54 | "426": [ 55 | "debug('CouchDB.prototype.exists %j %j %j', ... )" 56 | ], 57 | "446": [ 58 | "debug('CouchDB.prototype.find %j %j %j', ... )" 59 | ], 60 | "465": [ 61 | "debug('CouchDB.prototype.updateAttributes %j %j %j', ... )" 62 | ], 63 | "490": [ 64 | "debug('CouchDB.prototype.updateOrCreate %j %j', ... )" 65 | ], 66 | "529": [ 67 | "debug('CouchDB.prototype.updateAll %j %j %j %j', ... )" 68 | ], 69 | "547": [ 70 | "util.format('Unable to update 1 or more ')" 71 | ], 72 | "548": [ 73 | "util.format('document(s): %s', ... )" 74 | ], 75 | "566": [ 76 | "debug('CouchDB.prototype.bulkReplace %j %j', ... )" 77 | ], 78 | "579": [ 79 | "util.format('Unable to update 1 or more ')" 80 | ], 81 | "580": [ 82 | "util.format('document(s): %s', ... )" 83 | ], 84 | "593": [ 85 | "debug('CouchDB.prototype.ping')" 86 | ], 87 | "595": [ 88 | "debug('CouchDB.prototype.ping results %j %j', ... )" 89 | ], 90 | "596": [ 91 | "cb('ping failed')" 92 | ], 93 | "611": [ 94 | "debug('CouchDB.prototype.replaceOrCreate %j %j', ... )" 95 | ], 96 | "653": [ 97 | "debug('CouchDB.prototype.replaceById %j %j %j', ... )" 98 | ], 99 | "704": [ 100 | "debug('CouchDB.prototype.selectModel use %j', ... )" 101 | ], 102 | "732": [ 103 | "debug('CouchDB.prototype.autoupdate %j', ... )" 104 | ], 105 | "745": [ 106 | "debug('automigrate iterate props, propertyName %s value %s', ... )" 107 | ], 108 | "779": [ 109 | "debug('CouchDB.prototype.automigrate models %j', ... )" 110 | ], 111 | "797": [ 112 | "debug('CouchDB.prototype.automigrate %j', ... )" 113 | ], 114 | "848": [ 115 | "debug('CouchDB.prototype.updateIndex -- modelName %s, idx %s', ... )" 116 | ], 117 | "854": [ 118 | "debug('CouchDB.prototype.updateIndex index %j %j', ... )" 119 | ], 120 | "988": [ 121 | "g.warn('38dd36a9798bf63308e55de0beec2b86')" 122 | ], 123 | "1061": [ 124 | "newFields.push('$elemMatch')" 125 | ], 126 | "1091": [ 127 | "debug('CouchDB.prototype.all (findRecursive) results: %j %j', ... )" 128 | ], 129 | "1104": [ 130 | "debug('findRecursive query: %s', ... )" 131 | ], 132 | "1105": [ 133 | "util.format('No documents returned for query: %s', ... )" 134 | ], 135 | "1149": [ 136 | "f.hasOwnProperty('_id:number')" 137 | ], 138 | "1239": [ 139 | "debug('createIndex: ddocName %s, indexName %s, fields %s', ... )" 140 | ] 141 | }, 142 | "lib/discovery.js": { 143 | "20": [ 144 | "debug('CouchDB2.prototype.discoverModelDefinitions %j', ... )" 145 | ], 146 | "27": [ 147 | "debug('CouchDB2.prototype.discoverModelDefinitions %j %j', ... )" 148 | ], 149 | "39": [ 150 | "debug('CouchDB2.prototype.discoverSchemas %j %j', ... )" 151 | ] 152 | }, 153 | "lib/view.js": { 154 | "40": [ 155 | "debug('CouchDB2.prototype.view ddocName %s viewName %s options %s', ... )" 156 | ] 157 | }, 158 | "test.js": { 159 | "17": [ 160 | "require.resolve('mocha/bin/_mocha')" 161 | ], 162 | "30": [ 163 | "ms('5s')" 164 | ], 165 | "35": [ 166 | "dockerStart('klaemo/couchdb:2.0.0')" 167 | ], 168 | "36": [ 169 | "ms('2s')" 170 | ], 171 | "38": [ 172 | "waitFor('/_all_dbs')" 173 | ], 174 | "39": [ 175 | "createDB('test-db')" 176 | ], 177 | "44": [ 178 | "console.error('error cleaning up:')" 179 | ], 180 | "47": [ 181 | "console.error('error running tests:')" 182 | ], 183 | "68": [ 184 | "console.log('pulling image: %s', ... )" 185 | ], 186 | "74": [ 187 | "console.log('starting container from image: %s', ... )" 188 | ], 189 | "85": [ 190 | "console.log('recording container for later cleanup: ')" 191 | ], 192 | "114": [ 193 | "console.log('env:')" 194 | ], 195 | "135": [ 196 | "console.log('waiting for instance to respond')" 197 | ], 198 | "139": [ 199 | "console.log('ping (%d/%d)', ... )" 200 | ], 201 | "141": [ 202 | "Error('failed to contact CouchDB')" 203 | ], 204 | "145": [ 205 | "res.on('error')" 206 | ], 207 | "146": [ 208 | "res.on('end')" 209 | ], 210 | "170": [ 211 | "console.log('creating db: %j', ... )" 212 | ], 213 | "173": [ 214 | "res.on('error')" 215 | ], 216 | "174": [ 217 | "res.on('end')" 218 | ], 219 | "185": [ 220 | "console.log('running mocha...')" 221 | ], 222 | "192": [ 223 | "fmt('mocha exited with code: %j, sig: %j', ... )" 224 | ], 225 | "203": [ 226 | "console.log('cleaning up container: %s', ... )" 227 | ] 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /lib/discovery.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | module.exports = mixinDiscovery; 9 | 10 | function mixinDiscovery(CouchDB2) { 11 | const debug = require('debug')('loopback:connector:couchdb2:discovery'); 12 | 13 | /** 14 | * Discover model definitions 15 | * 16 | * @param {Object} options Options for discovery 17 | * @param {Function} [cb] The callback function 18 | */ 19 | CouchDB2.prototype.discoverModelDefinitions = function(options, cb) { 20 | debug('CouchDB2.prototype.discoverModelDefinitions %j', options); 21 | 22 | if (!cb && typeof options === 'function') { 23 | cb = options; 24 | } 25 | 26 | this.db.list(function(err, dbs) { 27 | debug('CouchDB2.prototype.discoverModelDefinitions %j %j', err, dbs); 28 | if (err) cb(err); 29 | cb(null, dbs); 30 | }); 31 | }; 32 | 33 | /** 34 | * @param {string} dbname The database name 35 | * @param {Object} options The options for discovery 36 | * @param {Function} [cb] The callback function 37 | */ 38 | CouchDB2.prototype.discoverSchemas = function(dbname, options, cb) { 39 | debug('CouchDB2.prototype.discoverSchemas %j %j', dbname, options); 40 | const schema = { 41 | name: dbname, 42 | options: { 43 | idInjection: true, 44 | dbName: dbname, 45 | }, 46 | properties: {}, 47 | }; 48 | options.visited = options.visited || {}; 49 | if (!options.visited.hasOwnProperty(dbname)) { 50 | options.visited[dbname] = schema; 51 | } 52 | if (cb) cb(null, options.visited); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /lib/migrate.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const async = require('async'); 9 | const g = require('strong-globalize')(); 10 | const util = require('util'); 11 | const _ = require('lodash'); 12 | const inspect = util.inspect; 13 | 14 | module.exports = mixinMigrate; 15 | 16 | function mixinMigrate(CouchDB) { 17 | const debug = require('debug')('loopback:connector:couchdb2:migrate'); 18 | 19 | /** 20 | * Perform automigrate for the given models. 21 | * - destroy the model data if the model exists 22 | * - delete existing indexes in database 23 | * - create new indexes according to model definition 24 | * 25 | * @param {String|String[]} [models] A model name or an array of model names. 26 | * If not present, apply to all models 27 | * @callback {Function} cb The callback function 28 | */ 29 | CouchDB.prototype.automigrate = function(models, cb) { 30 | debug('CouchDB.prototype.automigrate models %j', models); 31 | const self = this; 32 | const existingModels = models; 33 | async.series([ 34 | function(callback) { 35 | destroyData(callback); 36 | }, 37 | function(callback) { 38 | debug('CouchDB.prototype.automigrate update indexes for %j', models); 39 | self.migrateOrUpdateIndex(models, true, callback); 40 | }], cb); 41 | function destroyData(destroyCb) { 42 | async.eachSeries(existingModels, function(model, cb) { 43 | self.destroyAll(model, {}, {}, cb); 44 | }, function(err) { 45 | debug('CouchDB.prototype.automigrate destroy all data has error: %j', 46 | err); 47 | destroyCb(err); 48 | }); 49 | } 50 | }; 51 | 52 | /** 53 | * Perform autoupdate for the given models. 54 | * It does NOT destroy previous model instances if model exists, only 55 | * `automigrate` does that. 56 | * - compare new indexes and existing indexes 57 | * - keep unchanged indexes 58 | * - add newly defined indexes 59 | * - delete old indexes 60 | * @param {String[]} [models] A model name or an array of model names. If not 61 | * present, apply to all models 62 | * @callback {Function} cb The callback function 63 | */ 64 | 65 | CouchDB.prototype.autoupdate = function(models, cb) { 66 | this.migrateOrUpdateIndex(models, false, cb); 67 | }; 68 | 69 | /** 70 | * Add and delete certain indexes, performs depends on which function calls it. 71 | * 72 | * @param {String[]} [models] A model name or an array of model names. Passed in 73 | * from `automigrate` or `autoupdate` 74 | * @param {boolean} isMigrate 75 | * - `true` when called by `automigrate` 76 | * - `false` when called by `autoupdate` 77 | * 78 | * @callback {Function} cb The callback function 79 | */ 80 | 81 | CouchDB.prototype.migrateOrUpdateIndex = function(models, isMigrate, cb) { 82 | debug('CouchDB.prototype.migrateOrUpdateIndex %j', models); 83 | 84 | const self = this; 85 | async.each(models, autoUpdateOneModel, cb); 86 | 87 | function autoUpdateOneModel(model, cb) { 88 | debug('CouchDB.prototype.migrateOrUpdateIndex updating model: %j', model); 89 | const mo = self.selectModel(model, true); 90 | if (!mo) return cb(new Error('model ' + model + ' does not exist in your registry!')); 91 | const indexes = mo.mo.model.definition.indexes(); 92 | 93 | self.buildModelViewIndex(mo, indexes); 94 | self.getModifyIndexes(mo, indexes, isMigrate, function(err, results) { 95 | debug('start drop and add indexes %j for model %j', results, model); 96 | if (err) return cb(err); 97 | if (typeof results !== 'object') 98 | return cb(new Error('results of modified indexes must be an object!')); 99 | 100 | async.series([ 101 | function dropIndexes(cb) { 102 | removeIndexes(results.indexesToDrop, cb); 103 | }, 104 | function addIndexes(cb) { 105 | createIndexes(results.indexesToAdd, cb); 106 | }, 107 | ], cb); 108 | }); 109 | 110 | function createIndexes(indexes, cb) { 111 | if (typeof indexes !== 'object') return cb(new Error('indexes to create must be an object!')); 112 | 113 | async.eachOf(indexes, create, cb); 114 | // {indexName: "foo"} or {indexName: [{"foo": "asc"}, {"bar": "asc"}]} 115 | function create(value, key, cb) { 116 | self._createIndex(mo, model, key, value, cb); 117 | } 118 | } 119 | 120 | function removeIndexes(indexes, cb) { 121 | if (typeof indexes !== 'object') return cb(new Error('indexes to drop must be an object!')); 122 | async.eachOf(indexes, removeIndex, cb); 123 | // {ddoc: "_design/ddocName", name: "indexName"} 124 | function removeIndex(value, key, cb) { 125 | self.deleteIndex(mo, value.ddoc, cb); 126 | } 127 | } 128 | } 129 | }; 130 | 131 | /** 132 | * Used in function `migrateOrUpdateIndex`. 133 | * Create index with index fields. 134 | * @param {Object} mo The model configuration. 135 | * @param {String} model The model name. 136 | * @param {String} name The index name. 137 | * @param {Object} indexObj The index object e.g. [{"foo": "asc"}, {"bar": "asc"}] 138 | * @param {Function} cb 139 | */ 140 | CouchDB.prototype._createIndex = function(mo, model, name, fields, cb) { 141 | const self = this; 142 | fields = self.coerceIndexFields(fields); 143 | self.addModelViewToIndex(mo.modelView, fields); 144 | // naming convertion: '_design/LBModel__Foo__LBIndex__foo_index', 145 | // here the driver api takes in the name without prefix '_design/' 146 | const config = { 147 | ddocName: self.getIndexModelPrefix(mo) + '__' + model + '__' + 148 | self.getIndexPropertyPrefix(mo) + '__' + name, 149 | indexName: name, 150 | fields: fields, 151 | }; 152 | self.createIndex(config.ddocName, config.indexName, config.fields, cb); 153 | }; 154 | 155 | /** 156 | * Add an index for modelView property to indexes got from modelDef 157 | * @param {Object} modelObject generated by CouchDB.prototype.selectModel 158 | * @param {Object} indexes the modelDef indexes of a model 159 | * 160 | */ 161 | CouchDB.prototype.buildModelViewIndex = function(modelObject, indexes) { 162 | indexes[modelObject.modelView + '_index'] = true; 163 | }; 164 | 165 | /** 166 | * Coerce the index fields to be either ALL ASC or ALL DESC if conflict exists. 167 | * If the fields are specified with different orders: 168 | * - select the order that is on the first field 169 | * - force all fields using that order 170 | * - print warning for coerced fields' names 171 | * 172 | * @param {Array} fields The fields defined in an index, example: 173 | * ```js 174 | * [{foo: 'asc'}, {bar: 'desc'}] 175 | * ``` 176 | * @returns {Array} The coerced fields 177 | */ 178 | CouchDB.prototype.coerceIndexFields = function(fields) { 179 | if (fields.length <= 1) return fields; 180 | const defaultOrder = this.getDefaultOrder(fields); 181 | const coercedFields = []; 182 | 183 | const result = _.map(fields, coerceOrder); 184 | if (coercedFields.length > 0) { 185 | printWarning(); 186 | } 187 | return result; 188 | 189 | function coerceOrder(field) { 190 | if (field[Object.keys(field)[0]] === defaultOrder) 191 | return field; 192 | field[Object.keys(field)[0]] = defaultOrder; 193 | coercedFields.push(Object.keys(field)[0]); 194 | return field; 195 | } 196 | 197 | function printWarning() { 198 | const couchRule = 'Couchdb does NOT allow composite indexes with conflicting sort directions, ' + 199 | 'please see http://docs.couchdb.org/en/2.0.0/api/database/find.html#db-index for details.' + '\n'; 200 | const connectorStrategy = 'The index will be created using ' + defaultOrder + ' order, specified by: '; 201 | g.warn(couchRule + connectorStrategy + '%s', coercedFields.join(',')); 202 | } 203 | }; 204 | 205 | /** 206 | * Add model view property to each created index, because we include it in 207 | * the selector when send query. 208 | * It is appended as the last element in the fields, and the direction is identical 209 | * to the coreced one. 210 | * 211 | * @param {String} modelView the model view name, get from `mo.modelView` 212 | */ 213 | 214 | CouchDB.prototype.addModelViewToIndex = function(modelView, fields) { 215 | debug('addModelViewIndex: modelView %s, fields %s', modelView, fields); 216 | 217 | if (fields.length < 1) return fields.push({modelView: 'asc'}); 218 | const modelViewExistInField = _.findIndex(fields, function(o) { return !!o[modelView]; }) > -1; 219 | if (modelViewExistInField) return fields; 220 | 221 | const defaultOrder = this.getDefaultOrder(fields); 222 | const modelViewIndex = {}; 223 | modelViewIndex[modelView] = defaultOrder; 224 | fields.push(modelViewIndex); 225 | return fields; 226 | }; 227 | 228 | /** 229 | * The default order direction is the one specified for the first property in `fields` 230 | * 231 | * @param {Array} an array of fields to be put in an index 232 | */ 233 | 234 | CouchDB.prototype.getDefaultOrder = function(fields) { 235 | const firstProperty = fields[0]; 236 | return firstProperty[Object.keys(firstProperty)[0]]; 237 | }; 238 | 239 | /** 240 | * Create an index in couchdb, you can specify the ddocName and indexName 241 | * @param {String} ddocName design doc name with prefix '_design/' 242 | * @param {String} indexName index name 243 | * @param {Array} fields example format: [{field1: 'asc'}, {field2: 'asc'}] 244 | * @callback {Function} cb The callback function 245 | */ 246 | CouchDB.prototype.createIndex = function(ddocName, indexName, fields, cb) { 247 | debug('createIndex: ddocName %s, indexName %s, fields %s', ddocName, 248 | indexName, fields); 249 | 250 | if (!Array.isArray(fields)) return cb(new Error('fields in the index must be an array!')); 251 | if (typeof ddocName !== 'string') return cb(new Error('ddocName of index must be a string!')); 252 | if (typeof indexName !== 'string') return cb(new Error('indexName in the index must be a string!')); 253 | 254 | const self = this; 255 | const indexBody = { 256 | index: { 257 | fields: fields, 258 | }, 259 | ddoc: ddocName, 260 | name: indexName, 261 | type: 'json', 262 | }; 263 | 264 | const requestObject = { 265 | db: self.settings.database, 266 | path: '_index', 267 | method: 'post', 268 | body: indexBody, 269 | }; 270 | 271 | self.getDriverInst().request(requestObject, cb); 272 | }; 273 | 274 | /** 275 | * Return modify index results with: `indexesToDrop`, `indexesToAdd` 276 | * @param {Object} mo the model object returned by this.selectModel(modelName) 277 | * @param {Object} newLBIndexes returned from juggler's modelDefinition.indexes() 278 | * @param {Boolean} isMigrate flag to tell do we want to compare new/old indexes 279 | * @callback {Function} cb The callback function 280 | * @param {Object} result indexes to modify in the following format: 281 | * ```js 282 | * result: { 283 | * indexesToAdd: { 284 | * foo_index: [{foo: 'asc'}], 285 | * bar_index: [{bar1: 'asc'}, {bar2: 'asc'}] 286 | * }, 287 | * indexesToDrop: { 288 | * foobar_index: { 289 | * ddoc: '_design/LBModel__Foo__LBIndex__foobar_index', 290 | * fields: [{foo: 'asc'}, {bar: 'asc'}] 291 | * } 292 | * } 293 | * } 294 | * ``` 295 | */ 296 | CouchDB.prototype.getModifyIndexes = function(mo, newLBIndexes, isMigrate, cb) { 297 | debug('CouchDB.prototype.getModifyIndexes'); 298 | if (typeof newLBIndexes !== 'object') return cb(new Error('indexes from modelDef must be an object!')); 299 | 300 | const self = this; 301 | let results = {}; 302 | // `newLBIndexes` is generated from modelDef, convert it to the format we need and 303 | // store in `newIndexes` 304 | let newIndexes = {}; 305 | const modelName = mo.mo.model.modelName; 306 | 307 | const newModelIndexes = _.pickBy(newLBIndexes, function(value, index) { 308 | if (value.keys) 309 | return newLBIndexes[index]; 310 | }); 311 | const newPropertyIndexes = _.pickBy(newLBIndexes, function(value, index) { 312 | if (!value.keys) 313 | return newLBIndexes[index]; 314 | }); 315 | 316 | newIndexes = _.merge(newIndexes, self._generateModelLevelIndexes(newModelIndexes)); 317 | newIndexes = _.merge(newIndexes, self._generatePropertyLevelIndexes(newPropertyIndexes)); 318 | 319 | // Call `getModelIndexes` to get existing indexes. 320 | self.getModelIndexes(modelName, function(err, oldIndexes) { 321 | if (err) return cb(err); 322 | if (isMigrate) { 323 | results.indexesToAdd = newIndexes; 324 | results.indexesToDrop = oldIndexes; 325 | } else { 326 | results = self.compare(newIndexes, oldIndexes); 327 | } 328 | debug('getModifyIndexes results: %s', inspect(results, {depth: 4})); 329 | cb(null, results); 330 | }); 331 | }; 332 | 333 | /** 334 | * Used in function `getModifyIndexes()`. 335 | * Generate indexes for model properties that are configured as 336 | * `{index: true}` 337 | * @param {Object} indexes indexes from model config, retrieved in 338 | * `getModifyIndexes()` 339 | */ 340 | CouchDB.prototype._generatePropertyLevelIndexes = function(indexes) { 341 | const results = {}; 342 | for (const key in indexes) { 343 | const field = {}; 344 | // By default the order will be `asc`, 345 | // please create Model level index if you need `desc` 346 | field[key.split('_index')[0]] = 'asc'; 347 | const fields = [field]; 348 | results[key] = fields; 349 | } 350 | return results; 351 | }; 352 | 353 | /** 354 | * Used in function `getModifyIndexes()`. 355 | * Generate indexes for indexes defined in the model config. 356 | * @param {Object} indexes indexes from model config, provided by 357 | * `getModifyIndexes()` 358 | */ 359 | CouchDB.prototype._generateModelLevelIndexes = function(indexes, cb) { 360 | const results = {}; 361 | for (const key in indexes) { 362 | const keys = indexes[key].keys; 363 | if (!keys || typeof keys !== 'object') return cb(new Error( 364 | 'the keys in your model index are not well defined! please see' + 365 | 'https://loopback.io/doc/en/lb3/Model-definition-JSON-file.html#indexes', 366 | )); 367 | 368 | const fields = []; 369 | _.forEach(keys, function(value, key) { 370 | const obj = {}; 371 | let order; 372 | if (keys[key] === 1) order = 'asc'; 373 | else order = 'desc'; 374 | obj[key] = order; 375 | fields.push(obj); 376 | }); 377 | results[key] = fields; 378 | } 379 | return results; 380 | }; 381 | 382 | /** 383 | * Perform the indexes comparison for `autoupdate`. 384 | * @param {Object} newIndexes 385 | * newIndexes in format: 386 | * ```js 387 | * { 388 | * indexName: [{afield: 'asc'}], 389 | * compositeIndexName: [{field1: 'asc'}, {field2: 'asc'}] 390 | * } 391 | * ``` 392 | * @param {Object} oldIndexes 393 | * oldIndexes in format: 394 | * ```js 395 | * { 396 | * indexName: { 397 | * ddoc: '_design/LBModel__Foo__LBIndex__bar_index', 398 | * fields: [{afield: 'asc'}], 399 | * } 400 | * } 401 | * ``` 402 | * @callback {Function} cb The callback function 403 | * @param {Object} result indexes to add and drop after comparison 404 | * ```js 405 | * result: {indexesToAdd: {$someIndexes}, indexesToDrop: {$someIndexes}} 406 | * ``` 407 | */ 408 | CouchDB.prototype.compare = function(newIndexes, oldIndexes, cb) { 409 | debug('CouchDB.prototype.compare'); 410 | const result = {}; 411 | let indexesToDrop = {}; 412 | let indexesToAdd = {}; 413 | let iAdd; 414 | for (const niKey in newIndexes) { 415 | if (!oldIndexes.hasOwnProperty(niKey)) { 416 | // Add item to `indexesToAdd` if it's new 417 | iAdd = {}; 418 | iAdd[niKey] = newIndexes[niKey]; 419 | indexesToAdd = _.merge(indexesToAdd, iAdd); 420 | } else { 421 | if (arrEqual(newIndexes[niKey], oldIndexes[niKey].fields)) { 422 | // Don't change it if index already exists 423 | delete oldIndexes[niKey]; 424 | } else { 425 | // Update index if fields change 426 | iAdd = {}; 427 | const iDrop = {}; 428 | iAdd[niKey] = newIndexes[niKey]; 429 | indexesToAdd = _.merge(indexesToAdd, iAdd); 430 | } 431 | 432 | function arrEqual(arr1, arr2) { 433 | if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false; 434 | let isEqual = true; 435 | for (const item in arr1) { 436 | const i = _.findIndex(arr2, arr1[item]); 437 | isEqual = isEqual && (i > -1); 438 | } 439 | return isEqual; 440 | } 441 | } 442 | } 443 | indexesToDrop = oldIndexes; 444 | result.indexesToAdd = indexesToAdd; 445 | result.indexesToDrop = indexesToDrop; 446 | return result; 447 | }; 448 | 449 | /** 450 | * Get all indexes of a model. 451 | * 452 | * @param {String} model The model name 453 | * @callback {Function} cb The callback function 454 | * @param {Object} existingIndexes indexes in database that belongs to the model 455 | * - existingIndexes format see jsdoc of `CouchDB.prototype.parseIndexes` 456 | */ 457 | CouchDB.prototype.getModelIndexes = function(model, cb) { 458 | debug('CouchDB.prototype.getModelIndexes: %j', model); 459 | const self = this; 460 | const mo = self.selectModel(model); 461 | const dbName = mo.dbName; 462 | self.getIndexes(dbName, function(err, result) { 463 | if (err) return cb(err); 464 | const ddocsOfModel = _.filter(result.indexes, isSameModel); 465 | function isSameModel(item) { 466 | let isSame = false; 467 | if (item.ddoc !== null) { 468 | // slice the '_design/' 469 | const ddocName = item.ddoc.slice(8); 470 | // need to be careful on the subString comparison based on 471 | // naming convertion, avoid one index belongs to two models 472 | const modelIndexName = self.getIndexModelPrefix(mo) + '__' + model + '__' + 473 | self.getIndexPropertyPrefix(mo); 474 | if (ddocName.indexOf(modelIndexName) === 0) 475 | isSame = true; 476 | } 477 | return isSame; 478 | } 479 | 480 | const existingIndexes = self.parseIndexes(ddocsOfModel); 481 | cb(null, existingIndexes); 482 | }); 483 | }; 484 | 485 | /** 486 | * Parse the raw index object returned from database 487 | * to the format we need in connector 488 | * Example: 489 | * 490 | * raw index object: 491 | * ```js 492 | * { 493 | * ddoc: "_design/LBModel__User__LBIndex__name", 494 | * name: "name_index", 495 | * def: { 496 | * fields: [ 497 | * {firstName: "asc"}, 498 | * {lastName: "asc"} 499 | * ] 500 | * } 501 | * } 502 | * ``` 503 | * converts to the format: 504 | * { 505 | * name_index: { 506 | * ddoc: "_design/LBModel__User__LBIndex__name", 507 | * fields: [ 508 | * {firstName: "asc"}, 509 | * {lastName: "asc"} 510 | * ] 511 | * } 512 | * } 513 | * 514 | * @param {Object} indexes Raw index object returned from database 515 | * @returns {Object} results The parsed indexes 516 | */ 517 | CouchDB.prototype.parseIndexes = function(indexes) { 518 | const results = {}; 519 | for (const item in indexes) { 520 | const value = indexes[item]; 521 | results[value.name] = { 522 | ddoc: value.ddoc, 523 | fields: value.def.fields, 524 | }; 525 | } 526 | return results; 527 | }; 528 | 529 | /** 530 | * Get all indexes in a database. 531 | * @param {String} dbName a database name 532 | * @callback {Function} cb The callback function 533 | */ 534 | CouchDB.prototype.getIndexes = function(dbName, cb) { 535 | const self = this; 536 | const requestObject = { 537 | db: dbName, 538 | path: '_index', 539 | method: 'get', 540 | }; 541 | 542 | self.getDriverInst().request(requestObject, cb); 543 | }; 544 | 545 | /** 546 | * Delete an index by its ddocName 547 | * This function makes sure we can cleanUp an existing model when automigrate 548 | * 549 | * @param {String} mo model in the connector 550 | * @param {String} ddocName design doc name with prefix '_design/' 551 | * @callback {Function} cb The callback function 552 | */ 553 | CouchDB.prototype.deleteIndex = function(mo, ddocName, cb) { 554 | debug('CouchDB.prototype.deleteIndex ddocName: %j', ddocName); 555 | const self = this; 556 | const db = mo.db; 557 | db.get(ddocName, function(err, result) { 558 | if (err) return cb(err); 559 | db.destroy(result._id, result._rev, cb); 560 | }); 561 | }; 562 | } 563 | -------------------------------------------------------------------------------- /lib/view.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const URL = require('url'); 9 | const assert = require('assert'); 10 | const util = require('util'); 11 | const _ = require('lodash'); 12 | 13 | module.exports = mixinView; 14 | 15 | function mixinView(CouchDB) { 16 | const debug = require('debug')('loopback:connector:couchdb2:view'); 17 | 18 | /** 19 | * Gets data at `/{db}/_design/{ddocName}/views/{viewName}` 20 | * 21 | * Example: 22 | * User has a view called `getModel` in design document /{db}/_design/model, 23 | * to query the view, user can call function: 24 | * ``` 25 | * ds.viewDocs(model, getModel, {key: 'purchase'}, cb); 26 | * ``` 27 | * 28 | * @param {String} ddocName The design doc name without {db}/_design/ prefix 29 | * @param {String} viewName The view name 30 | * @param {Object} options The CouchDB view filter 31 | * @callback 32 | * @param {Function} cb 33 | */ 34 | CouchDB.prototype.viewDocs = function(ddocName, viewName, options, cb) { 35 | // omit options, e.g. ds.viewDocs(ddocName, viewName, cb); 36 | if (typeof options === 'function' && !cb) { 37 | cb = options; 38 | options = {}; 39 | } 40 | debug('CouchDB2.prototype.view ddocName %s viewName %s options %s', 41 | ddocName, viewName, options); 42 | 43 | const self = this; 44 | const db = this.couchdb.use(self.getDbName(self)); 45 | 46 | db.view(ddocName, viewName, options, cb); 47 | }; 48 | 49 | /** 50 | * Return CouchDB database name 51 | * @param {Object} connector The CouchDB connector instance 52 | * @return {String} The database name 53 | */ 54 | CouchDB.prototype.getDbName = function(connector) { 55 | const dbName = connector.settings.database || connector.settings.db || 56 | getDbFromUrl(connector.settings.url); 57 | return dbName; 58 | }; 59 | } 60 | 61 | /** 62 | * Parse url and return the database name if provided 63 | * @param {String} url The CouchDB connection url 64 | * @return {String} The database name parsed from url 65 | */ 66 | function getDbFromUrl(url) { 67 | const parsedUrl = URL.parse(url); 68 | if (parsedUrl.path && parsedUrl.path !== '/') 69 | return parsedUrl.path.split('/')[1]; 70 | return ''; 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-connector-couchdb2", 3 | "version": "1.5.3", 4 | "publishConfig": { 5 | "export-tests": true 6 | }, 7 | "description": "LoopBack Connector for CouchDB 2.0", 8 | "engines": { 9 | "node": ">=8.9" 10 | }, 11 | "author": "IBM Corp.", 12 | "keywords": [ 13 | "IBM", 14 | "StrongLoop", 15 | "LoopBack", 16 | "Couch", 17 | "CouchDB", 18 | "DataSource", 19 | "Connector" 20 | ], 21 | "main": "index.js", 22 | "scripts": { 23 | "lint": "eslint .", 24 | "lint:fix": "eslint . --fix", 25 | "test": "node test.js", 26 | "posttest": "npm run lint", 27 | "mocha": "./node_modules/.bin/_mocha --timeout 40000 --require test/init.js --require strong-mocha-interfaces --ui strong-bdd" 28 | }, 29 | "dependencies": { 30 | "async": "^1.5.0", 31 | "debug": "^4.1.1", 32 | "lodash": "^4.17.11", 33 | "loopback-connector": "^4.0.0", 34 | "nano": "^6.4.2", 35 | "request": "^2.81.0", 36 | "strong-globalize": "^6.0.3" 37 | }, 38 | "devDependencies": { 39 | "dockerode": "^2.4.3", 40 | "escape-string-regexp": "1.0.5", 41 | "eslint": "^6.8.0", 42 | "eslint-config-loopback": "^13.1.0", 43 | "loopback-datasource-juggler": "^3.0.0", 44 | "mocha": "^4.0.0", 45 | "ms": "^2.0.0", 46 | "rc": "^1.1.5", 47 | "should": "^8.4.0", 48 | "sinon": "^1.17.2", 49 | "strong-mocha-interfaces": "^1.0.0" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "git://github.com/strongloop/loopback-connector-couchdb2.git" 54 | }, 55 | "homepage": "https://github.com/strongloop/loopback-connector-couchdb2", 56 | "bugs": { 57 | "url": "https://github.com/strongloop/loopback-connector-couchdb2/issues" 58 | }, 59 | "license": "Apache-2.0" 60 | } 61 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Shell script to start the database and app services before running the tests. 4 | 5 | ## color codes 6 | RED='\033[1;31m' 7 | GREEN='\033[1;32m' 8 | YELLOW='\033[1;33m' 9 | CYAN='\033[1;36m' 10 | PLAIN='\033[0m' 11 | 12 | ## variables 13 | COUCH_CONTAINER="couch_c" 14 | COUCH_IMAGE="klaemo/couchdb" 15 | COUCH_IMAGE_TAG="latest" 16 | 17 | HOST=localhost 18 | USER='admin' 19 | PASSWORD='pass' 20 | PORT=5984 21 | DATABASE='testdb' 22 | if [ "$1" ]; then 23 | HOST=$1 24 | fi 25 | if [ "$2" ]; then 26 | PORT=$2 27 | fi 28 | if [ "$2" ]; then 29 | USER=$3 30 | fi 31 | if [ "$4" ]; then 32 | PASSWORD=$4 33 | fi 34 | if [ "$5" ]; then 35 | DATABASE=$5 36 | fi 37 | 38 | ## check if docker exists 39 | printf "\n${RED}>> Checking for docker${PLAIN} ${GREEN}...${PLAIN}" 40 | docker -v > /dev/null 2>&1 41 | DOCKER_EXISTS=$? 42 | if [ "$DOCKER_EXISTS" -ne 0 ]; then 43 | printf "\n\n${CYAN}Status: ${PLAIN}${RED}Docker not found. Terminating setup.${PLAIN}\n\n" 44 | exit 1 45 | fi 46 | printf "\n${CYAN}Found docker. Moving on with the setup.${PLAIN}\n" 47 | 48 | ## cleaning up previous builds 49 | printf "\n${RED}>> Finding old builds and cleaning up${PLAIN} ${GREEN}...${PLAIN}" 50 | docker rm -f $COUCH_CONTAINER > /dev/null 2>&1 51 | printf "\n${CYAN}Clean up complete.${PLAIN}\n" 52 | 53 | ## pull latest couch image 54 | printf "\n${RED}>> Pulling latest couch image${PLAIN} ${GREEN}...${PLAIN}" 55 | docker pull $COUCH_IMAGE:$COUCH_IMAGE_TAG > /dev/null 2>&1 56 | printf "\n${CYAN}Image successfully built.${PLAIN}\n" 57 | 58 | ## run the couch container 59 | printf "\n${RED}>> Starting the couch container${PLAIN} ${GREEN}...${PLAIN}" 60 | CONTAINER_STATUS=$(docker run -d -e COUCHDB_USER=$USER -e COUCHDB_PASSWORD=$PASSWORD -p $PORT:5984 --name $COUCH_CONTAINER $COUCH_IMAGE:$COUCH_IMAGE_TAG 2>&1) 61 | if [[ "$CONTAINER_STATUS" == *"Error"* ]]; then 62 | printf "\n\n${CYAN}Status: ${PLAIN}${RED}Error starting container. Terminating setup.${PLAIN}\n\n" 63 | exit 1 64 | fi 65 | printf "\n${CYAN}Container is up and running.${PLAIN}\n" 66 | 67 | ## wait for couch service 68 | OUTPUT=$? 69 | TIMEOUT=120 70 | TIME_PASSED=0 71 | WAIT_STRING="." 72 | 73 | printf "\n${GREEN}Waiting for couch service to be up $WAIT_STRING${PLAIN}" 74 | while [ "$OUTPUT" -ne 200 ] && [ "$TIMEOUT" -gt 0 ] 75 | do 76 | OUTPUT=$(curl -s -o /dev/null -w "%{http_code}" --request GET --url http://$USER:$PASSWORD@$HOST:$PORT/_all_dbs) 77 | sleep 1s 78 | TIMEOUT=$((TIMEOUT - 1)) 79 | TIME_PASSED=$((TIME_PASSED + 1)) 80 | 81 | if [ "$TIME_PASSED" -eq 5 ]; then 82 | printf "${GREEN}.${PLAIN}" 83 | TIME_PASSED=0 84 | fi 85 | done 86 | 87 | if [ "$TIMEOUT" -le 0 ]; then 88 | printf "\n\n${CYAN}Status: ${PLAIN}${RED}Failed to start Couch service. Terminating setup.${PLAIN}\n\n" 89 | exit 1 90 | fi 91 | printf "\n${CYAN}Couch started.${PLAIN}\n" 92 | 93 | ## create database 94 | printf "\n${RED}>> Creating database in Couch${PLAIN}" 95 | curl --request PUT --url http://$USER:$PASSWORD@$HOST:$PORT/$DATABASE > /dev/null 2>&1 96 | DB_OUTPUT=$? 97 | if [ "$DB_OUTPUT" -ne 0 ]; then 98 | printf "\n\n${CYAN}Status: ${PLAIN}${RED}Database could not be created. Terminating setup.${PLAIN}\n\n" 99 | exit 1 100 | fi 101 | printf "\n${CYAN}Database created succesfully.${PLAIN}\n" 102 | 103 | ## set env variables for running test 104 | printf "\n${RED}>> Setting env variables to run test${PLAIN} ${GREEN}...${PLAIN}" 105 | export COUCHDB_URL=http://$USER:$PASSWORD@$HOST:$PORT 106 | export COUCHDB_USERNAME=$USER 107 | export COUCHDB_PASSWORD=$PASSWORD 108 | export COUCHDB_PORT=$PORT 109 | export COUCHDB_DATABASE=$DATABASE 110 | export CI=true 111 | printf "\n${CYAN}Env variables set.${PLAIN}\n" 112 | 113 | printf "\n${CYAN}Status: ${PLAIN}${GREEN}Set up completed successfully.${PLAIN}\n" 114 | printf "\n${CYAN}Instance url: ${YELLOW}http://$USER:$PASSWORD@$HOST:$PORT/$DATABASE${PLAIN}\n" 115 | printf "\n${CYAN}To run the test suite:${PLAIN} ${YELLOW}npm run mocha${PLAIN}\n\n" 116 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const _ = require('lodash'); 9 | const async = require('async'); 10 | const spawn = require('child_process').spawn; 11 | const docker = new require('dockerode')(); 12 | const fmt = require('util').format; 13 | const http = require('http'); 14 | const ms = require('ms'); 15 | 16 | // we don't pass any node flags, so we can call _mocha instead the wrapper 17 | const mochaBin = require.resolve('mocha/bin/_mocha'); 18 | 19 | process.env.COUCHDB_DATABASE = 'test-db'; 20 | process.env.COUCHDB_PASSWORD = 'pass'; 21 | process.env.COUCHDB_USERNAME = 'admin'; 22 | 23 | // these are placeholders. They get set dynamically based on what IP and port 24 | // get assigned by docker. 25 | process.env.COUCHDB_PORT = 'TBD'; 26 | process.env.COUCHDB_HOST = 'TBD'; 27 | process.env.COUCHDB_URL = 'TBD'; 28 | 29 | const CONNECT_RETRIES = 30; 30 | const CONNECT_DELAY = ms('5s'); 31 | 32 | let containerToDelete = null; 33 | 34 | async.waterfall([ 35 | dockerStart('klaemo/couchdb:2.0.0'), 36 | sleep(ms('2s')), 37 | setCouchDBEnv, 38 | waitFor('/_all_dbs'), 39 | createDB('test-db'), 40 | run([mochaBin, '--timeout', '40000', '--require', 'strong-mocha-interfaces', '--require', 'test/init.js', '--ui', 'strong-bdd']), 41 | ], function(testErr) { 42 | dockerCleanup(function(cleanupErr) { 43 | if (cleanupErr) { 44 | console.error('error cleaning up:', cleanupErr); 45 | } 46 | if (testErr) { 47 | console.error('error running tests:', testErr); 48 | process.exit(1); 49 | } 50 | }); 51 | }); 52 | 53 | function sleep(n) { 54 | return function delayedPassThrough() { 55 | const args = [].slice.call(arguments); 56 | // last argument is the callback 57 | const next = args.pop(); 58 | // prepend `null` to indicate no error 59 | args.unshift(null); 60 | setTimeout(function() { 61 | next.apply(null, args); 62 | }, n); 63 | }; 64 | } 65 | 66 | function dockerStart(imgName) { 67 | return function pullAndStart(next) { 68 | console.log('pulling image: %s', imgName); 69 | docker.pull(imgName, function(err, stream) { 70 | if (err) return next(err); 71 | docker.modem.followProgress(stream, function(err, output) { 72 | if (err) { 73 | return next(err); 74 | } 75 | console.log('starting container from image: %s', imgName); 76 | docker.createContainer({ 77 | Image: imgName, 78 | HostConfig: { 79 | PublishAllPorts: true, 80 | }, 81 | Env: [ 82 | 'COUCHDB_USER=' + process.env.COUCHDB_USERNAME, 83 | 'COUCHDB_PASSWORD=' + process.env.COUCHDB_PASSWORD, 84 | ], 85 | }, function(err, container) { 86 | console.log('recording container for later cleanup: ', container.id); 87 | containerToDelete = container; 88 | if (err) { 89 | return next(err); 90 | } 91 | container.start(function(err, data) { 92 | next(err, container); 93 | }); 94 | }); 95 | }); 96 | }); 97 | }; 98 | } 99 | 100 | function setCouchDBEnv(container, next) { 101 | container.inspect(function(err, c) { 102 | // if swarm, Node.Ip will be set to actual node's IP 103 | // if not swarm, but remote docker, use docker host's IP 104 | // if local docker, use localhost 105 | const host = _.get(c, 'Node.IP', _.get(docker, 'modem.host', '127.0.0.1')); 106 | // container's port 80 is dynamically mapped to an external port 107 | const port = _.get(c, 108 | ['NetworkSettings', 'Ports', '5984/tcp', '0', 'HostPort']); 109 | 110 | process.env.COUCHDB_PORT = port; 111 | process.env.COUCHDB_HOST = host; 112 | const usr = process.env.COUCHDB_USERNAME; 113 | const pass = process.env.COUCHDB_PASSWORD; 114 | process.env.COUCHDB_URL = 'http://' + usr + ':' + pass + '@' + 115 | host + ':' + port; 116 | console.log('env:', _.pick(process.env, [ 117 | 'COUCHDB_URL', 118 | 'COUCHDB_HOST', 119 | 'COUCHDB_PORT', 120 | 'COUCHDB_USERNAME', 121 | 'COUCHDB_PASSWORD', 122 | 'COUCHDB_DATABASE', 123 | ])); 124 | next(null, container); 125 | }); 126 | } 127 | 128 | function waitFor(path) { 129 | return function waitForPath(container, next) { 130 | const opts = { 131 | host: process.env.COUCHDB_HOST, 132 | port: process.env.COUCHDB_PORT, 133 | auth: process.env.COUCHDB_USERNAME + ':' + process.env.COUCHDB_PASSWORD, 134 | path: path, 135 | }; 136 | 137 | console.log('waiting for instance to respond'); 138 | return ping(null, CONNECT_RETRIES); 139 | 140 | function ping(err, tries) { 141 | console.log('ping (%d/%d)', CONNECT_RETRIES - tries, CONNECT_RETRIES); 142 | if (tries < 1) { 143 | next(err || new Error('failed to contact CouchDB')); 144 | } 145 | http.get(opts, function(res) { 146 | res.pipe(devNull()); 147 | res.on('error', tryAgain); 148 | res.on('end', function() { 149 | if (res.statusCode === 200) { 150 | setImmediate(next, null, container); 151 | } else { 152 | tryAgain(); 153 | } 154 | }); 155 | }).on('error', tryAgain); 156 | function tryAgain(err) { 157 | setTimeout(ping, CONNECT_DELAY, err, tries - 1); 158 | } 159 | } 160 | }; 161 | } 162 | 163 | function createDB(db) { 164 | return function create(container, next) { 165 | const opts = { 166 | method: 'PUT', 167 | path: '/' + db, 168 | host: process.env.COUCHDB_HOST, 169 | port: process.env.COUCHDB_PORT, 170 | auth: process.env.COUCHDB_USERNAME + ':' + process.env.COUCHDB_PASSWORD, 171 | }; 172 | console.log('creating db: %j', db); 173 | http.request(opts, function(res) { 174 | res.pipe(devNull()); 175 | res.on('error', next); 176 | res.on('end', function() { 177 | setImmediate(next, null, container); 178 | }); 179 | }) 180 | .on('error', next) 181 | .end(); 182 | }; 183 | } 184 | 185 | function run(cmd) { 186 | return function spawnNode(container, next) { 187 | spawn(process.execPath, cmd, {stdio: 'inherit'}) 188 | .on('error', next) 189 | .on('exit', onExit); 190 | 191 | function onExit(code, sig) { 192 | if (code) { 193 | next(new Error(fmt('mocha exited with code: %j, sig: %j', code, sig))); 194 | } else { 195 | next(); 196 | } 197 | } 198 | }; 199 | } 200 | 201 | // clean up any previous containers 202 | function dockerCleanup(next) { 203 | if (containerToDelete) { 204 | console.log('cleaning up container: %s', containerToDelete.id); 205 | containerToDelete.remove({force: true}, function(err) { 206 | next(err); 207 | }); 208 | } else { 209 | setImmediate(next); 210 | } 211 | } 212 | 213 | // A Writable Stream that just consumes a stream. Useful for draining readable 214 | // streams so that they 'end' properly, like sometimes-empty http responses. 215 | function devNull() { 216 | return new require('stream').Writable({ 217 | write: function(_chunk, _encoding, cb) { 218 | return cb(null); 219 | }, 220 | }); 221 | } 222 | -------------------------------------------------------------------------------- /test/automigrate.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | let db, Foo, Bar, NotExist, isActualTestFoo, isActualTestBar; 9 | const util = require('util'); 10 | 11 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 12 | require('./init.js'); 13 | } 14 | 15 | describe('CouchDB automigrate', function() { 16 | it('automigrates models attached to db', function(done) { 17 | db = global.getSchema(); 18 | 19 | // Make sure automigrate doesn't destroy model doesn't exist 20 | NotExist = db.define('NotExist', { 21 | id: {type: Number, index: true}, 22 | }); 23 | Foo = db.define('Foo', { 24 | name: {type: String}, 25 | }); 26 | Bar = db.define('Bar', { 27 | name: {type: String}, 28 | }); 29 | db.automigrate(function verifyMigratedModel(err) { 30 | if (err) return done(err); 31 | Foo.create({name: 'foo'}, function(err, r) { 32 | if (err) return done(err); 33 | r.should.not.be.empty(); 34 | r.name.should.equal('foo'); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | 40 | it('autoupdates models attached to db', function(done) { 41 | db = global.getSchema(); 42 | 43 | // each test case gets a new db since it should not contain models attached 44 | // to old db 45 | Foo = db.define('Foo', { 46 | updatedName: {type: String}, 47 | }); 48 | 49 | db.autoupdate(function(err) { 50 | if (err) return done(err); 51 | Foo.find(function(err, results) { 52 | if (err) return done(err); 53 | // Verify autoupdate doesn't destroy existing data 54 | results.length.should.equal(1); 55 | results[0].name.should.equal('foo'); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | 61 | it('destroy existing model when automigrates', function(done) { 62 | db = global.getSchema(); 63 | 64 | Foo = db.define('Foo', { 65 | updatedName: {type: String}, 66 | }); 67 | db.automigrate(function(err) { 68 | if (err) return done(err); 69 | Foo.find(function(err, result) { 70 | if (err) return done(err); 71 | result.length.should.equal(0); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | 77 | it('create index for property with `index: true`', function(done) { 78 | db = global.getSchema(); 79 | 80 | Foo = db.define('Foo', { 81 | age: {type: Number, index: true}, 82 | name: {type: String}, 83 | }); 84 | db.automigrate(function(err) { 85 | if (err) return done(err); 86 | Foo.create([ 87 | {name: 'John', age: 20}, 88 | {name: 'Lucy', age: 10}, 89 | {name: 'Zoe', age: 25}], function(err, r) { 90 | if (err) return done(err); 91 | Foo.find({ 92 | where: {age: {gt: null}}, 93 | order: 'age', 94 | }, function(err, result) { 95 | if (err) return done(err); 96 | result.length.should.equal(3); 97 | result[0].age.should.equal(10); 98 | result[1].age.should.equal(20); 99 | result[2].age.should.equal(25); 100 | done(); 101 | }); 102 | }); 103 | }); 104 | }); 105 | 106 | describe('isActual', function() { 107 | db = global.getSchema(); 108 | 109 | it('returns true only when all models exist', function(done) { 110 | // `isActual` requires the model be attached to a db, 111 | // therefore use db.define here 112 | Foo = db.define('Foo', { 113 | name: {type: String}, 114 | }); 115 | Bar = db.define('Bar', { 116 | name: {type: String}, 117 | }); 118 | db.isActual(['Foo', 'Bar'], function(err, ok) { 119 | if (err) return done(err); 120 | ok.should.equal(true); 121 | done(); 122 | }); 123 | }); 124 | 125 | it('returns false when one or more models not exist', function(done) { 126 | // model isActualTestFoo and isActualTestBar are not 127 | // defined/used elsewhere, so they don't exist in database 128 | isActualTestFoo = db.define('isActualTestFoo', { 129 | name: {type: String}, 130 | }); 131 | isActualTestBar = db.define('isActualTestBar', { 132 | name: {type: String}, 133 | }); 134 | db.isActual(['Foo', 'isActualTestFoo', 'isActualTestBar'], 135 | function(err, ok) { 136 | if (err) return done(err); 137 | ok.should.equal(false); 138 | done(); 139 | }); 140 | }); 141 | 142 | it('accepts string type single model as param', function(done) { 143 | db.isActual('Foo', function(err, ok) { 144 | if (err) return done(err); 145 | ok.should.equal(true); 146 | done(); 147 | }); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/autoupdate.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | let db, AutoupdateTestFoo, connector; 9 | const async = require('async'); 10 | const _ = require('lodash'); 11 | const util = require('util'); 12 | let EXPECTED_INDEXES = {}; 13 | 14 | describe('CouchDB autoupdate', function() { 15 | before(function(done) { 16 | db = global.getDataSource(); 17 | const testModelDef = getTestModelDef(); 18 | AutoupdateTestFoo = db.define('AutoupdateTestFoo', testModelDef.properties, 19 | testModelDef.config); 20 | connector = db.connector; 21 | db.autoupdate(done); 22 | }); 23 | 24 | it('autoupdate creates indexes when model first created', function(done) { 25 | connector.getModelIndexes('AutoupdateTestFoo', function(err, result) { 26 | if (err) return done(err); 27 | Object.keys(result).length.should.equal(4); 28 | 29 | // result should contain 'name' 'age' 'email' 'loopback__model__name' index 30 | EXPECTED_INDEXES = getExpectedIndexesForFirstCreatedModel(); 31 | async.eachOf(result, assertIndex, done); 32 | }); 33 | }); 34 | 35 | it('autoupdate drops and adds indexes', function(done) { 36 | // Drop age, name indexes. 37 | // Add postcode, fullName indexes. 38 | // Keep email 39 | const newTestModelDef = getNewTestModelDef(); 40 | 41 | AutoupdateTestFoo = db.define('AutoupdateTestFoo', newTestModelDef.properties, 42 | newTestModelDef.config); 43 | connector = db.connector; 44 | 45 | db.autoupdate(function(err) { 46 | if (err) return done(err); 47 | connector.getModelIndexes('AutoupdateTestFoo', function(err, result) { 48 | if (err) return done(err); 49 | // result should contain 'email', 'fullName_index', 'postcode', 'loopback__model__name' 50 | // should not contain 'age', 'name_index' 51 | Object.keys(result).length.should.equal(4); 52 | EXPECTED_INDEXES = getExpectedIndexesForUpdatedModel(); 53 | async.eachOf(result, assertIndex, done); 54 | }); 55 | }); 56 | }); 57 | }); 58 | 59 | function getTestModelDef() { 60 | return { 61 | properties: { 62 | email: {type: 'string', index: true}, 63 | age: {type: 'number', index: true}, 64 | firstName: {type: 'string'}, 65 | lastName: {type: 'string'}, 66 | }, 67 | config: { 68 | indexes: { 69 | 'name_index': { 70 | keys: { 71 | firstName: 1, 72 | lastName: 1, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }; 78 | } 79 | 80 | function getNewTestModelDef() { 81 | return { 82 | properties: { 83 | email: {type: 'string', index: true}, 84 | age: {type: 'number'}, 85 | postcode: {type: 'string', index: true}, 86 | firstName: {type: 'string'}, 87 | middleName: {type: 'string'}, 88 | lastName: {type: 'string'}, 89 | }, 90 | config: { 91 | indexes: { 92 | 'fullName_index': { 93 | keys: { 94 | firstName: 1, 95 | middleName: 1, 96 | lastName: 1, 97 | }, 98 | }, 99 | }, 100 | }, 101 | }; 102 | } 103 | 104 | function getExpectedIndexesForFirstCreatedModel() { 105 | /* eslint camelcase: ["error", {properties: "never"}] */ 106 | const result = { 107 | age_index: { 108 | ddoc: '_design/LBModel__AutoupdateTestFoo__LBIndex__age_index', 109 | fields: [{age: 'asc'}], 110 | }, 111 | email_index: { 112 | ddoc: '_design/LBModel__AutoupdateTestFoo__LBIndex__email_index', 113 | fields: [{email: 'asc'}], 114 | }, 115 | name_index: { 116 | ddoc: '_design/LBModel__AutoupdateTestFoo__LBIndex__name_index', 117 | fields: [{firstName: 'asc'}, {lastName: 'asc'}], 118 | }, 119 | loopback__model__name_index: { 120 | ddoc: '_design/LBModel__AutoupdateTestFoo__LBIndex__loopback__model__name_index', 121 | fields: [{loopback__model__name: 'asc'}], 122 | }, 123 | }; 124 | return result; 125 | } 126 | 127 | function getExpectedIndexesForUpdatedModel() { 128 | const result = { 129 | postcode_index: { 130 | ddoc: '_design/LBModel__AutoupdateTestFoo__LBIndex__postcode_index', 131 | fields: [{postcode: 'asc'}], 132 | }, 133 | email_index: { 134 | ddoc: '_design/LBModel__AutoupdateTestFoo__LBIndex__email_index', 135 | fields: [{email: 'asc'}], 136 | }, 137 | fullName_index: { 138 | ddoc: '_design/LBModel__AutoupdateTestFoo__LBIndex__fullName_index', 139 | fields: [{firstName: 'asc'}, {lastName: 'asc'}, {middleName: 'asc'}], 140 | }, 141 | loopback__model__name_index: { 142 | ddoc: '_design/LBModel__AutoupdateTestFoo__LBIndex__loopback__model__name_index', 143 | fields: [{loopback__model__name: 'asc'}], 144 | }, 145 | }; 146 | return result; 147 | } 148 | 149 | function assertIndex(value, key, cb) { 150 | EXPECTED_INDEXES[key].should.exist; 151 | checkDdocname(key, value.ddoc); 152 | checkFields(key, value.fields); 153 | cb(); 154 | } 155 | 156 | function checkDdocname(key, ddocName) { 157 | EXPECTED_INDEXES[key].ddoc.should.equal(ddocName); 158 | } 159 | 160 | function checkFields(key, fields) { 161 | arrayEqual(EXPECTED_INDEXES[key].fields, fields); 162 | } 163 | 164 | function arrayEqual(expect, actual) { 165 | const notEqualMsg = util.inspect(expect, 4) + ' is not equal to ' + 166 | util.inspect(actual, 4); 167 | for (const item in expect) { 168 | const cond = expect[item]; 169 | const i = _.findIndex(actual, cond); 170 | i.should.above(-1, notEqualMsg); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /test/connection.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | const should = require('should'); 8 | 9 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 10 | require('./init.js'); 11 | } 12 | 13 | describe('connectivity', function() { 14 | let db; 15 | before(setUpDataSource); 16 | 17 | describe('ping()', function() { 18 | context('with a valid connection', function() { 19 | it('returns true', function(done) { 20 | db.ping(done); 21 | }); 22 | }); 23 | context('with an invalid connection', function() { 24 | it('returns error with fake url', function(done) { 25 | const fakeConfig = { 26 | url: 'http://fake:foo@localhost:4', 27 | }; 28 | const fakeDB = global.getDataSource(fakeConfig); 29 | fakeDB.ping(function(err) { 30 | should.exist(err); 31 | err.message.should.equal('ping failed'); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | }); 37 | 38 | function setUpDataSource() { 39 | db = global.getDataSource(); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /test/couchdb.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | require('./init.js'); 9 | const CouchDB = require('../lib/couchdb'); 10 | const _ = require('lodash'); 11 | const should = require('should'); 12 | const testUtil = require('./lib/test-util'); 13 | const url = require('url'); 14 | let db, Product, CustomerSimple, SimpleEmployee; 15 | 16 | describe('CouchDB2 connector', function() { 17 | before(function(done) { 18 | db = global.getDataSource(); 19 | 20 | Product = db.define('Product', { 21 | name: {type: String}, 22 | description: {type: String}, 23 | price: {type: Number}, 24 | releases: {type: ['number']}, 25 | type: {type: [String]}, 26 | foo: {type: [Object]}, 27 | }, {forceId: false}); 28 | 29 | // CustomerSimple means some nested property defs are missing in modelDef, 30 | // tests for CustomerSimple are created to make sure the typeSearch algorithm 31 | // won't crash when iterating 32 | CustomerSimple = db.define('CustomerSimple', { 33 | name: { 34 | type: String, 35 | }, 36 | seq: { 37 | type: Number, 38 | }, 39 | address: { 40 | street: String, 41 | state: String, 42 | zipCode: String, 43 | tags: [], 44 | }, 45 | friends: [], 46 | favorate: { 47 | labels: [ 48 | {label: String}, 49 | ], 50 | }, 51 | }, { 52 | indexes: { 53 | 'address_city_index': { 54 | keys: { 55 | 'address.city': 1, 56 | }, 57 | }, 58 | 'missing_property_index': { 59 | keys: { 60 | 'missingProperty': 1, 61 | }, 62 | }, 63 | }, 64 | }); 65 | 66 | SimpleEmployee = db.define('SimpleEmployee', { 67 | id: { 68 | type: Number, 69 | id: true, 70 | required: true, 71 | generated: false, 72 | }, 73 | name: { 74 | type: String, 75 | }, 76 | age: { 77 | type: Number, 78 | }, 79 | }); 80 | 81 | db.automigrate(done); 82 | }); 83 | 84 | describe('model with array props gets updated properly', function() { 85 | let prod1, prod2; 86 | before('create Product', function(done) { 87 | Product.create({ 88 | id: 1, 89 | name: 'bread', 90 | price: 100, 91 | releases: [1, 2, 3], 92 | type: ['plain', 'sesame', 'whole wheat'], 93 | foo: [{id: 1, name: 'bread1'}, {id: 2, name: 'bread2'}], 94 | }, function(err, product) { 95 | if (err) return done(err); 96 | prod1 = product; 97 | Product.create({ 98 | id: 2, 99 | name: 'bagel', 100 | price: 100, 101 | releases: [1, 2, 3], 102 | type: ['plain', 'sesame', 'whole wheat'], 103 | foo: [{id: 1, name: 'bagel1'}, {id: 2, name: 'bagel2'}], 104 | }, function(err, product) { 105 | if (err) return done(err); 106 | prod2 = product; 107 | delete prod1._rev; 108 | delete prod2._rev; 109 | done(); 110 | }); 111 | }); 112 | }); 113 | 114 | after(function(done) { 115 | Product.destroyAll(null, {limit: testUtil.QUERY_MAX}, done); 116 | }); 117 | 118 | it('updates a single instance with array props', 119 | function(done) { 120 | prod1.setAttribute('type', ['cinnamon raisin']); 121 | prod1.setAttribute('releases', [4, 5, 6]); 122 | prod1.setAttribute('foo', [{id: 3, name: 'bread3'}]); 123 | Product.updateAll({id: 1}, prod1, function(err, res) { 124 | if (err) return done(err); 125 | Product.findById('1', function(err, res) { 126 | if (err) done(err); 127 | res.name.should.equal(prod1.name); 128 | res.price.should.equal(prod1.price); 129 | res.releases.should.deepEqual([4, 5, 6]); 130 | res.type.should.deepEqual(['cinnamon raisin']); 131 | res.foo.should.deepEqual([{id: 3, name: 'bread3'}]); 132 | Product.findById('2', function(err, res) { 133 | if (err) done(err); 134 | res.name.should.equal(prod2.name); 135 | res.price.should.equal(prod2.price); 136 | res.releases.should.deepEqual(prod2.releases); 137 | res.type.should.deepEqual(prod2.type); 138 | res.foo.should.deepEqual(prod2.foo); 139 | done(); 140 | }); 141 | }); 142 | }); 143 | }); 144 | 145 | it('updates all matching instances with array props', 146 | function(done) { 147 | const data = { 148 | price: 200, 149 | releases: [7], 150 | type: ['everything'], 151 | foo: [{id: 1, name: 'bar'}], 152 | }; 153 | 154 | Product.updateAll({price: 100}, data, function(err, res) { 155 | if (err) done(err); 156 | Product.find(function(err, res) { 157 | if (err) done(err); 158 | res.length.should.equal(2); 159 | res[0].name.should.oneOf(prod1.name, prod2.name); 160 | res[0].price.should.equal(data.price); 161 | res[0].releases.should.deepEqual(data.releases); 162 | res[0].type.should.deepEqual(data.type); 163 | res[0].foo.should.deepEqual(data.foo); 164 | res[1].name.should.oneOf(prod1.name, prod2.name); 165 | res[1].price.should.equal(data.price); 166 | res[1].releases.should.deepEqual(data.releases); 167 | res[1].type.should.deepEqual(data.type); 168 | res[1].foo.should.deepEqual(data.foo); 169 | done(); 170 | }); 171 | }); 172 | }); 173 | }); 174 | 175 | // the test suite is to make sure when 176 | // user queries against a non existing property 177 | // the app won't crash 178 | describe('nested property', function() { 179 | let seedCount = 0; 180 | before(function createSampleData(done) { 181 | const seedItems = seed(); 182 | seedCount = seedItems.length; 183 | CustomerSimple.create(seedItems, done); 184 | }); 185 | 186 | after(function(done) { 187 | CustomerSimple.destroyAll(null, {limit: seedCount}, done); 188 | }); 189 | 190 | describe('missing in modelDef', function() { 191 | it('returns result when nested property is not an array type', 192 | function(done) { 193 | CustomerSimple.find({where: {'address.city': 'San Jose'}}, 194 | function(err, customers) { 195 | if (err) return done(err); 196 | customers.length.should.be.equal(1); 197 | customers[0].address.city.should.be.eql('San Jose'); 198 | done(); 199 | }); 200 | }); 201 | it('returns null when first level property is array', function(done) { 202 | CustomerSimple.find({where: {'friends.name': {regexp: /^Ringo/}}}, 203 | function(err, customers) { 204 | if (err) return done(err); 205 | customers.should.be.empty(); 206 | done(); 207 | }); 208 | }); 209 | it('returns result when first level property is array type' + 210 | ' and $elemMatch provided', function(done) { 211 | CustomerSimple.find({where: { 212 | 'friends.$elemMatch.name': {regexp: /^Ringo/}}}, 213 | function(err, customers) { 214 | if (err) return done(err); 215 | customers.length.should.be.equal(2); 216 | const expected1 = ['John Lennon', 'Paul McCartney']; 217 | const expected2 = ['Paul McCartney', 'John Lennon']; 218 | const actual = customers.map(function(c) { return c.name; }); 219 | should(actual).be.oneOf(expected1, expected2); 220 | done(); 221 | }); 222 | }); 223 | it('returns null when multi-level nested property' + 224 | ' contains array type', function(done) { 225 | CustomerSimple.find({where: {'address.tags.tag': 'business'}}, 226 | function(err, customers) { 227 | if (err) return done(err); 228 | customers.should.be.empty(); 229 | done(); 230 | }); 231 | }); 232 | it('returns result when multi-level nested property contains array type' + 233 | ' and $elemMatch provided', function(done) { 234 | CustomerSimple.find({ 235 | where: {'address.tags.$elemMatch.tag': 'business'}}, 236 | function(err, customers) { 237 | if (err) return done(err); 238 | customers.length.should.be.equal(1); 239 | customers[0].address.tags[0].tag.should.be.equal('business'); 240 | customers[0].address.tags[1].tag.should.be.equal('rent'); 241 | done(); 242 | }); 243 | }); 244 | it('returns error missing data type when sorting', function(done) { 245 | CustomerSimple.find({where: {'address.state': 'CA'}, 246 | order: 'address.state DESC'}, 247 | function(err, customers) { 248 | should.exist(err); 249 | err.message.should.match(/no_usable_index,missing_sort_index/); 250 | done(); 251 | }); 252 | }); 253 | it('returns result when sorting type provided - ' + 254 | 'missing first level property', function(done) { 255 | // Similar test case exist in juggler, but since it takes time to 256 | // recover them, I temporarily add it here 257 | CustomerSimple.find({where: {'address.state': 'CA'}, 258 | order: 'missingProperty'}, function(err, customers) { 259 | if (err) return done(err); 260 | customers.length.should.be.equal(2); 261 | const expected1 = ['San Mateo', 'San Jose']; 262 | const expected2 = ['San Jose', 'San Mateo']; 263 | const actual = customers.map(function(c) { return c.address.city; }); 264 | should(actual).be.oneOf(expected1, expected2); 265 | done(); 266 | }); 267 | }); 268 | // To ensure sorting in CouchDB, it must follow some specifics: 269 | // - At least one of the sort fields is included in the selector. 270 | // - There is an index already defined, with all the sort fields in the same order. 271 | // - Each object in the sort array has a single key. 272 | // http://docs.couchdb.org/en/2.0.0/api/database/find.html#sort-syntax 273 | it('returns result when sorting type provided - nested property', 274 | function(done) { 275 | CustomerSimple.find({where: {'address.city': {gt: null}}, 276 | order: 'address.city DESC'}, 277 | function(err, customers) { 278 | if (err) return done(err); 279 | customers.length.should.be.equal(2); 280 | customers[0].address.city.should.be.eql('San Mateo'); 281 | customers[1].address.city.should.be.eql('San Jose'); 282 | done(); 283 | }); 284 | }); 285 | }); 286 | describe('defined in modelDef', function() { 287 | it('returns result when complete query of' + 288 | ' multi-level nested property provided', function(done) { 289 | CustomerSimple.find({ 290 | where: {'favorate.labels.$elemMatch.label': 'food'}}, 291 | function(err, customers) { 292 | if (err) return done(err); 293 | customers.length.should.be.equal(1); 294 | customers[0].favorate.labels[0].label.should.be.equal('food'); 295 | customers[0].favorate.labels[1].label.should.be.equal('drink'); 296 | done(); 297 | }); 298 | }); 299 | }); 300 | }); 301 | 302 | describe('allow numerical `id` value', function() { 303 | const data = [{ 304 | id: 1, 305 | name: 'John Chow', 306 | age: 45, 307 | }, { 308 | id: 5, 309 | name: 'Kelly Johnson', 310 | age: 25, 311 | }, { 312 | id: 12, 313 | name: 'Michael Santer', 314 | age: 30, 315 | }]; 316 | let rev; 317 | 318 | before(function(done) { 319 | SimpleEmployee.create(data, function(err, result) { 320 | should.not.exist(err); 321 | rev = result[1]._rev; 322 | done(); 323 | }); 324 | }); 325 | 326 | after(function(done) { 327 | SimpleEmployee.destroyAll(null, {limit: testUtil.QUERY_MAX}, done); 328 | }); 329 | 330 | it('find instances with numeric id (findById)', function(done) { 331 | SimpleEmployee.findById(data[1].id, function(err, result) { 332 | should.not.exist(err); 333 | should.exist(result); 334 | testUtil.checkData(data[1], result.__data); 335 | done(); 336 | }); 337 | }); 338 | 339 | it('find instances with "where" filter', function(done) { 340 | SimpleEmployee.find({where: {id: data[0].id}}, function(err, result) { 341 | should.not.exist(err); 342 | should.exist(result); 343 | should.equal(result.length, 1); 344 | testUtil.checkData(data[0], result[0].__data); 345 | done(); 346 | }); 347 | }); 348 | 349 | it('find instances with "order" filter (ASC)', function(done) { 350 | SimpleEmployee.find({order: 'id ASC'}, function(err, result) { 351 | should.not.exist(err); 352 | should.exist(result); 353 | should(result[0].id).equal(data[0].id); 354 | should(result[1].id).equal(data[1].id); 355 | should(result[2].id).equal(data[2].id); 356 | done(); 357 | }); 358 | }); 359 | 360 | it('find instances with "order" filter (DESC)', function(done) { 361 | SimpleEmployee.find({order: 'id DESC'}, function(err, result) { 362 | should.not.exist(err); 363 | should.exist(result); 364 | should(result[0].id).equal(data[2].id); 365 | should(result[1].id).equal(data[1].id); 366 | should(result[2].id).equal(data[0].id); 367 | done(); 368 | }); 369 | }); 370 | 371 | it('replace instances with numerical id (replaceById)', 372 | function(done) { 373 | const updatedData = { 374 | id: data[1].id, 375 | name: 'Christian Thompson', 376 | age: 32, 377 | _rev: rev, 378 | }; 379 | data[1].name = updatedData.name; 380 | data[1].age = updatedData.age; 381 | 382 | SimpleEmployee.replaceById(data[1].id, updatedData, 383 | function(err, result) { 384 | should.not.exist(err); 385 | should.exist(result); 386 | should.equal(result.id, data[1].id); 387 | should.equal(result.name, updatedData.name); 388 | should.equal(result.age, updatedData.age); 389 | 390 | SimpleEmployee.find(function(err, result) { 391 | should.not.exist(err); 392 | should.exist(result); 393 | should.equal(result.length, 3); 394 | // checkData ignoring its order 395 | data.forEach(function(item, index) { 396 | const r = _.find(result, function(o) { 397 | return o.__data.id === item.id; 398 | }); 399 | testUtil.checkData(data[index], r.__data); 400 | }); 401 | done(); 402 | }); 403 | }); 404 | }); 405 | 406 | it('destroy instances with numerical id (destroyById)', function(done) { 407 | SimpleEmployee.destroyById(data[1].id, function(err, result) { 408 | should.not.exist(err); 409 | should.exist(result); 410 | should(result).have.property('count'); 411 | should.equal(result.count, 1); 412 | 413 | SimpleEmployee.find(function(err, result) { 414 | should.not.exist(err); 415 | should.exist(result); 416 | should.equal(result.length, 2); 417 | testUtil.checkData(data[0], result[0].__data); 418 | testUtil.checkData(data[2], result[1].__data); 419 | done(); 420 | }); 421 | }); 422 | }); 423 | 424 | it('destroy instances with "where" filter', function(done) { 425 | SimpleEmployee.destroyAll({id: data[2].id}, {limit: testUtil.QUERY_MAX}, 426 | function(err, result) { 427 | should.not.exist(err); 428 | should.exist(result); 429 | should(result).have.property('count'); 430 | should.equal(result.count, 1); 431 | 432 | SimpleEmployee.find(function(err, result) { 433 | should.not.exist(err); 434 | should.exist(result); 435 | should.equal(result.length, 1); 436 | testUtil.checkData(data[0], result[0].__data); 437 | done(); 438 | }); 439 | }); 440 | }); 441 | 442 | after(function(done) { 443 | SimpleEmployee.destroyAll(null, {limit: 1000}, function(err) { 444 | return done(err); 445 | }); 446 | }); 447 | }); 448 | }); 449 | 450 | describe('CouchDB2 constructor', function() { 451 | it('should allow passthrough of properties in the settings object', 452 | function() { 453 | const ds = global.getDataSource(); 454 | ds.settings = _.clone(ds.settings) || {}; 455 | let result = {}; 456 | ds.settings.Driver = function(options) { 457 | result = options; 458 | const fakedb = {db: {}}; 459 | fakedb.db.get = function(opts, cb) { 460 | cb(); 461 | }; 462 | return fakedb; 463 | }; 464 | ds.settings.foobar = { 465 | foo: 'bar', 466 | }; 467 | ds.settings.plugin = 'whack-a-mole'; 468 | ds.settings.requestDefault = {proxy: 'http://localhost:8080'}; 469 | const connector = CouchDB.initialize(ds, function(err) { 470 | should.not.exist(err); 471 | should.exist(result.foobar); 472 | result.foobar.foo.should.be.equal('bar'); 473 | result.plugin.should.be.equal(ds.settings.plugin); 474 | should.exist(result.requestDefault); 475 | result.requestDefault.proxy.should.be.equal('http://localhost:8080'); 476 | }); 477 | }); 478 | 479 | it('should pass the url as an object property', function() { 480 | const ds = global.getDataSource(); 481 | ds.settings = _.clone(ds.settings) || {}; 482 | let result = {}; 483 | ds.settings.Driver = function(options) { 484 | result = options; 485 | const fakedb = {db: {}}; 486 | fakedb.db.get = function(opts, cb) { 487 | cb(); 488 | }; 489 | return fakedb; 490 | }; 491 | ds.settings.url = 'https://totallyfakeuser:fakepass@definitelynotreal.cloudant.com'; 492 | const connector = CouchDB.initialize(ds, function() { 493 | // The url will definitely cause a connection error, so ignore. 494 | should.exist(result.url); 495 | result.url.should.equal(ds.settings.url); 496 | }); 497 | }); 498 | it('should convert first part of url path to database name', function(done) { 499 | const myConfig = _.clone(global.config); 500 | myConfig.url = myConfig.url + '/some/random/path'; 501 | myConfig.database = ''; 502 | let result = {}; 503 | myConfig.Driver = function(options) { 504 | result = options; 505 | const fakedb = {db: {}}; 506 | fakedb.db.get = function(opts, cb) { 507 | cb(); 508 | }; 509 | return fakedb; 510 | }; 511 | const ds = global.getDataSource(myConfig); 512 | result.url.should.equal(global.config.url); 513 | result.database.should.equal('some'); 514 | done(); 515 | }); 516 | 517 | it('should give 401 error for wrong creds', function(done) { 518 | const myConfig = _.clone(global.config); 519 | const parsedUrl = url.parse(myConfig.url); 520 | parsedUrl.auth = 'foo:bar'; 521 | myConfig.url = parsedUrl.format(); 522 | const ds = global.getDataSource(myConfig); 523 | ds.once('error', function(err) { 524 | should.exist(err); 525 | err.statusCode.should.equal(401); 526 | err.error.should.equal('unauthorized'); 527 | err.reason.should.equal('Name or password is incorrect.'); 528 | done(); 529 | }); 530 | }); 531 | it('should give 404 error for nonexistant db', function(done) { 532 | const myConfig = _.clone(global.config); 533 | const parsedUrl = url.parse(myConfig.url); 534 | parsedUrl.path = ''; 535 | myConfig.url = parsedUrl.format(); 536 | myConfig.database = 'idontexist'; 537 | const ds = global.getDataSource(myConfig); 538 | ds.once('error', function(err) { 539 | should.exist(err); 540 | err.statusCode.should.equal(404); 541 | err.error.should.equal('not_found'); 542 | err.reason.should.equal('Database does not exist.'); 543 | done(); 544 | }); 545 | }); 546 | }); 547 | 548 | function seed() { 549 | const beatles = [ 550 | { 551 | seq: 0, 552 | name: 'John Lennon', 553 | email: 'john@b3atl3s.co.uk', 554 | role: 'lead', 555 | birthday: new Date('1980-12-08'), 556 | order: 2, 557 | vip: true, 558 | address: { 559 | street: '123 A St', 560 | city: 'San Jose', 561 | state: 'CA', 562 | zipCode: '95131', 563 | tags: [ 564 | {tag: 'business'}, 565 | {tag: 'rent'}, 566 | ], 567 | }, 568 | friends: [ 569 | {name: 'Paul McCartney'}, 570 | {name: 'George Harrison'}, 571 | {name: 'Ringo Starr'}, 572 | ], 573 | }, 574 | { 575 | seq: 1, 576 | name: 'Paul McCartney', 577 | email: 'paul@b3atl3s.co.uk', 578 | role: 'lead', 579 | birthday: new Date('1942-06-18'), 580 | order: 1, 581 | vip: true, 582 | address: { 583 | street: '456 B St', 584 | city: 'San Mateo', 585 | state: 'CA', 586 | zipCode: '94065', 587 | }, 588 | friends: [ 589 | {name: 'John Lennon'}, 590 | {name: 'George Harrison'}, 591 | {name: 'Ringo Starr'}, 592 | ], 593 | }, 594 | { 595 | seq: 2, 596 | name: 'George Harrison', 597 | order: 5, 598 | vip: false, 599 | favorate: { 600 | labels: [ 601 | {label: 'food'}, 602 | {label: 'drink'}, 603 | ], 604 | }, 605 | }, 606 | {seq: 3, name: 'Ringo Starr', order: 6, vip: false}, 607 | {seq: 4, name: 'Pete Best', order: 4}, 608 | {seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true}, 609 | ]; 610 | return beatles; 611 | } 612 | -------------------------------------------------------------------------------- /test/count.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const _ = require('lodash'); 9 | const should = require('should'); 10 | const COUNT_OF_SAMPLES = 70; 11 | let db, TestCountUser; 12 | 13 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 14 | require('./init.js'); 15 | } 16 | 17 | function create50Samples() { 18 | const r = []; 19 | for (let i = 0; i < COUNT_OF_SAMPLES; i++) { 20 | r.push({name: 'user'.concat(i)}); 21 | } 22 | return r; 23 | } 24 | 25 | function cleanUpData(done) { 26 | TestCountUser.destroyAll(done); 27 | } 28 | 29 | describe('count', function() { 30 | before((done) => { 31 | // globalLimit is greater than COUNT_OF_SAMPLES 32 | const config = _.assign(global.config, {globalLimit: 100}); 33 | const samples = create50Samples(); 34 | db = global.getDataSource(config); 35 | 36 | TestCountUser = db.define('TestCountUser', { 37 | name: {type: String}, 38 | }, {forceId: false}); 39 | 40 | db.automigrate((err) => { 41 | if (err) return done(err); 42 | TestCountUser.create(samples, done); 43 | }); 44 | }); 45 | 46 | it('returns more than 25 results with global limit set', (done) => { 47 | TestCountUser.count((err, r)=> { 48 | if (err) return done(err); 49 | r.should.equal(COUNT_OF_SAMPLES); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('destroys more than 25 results with global limit set', (done) => { 55 | cleanUpData((err)=> { 56 | if (err) return done(err); 57 | TestCountUser.count((err, r) => { 58 | if (err) return done(err); 59 | r.should.equal(0); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/create.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const _ = require('lodash'); 9 | const async = require('async'); 10 | const should = require('should'); 11 | const testUtil = require('./lib/test-util'); 12 | const url = require('url'); 13 | let db, Product; 14 | 15 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 16 | require('./init.js'); 17 | } 18 | 19 | function cleanUpData(done) { 20 | Product.destroyAll(done); 21 | } 22 | 23 | const bread = { 24 | name: 'bread', 25 | price: 100, 26 | }; 27 | 28 | describe('create', function() { 29 | before(function(done) { 30 | db = global.getDataSource(); 31 | 32 | Product = db.define('Product', { 33 | name: {type: String}, 34 | description: {type: String}, 35 | price: {type: Number}, 36 | }, {forceId: false}); 37 | 38 | db.automigrate(done); 39 | }); 40 | 41 | it('creates a model instance when `_rev` is provided', function(done) { 42 | const newBread = _.cloneDeep(bread); 43 | newBread._rev = '1-somerandomrev'; 44 | Product.create(newBread, function(err, result) { 45 | err = testUtil.refinedError(err, result); 46 | if (err) return done(err); 47 | Product.findById(result.id, function(err, result) { 48 | err = testUtil.refinedError(err, result); 49 | if (err) return done(err); 50 | // CouchDB's post call ignores the `_rev` value for their own safety check 51 | // therefore, creating an instance with a random `_rev` value works. 52 | // however, it shall not be equal to the `_rev` value the user provides. 53 | should.exist(result._rev); 54 | should.notEqual(newBread._rev, result._rev); 55 | testUtil.checkModel(newBread, result); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | 61 | it('creates when model instance does not exist', function(done) { 62 | Product.create(bread, function(err, result) { 63 | err = testUtil.refinedError(err, result); 64 | if (err) return done(err); 65 | Product.findById(result.id, function(err, result) { 66 | err = testUtil.refinedError(err, result); 67 | if (err) return done(err); 68 | should.exist(result._rev); 69 | testUtil.checkModel(bread, result); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | 75 | it('replaces when the instance exists', function(done) { 76 | Product.create(bread, function(err, result) { 77 | err = testUtil.refinedError(err, result); 78 | if (err) return done(err); 79 | should.exist(result._rev); 80 | const updatedBread = _.cloneDeep(result); 81 | // Make the new record different a subset of the old one. 82 | delete updatedBread.price; 83 | Product.create(updatedBread, function(err, result) { 84 | err = testUtil.refinedError(err, result); 85 | if (err) return done(err); 86 | testUtil.checkModel(updatedBread, result); 87 | should.notDeepEqual(bread, result); 88 | done(); 89 | }); 90 | }); 91 | }); 92 | 93 | it('throws on update when model exists and _rev is different ', 94 | function(done) { 95 | let initialResult; 96 | async.waterfall([ 97 | function(callback) { 98 | return Product.create(bread, callback); 99 | }, 100 | function(result, callback) { 101 | return Product.findById(result.id, callback); 102 | }, 103 | function(result, callback) { 104 | initialResult = _.cloneDeep(result); 105 | // Simulate the idea of another caller changing the record first! 106 | result.price = 250; 107 | return Product.create(result, callback); 108 | }, 109 | function(result, callback) { 110 | // Someone beat us to it, but we don't know that yet. 111 | initialResult.price = 150; 112 | return Product.create(initialResult, callback); 113 | }, 114 | ], function(err, result) { 115 | err.should.be.ok(); 116 | should(_.includes(err.message, 'Document update conflict')); 117 | done(); 118 | }); 119 | }); 120 | 121 | afterEach(cleanUpData); 122 | }); 123 | -------------------------------------------------------------------------------- /test/find.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const _ = require('lodash'); 9 | const async = require('async'); 10 | const should = require('should'); 11 | const testUtil = require('./lib/test-util'); 12 | const url = require('url'); 13 | let db, Product; 14 | 15 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 16 | require('./init.js'); 17 | } 18 | 19 | function cleanUpData(done) { 20 | Product.destroyAll(done); 21 | } 22 | 23 | const bread = [{ 24 | id: 1, 25 | name: 'bread1', 26 | price: 100, 27 | }, { 28 | id: 2, 29 | name: 'bread2', 30 | price: 50, 31 | }, { 32 | id: 5, 33 | name: 'bread3', 34 | price: 250, 35 | }]; 36 | 37 | describe('find', function() { 38 | before(function(done) { 39 | db = global.getDataSource(); 40 | 41 | Product = db.define('Product', { 42 | id: {type: Number, required: true, id: true}, 43 | name: {type: String}, 44 | description: {type: String}, 45 | price: {type: Number}, 46 | }, {forceId: false}); 47 | 48 | db.automigrate(function(err) { 49 | should.not.exist(err); 50 | Product.create(bread, done); 51 | }); 52 | }); 53 | 54 | after(cleanUpData); 55 | 56 | it('find all model instance', function(done) { 57 | Product.find(function(err, result) { 58 | err = testUtil.refinedError(err, result); 59 | if (err) return done(err); 60 | should.exist(result); 61 | result.length.should.equal(bread.length); 62 | for (let i = 0; i < bread.length; i++) { 63 | should.exist(result[i]._rev); 64 | testUtil.checkModel(bread[i], result); 65 | } 66 | done(); 67 | }); 68 | }); 69 | 70 | it('findById all model instance', function(done) { 71 | Product.findById(1, function(err, result) { 72 | err = testUtil.refinedError(err, result); 73 | if (err) return done(err); 74 | should.exist(result); 75 | should.exist(result._rev); 76 | testUtil.checkModel(bread[0], result); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/findById.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2020. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const should = require('should'); 9 | 10 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 11 | require('./init.js'); 12 | } 13 | 14 | let db, newInstId, Todo, Item; 15 | 16 | describe('couchdb2 findById', function() { 17 | before(function(done) { 18 | db = global.getDataSource(); 19 | 20 | Todo = db.define('Todo', { 21 | id: {type: String, id: true}, 22 | name: {type: String}, 23 | }, {forceId: false}); 24 | 25 | Item = db.define('Item', { 26 | id: {type: String, id: true}, 27 | name: {type: String}, 28 | }, {forceId: false}); 29 | 30 | db.automigrate(function(err) { 31 | should.not.exist(err); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('find an existing instance by id (Promise variant)', async function() { 37 | const todo = await Todo.create({name: 'a todo'}); 38 | todo.name.should.eql('a todo'); 39 | newInstId = todo.id; 40 | const result = await db.connector.findById('Todo', newInstId); 41 | result.name.should.eql('a todo'); 42 | result.id.should.eql(todo.id); 43 | }); 44 | 45 | it('returns empty when the found instance does not belong to query model', async function() { 46 | const result = await db.connector.findById('Item', newInstId); 47 | result.should.be.empty; 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/imported.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | // Comment test cases to get CI pass, 7 | // will recover them when CI config done 8 | 9 | 'use strict'; 10 | 11 | describe('CouchDB2 imported features', function() { 12 | before(function() { 13 | global.IMPORTED_TEST = true; 14 | }); 15 | after(function() { 16 | global.IMPORTED_TEST = false; 17 | }); 18 | 19 | require('loopback-datasource-juggler/test/include.test.js'); 20 | require('loopback-datasource-juggler/test/common.batch.js'); 21 | }); 22 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const _ = require('lodash'); 9 | const should = require('should'); 10 | 11 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 12 | require('./init.js'); 13 | } 14 | 15 | let connector, db, modelName, Product; 16 | const DEFAULT_MODEL_VIEW = 'loopback__model__name'; 17 | 18 | describe('couchdb2 indexes', function() { 19 | before(function(done) { 20 | db = global.getDataSource(); 21 | connector = db.connector; 22 | modelName = 'Product'; 23 | 24 | Product = db.define(modelName, { 25 | prodName: {type: String, index: true}, 26 | prodPrice: {type: Number}, 27 | prodCode: {type: String}, 28 | }); 29 | db.automigrate(done); 30 | }); 31 | 32 | after(function(done) { 33 | Product.destroyAll(done); 34 | }); 35 | 36 | it('support property level indexes', function(done) { 37 | connector.getIndexes(connector.getDbName(connector), function(err, indexes) { 38 | should.not.exist(err); 39 | indexes = indexes.indexes; 40 | const indexName = 'prodName_index'; 41 | 42 | should.not.exist(err); 43 | should.exist(indexes); 44 | 45 | const index = _.find(indexes, function(index) { 46 | return index.name === indexName; 47 | }); 48 | 49 | should.exist(index); 50 | should.exist(index.name); 51 | index.name.should.equal(indexName); 52 | index.def.fields[0]['prodName'].should.equal('asc'); 53 | index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('support model level indexes', function(done) { 59 | Product = db.define(modelName, { 60 | prodName: {type: String}, 61 | prodPrice: {type: Number}, 62 | prodCode: {type: String}, 63 | }, { 64 | indexes: { 65 | 'prodPrice_index': { 66 | keys: { 67 | prodPrice: -1, 68 | }, 69 | }, 70 | }, 71 | }); 72 | 73 | db.automigrate('Product', function(err) { 74 | should.not.exist(err); 75 | connector.getIndexes(connector.getDbName(connector), function(err, indexes) { 76 | should.not.exist(err); 77 | indexes = indexes.indexes; 78 | const indexName = 'prodPrice_index'; 79 | 80 | should.not.exist(err); 81 | should.exist(indexes); 82 | 83 | const index = _.find(indexes, function(index) { 84 | return index.name === indexName; 85 | }); 86 | 87 | should.exist(index); 88 | should.exist(index.name); 89 | index.name.should.equal(indexName); 90 | index.def.fields[0]['prodPrice'].should.equal('desc'); 91 | index.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('desc'); 92 | done(); 93 | }); 94 | }); 95 | }); 96 | 97 | it('support both property and model level indexes', function(done) { 98 | Product = db.define(modelName, { 99 | prodName: {type: String, index: true}, 100 | prodPrice: {type: Number}, 101 | prodCode: {type: String}, 102 | }, { 103 | indexes: { 104 | 'prodPrice_index': { 105 | keys: { 106 | prodPrice: -1, 107 | }, 108 | }, 109 | }, 110 | }); 111 | 112 | db.automigrate('Product', function(err) { 113 | should.not.exist(err); 114 | connector.getIndexes(connector.getDbName(connector), function(err, indexes) { 115 | indexes = indexes.indexes; 116 | const priceIndex = 'prodPrice_index'; 117 | const nameIndex = 'prodName_index'; 118 | 119 | should.not.exist(err); 120 | should.exist(indexes); 121 | 122 | const priceIndexDoc = _.find(indexes, function(index) { 123 | return index.name === priceIndex; 124 | }); 125 | 126 | const nameIndexDoc = _.find(indexes, function(index) { 127 | return index.name === nameIndex; 128 | }); 129 | 130 | should.exist(priceIndexDoc); 131 | should.exist(nameIndexDoc); 132 | should.exist(priceIndexDoc.name); 133 | should.exist(nameIndexDoc.name); 134 | priceIndexDoc.name.should.equal(priceIndex); 135 | nameIndexDoc.name.should.equal(nameIndex); 136 | priceIndexDoc.def.fields[0]['prodPrice'].should.equal('desc'); 137 | priceIndexDoc.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('desc'); 138 | nameIndexDoc.def.fields[0]['prodName'].should.equal('asc'); 139 | nameIndexDoc.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); 140 | done(); 141 | }); 142 | }); 143 | }); 144 | 145 | it('support multiple property level indexes', function(done) { 146 | Product = db.define(modelName, { 147 | prodName: {type: String, index: true}, 148 | prodPrice: {type: Number}, 149 | prodCode: {type: String, index: true}, 150 | }); 151 | 152 | db.automigrate(function(err) { 153 | should.not.exist(err); 154 | 155 | connector.getIndexes(connector.getDbName(connector), function(err, indexes) { 156 | should.not.exist(err); 157 | should.exist(indexes); 158 | 159 | indexes = indexes.indexes; 160 | const nameIndex = 'prodName_index'; 161 | const codeIndex = 'prodCode_index'; 162 | 163 | const nameIndexDoc = _.find(indexes, function(index) { 164 | return index.name === nameIndex; 165 | }); 166 | 167 | const codeIndexDoc = _.find(indexes, function(index) { 168 | return index.name === codeIndex; 169 | }); 170 | 171 | should.exist(codeIndexDoc); 172 | should.exist(nameIndexDoc); 173 | should.exist(codeIndexDoc.name); 174 | should.exist(nameIndexDoc.name); 175 | codeIndexDoc.name.should.equal(codeIndex); 176 | nameIndexDoc.name.should.equal(nameIndex); 177 | codeIndexDoc.def.fields[0]['prodCode'].should.equal('asc'); 178 | codeIndexDoc.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); 179 | nameIndexDoc.def.fields[0]['prodName'].should.equal('asc'); 180 | codeIndexDoc.def.fields[1][DEFAULT_MODEL_VIEW].should.equal('asc'); 181 | done(); 182 | }); 183 | }); 184 | }); 185 | 186 | it('support composite indexes going same direction', function(done) { 187 | Product = db.define(modelName, { 188 | prodName: {type: String}, 189 | prodPrice: {type: Number}, 190 | prodCode: {type: String}, 191 | }, { 192 | indexes: { 193 | 'price_code_index': { 194 | keys: { 195 | prodPrice: 1, 196 | prodCode: 1, 197 | }, 198 | }, 199 | }, 200 | }); 201 | 202 | db.automigrate('Product', function(err) { 203 | connector.getIndexes(connector.getDbName(connector), function(err, indexes) { 204 | indexes = indexes.indexes; 205 | const indexName = 'price_code_index'; 206 | 207 | should.not.exist(err); 208 | should.exist(indexes); 209 | 210 | const index = _.find(indexes, function(index) { 211 | return index.name === indexName; 212 | }); 213 | 214 | should.exist(index); 215 | should.exist(index.name); 216 | index.name.should.equal(indexName); 217 | index.def.fields[0]['prodPrice'].should.equal('asc'); 218 | index.def.fields[1]['prodCode'].should.equal('asc'); 219 | index.def.fields[2][DEFAULT_MODEL_VIEW].should.equal('asc'); 220 | done(); 221 | }); 222 | }); 223 | }); 224 | 225 | it('coerce order when composite indexes go opposite direction', function(done) { 226 | Product = db.define(modelName, { 227 | prodName: {type: String, index: true}, 228 | prodPrice: {type: Number, index: true}, 229 | prodCode: {type: String}, 230 | }, { 231 | indexes: { 232 | 'code_price_index': { 233 | keys: { 234 | prodCode: 1, 235 | prodPrice: -1, 236 | }, 237 | }, 238 | }, 239 | }); 240 | 241 | db.automigrate('Product', function(err) { 242 | should.not.exist(err); 243 | connector.getIndexes(connector.getDbName(connector), function(err, indexes) { 244 | should.exist(indexes); 245 | indexes = indexes.indexes; 246 | const compositeIndex = 'code_price_index'; 247 | 248 | const compositeIndexDoc = _.find(indexes, function(index) { 249 | return index.name === compositeIndex; 250 | }); 251 | 252 | should.exist(compositeIndexDoc); 253 | compositeIndexDoc.name.should.equal(compositeIndex); 254 | compositeIndexDoc.def.fields[0]['prodCode'].should.equal('asc'); 255 | compositeIndexDoc.def.fields[1]['prodPrice'].should.equal('asc'); 256 | compositeIndexDoc.def.fields[2][DEFAULT_MODEL_VIEW].should.equal('asc'); 257 | done(); 258 | }); 259 | }); 260 | }); 261 | 262 | context('query using indexes', function() { 263 | before(function(done) { 264 | Product.create(data, done); 265 | }); 266 | 267 | it('couchdb picked default index', function(done) { 268 | Product.find({ 269 | where: { 270 | prodPrice: { 271 | gt: 0, 272 | }, 273 | }, 274 | order: 'prodPrice desc', 275 | }, 276 | function(err, products) { 277 | should.not.exist(err); 278 | should.exist(products); 279 | // check if the prices are in descending order 280 | for (let i = 1; i < products.length; i++) { 281 | const previous = products[i - 1].prodPrice; 282 | const current = products[i].prodPrice; 283 | should.ok(previous >= current); 284 | } 285 | done(); 286 | }); 287 | }); 288 | 289 | it('user specified index', function(done) { 290 | /* eslint camelcase: ["error", {properties: "never"}] */ 291 | Product.find({where: {prodPrice: {gt: 0}}}, { 292 | use_index: 'LBModel__Product__LBIndex__prodPrice_index', 293 | }, function(err, products) { 294 | should.not.exist(err); 295 | should.exist(products); 296 | // check if the prices are in ascending order 297 | for (let i = 1; i < products.length; i++) { 298 | const previous = products[i - 1].prodPrice; 299 | const current = products[i].prodPrice; 300 | should.ok(previous <= current); 301 | } 302 | // check if the codes are in ascending order 303 | const codes = _.uniq(_.map(products, function(product) { 304 | return product.prodCode; 305 | })); 306 | should.deepEqual(codes, ['abc', 'def', 'ghi']); 307 | done(); 308 | }); 309 | }); 310 | }); 311 | }); 312 | 313 | const data = [{ 314 | prodName: 'prod1', 315 | prodPrice: 5, 316 | prodCode: 'abc', 317 | }, { 318 | prodName: 'prod2', 319 | prodPrice: 12, 320 | prodCode: 'def', 321 | }, { 322 | prodName: 'prod3', 323 | prodPrice: 4, 324 | prodCode: 'abc', 325 | }, { 326 | prodName: 'prod4', 327 | prodPrice: 10, 328 | prodCode: 'def', 329 | }, { 330 | prodName: 'prod5', 331 | prodPrice: 20, 332 | prodCode: 'ghi', 333 | }]; 334 | -------------------------------------------------------------------------------- /test/init.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | module.exports = require('should'); 9 | 10 | const DataSource = require('loopback-datasource-juggler').DataSource; 11 | const _ = require('lodash'); 12 | 13 | const config = { 14 | url: process.env.COUCHDB_URL, 15 | username: process.env.COUCHDB_USERNAME, 16 | password: process.env.COUCHDB_PASSWORD, 17 | database: process.env.COUCHDB_DATABASE, 18 | port: process.env.COUCHDB_PORT, 19 | plugin: 'retry', 20 | retryAttempts: 10, 21 | retryTimeout: 50, 22 | }; 23 | 24 | console.log('env config ', config); 25 | 26 | global.config = config; 27 | global.IMPORTED_TEST = false; 28 | 29 | const skips = [ 30 | 'find all limt ten', 31 | 'find all skip ten limit ten', 32 | 'find all skip two hundred', 33 | 'isActual', 34 | ]; 35 | 36 | if (process.env.LOOPBACK_MOCHA_SKIPS) { 37 | process.env.LOOPBACK_MOCHA_SKIPS = 38 | JSON.stringify(JSON.parse(process.env.LOOPBACK_MOCHA_SKIPS).concat(skips)); 39 | } else { 40 | process.env.LOOPBACK_MOCHA_SKIPS = JSON.stringify(skips); 41 | } 42 | 43 | global.getDataSource = global.getSchema = function(customConfig) { 44 | const db = new DataSource(require('../'), customConfig || config); 45 | db.log = function(a) { 46 | console.log(a); 47 | }; 48 | 49 | const originalConnector = _.clone(db.connector); 50 | const overrideConnector = {}; 51 | 52 | overrideConnector.save = function(model, data, options, cb) { 53 | if (!global.IMPORTED_TEST) { 54 | return originalConnector.save(model, data, options, cb); 55 | } else { 56 | const self = this; 57 | const idName = self.idName(model); 58 | const id = data[idName]; 59 | const mo = self.selectModel(model); 60 | data[idName] = id.toString(); 61 | 62 | mo.db.get(id, function(err, doc) { 63 | if (err) return cb(err); 64 | data._rev = doc._rev; 65 | const saveHandler = function(err, id) { 66 | if (err) return cb(err); 67 | mo.db.get(id, function(err, doc) { 68 | if (err) return cb(err); 69 | cb(null, self.fromDB(model, mo, doc)); 70 | }); 71 | }; 72 | self._insert(model, data, saveHandler); 73 | }); 74 | } 75 | }; 76 | 77 | overrideConnector._insert = function(model, data, cb) { 78 | if (!global.IMPORTED_TEST) { 79 | return originalConnector._insert(model, data, cb); 80 | } else { 81 | originalConnector._insert(model, data, function(err, rid, rrev) { 82 | if (err) return cb(err); 83 | cb(null, rid); 84 | }); 85 | } 86 | }; 87 | 88 | db.connector.save = overrideConnector.save; 89 | db.connector._insert = overrideConnector._insert; 90 | 91 | return db; 92 | }; 93 | 94 | global.connectorCapabilities = { 95 | ilike: false, 96 | nilike: false, 97 | nestedProperty: true, 98 | supportPagination: false, 99 | ignoreUndefinedConditionValue: false, 100 | adhocSort: false, 101 | cloudantCompatible: false, 102 | }; 103 | 104 | global.sinon = require('sinon'); 105 | -------------------------------------------------------------------------------- /test/lib/test-util.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const should = require('should'); 9 | 10 | /** 11 | * Helper method to validate top-level properties on a model. 12 | * @param {object} expected The expected object. 13 | * @param {object} actual The actual object. 14 | */ 15 | exports.checkModel = function checkModel(expected, actual) { 16 | exports.checkData(expected.__data, actual.__data); 17 | }; 18 | 19 | /** 20 | * Helper method to validate a model's data properties. 21 | * @param {object} expected The expected object. 22 | * @param {object} actual The actual object. 23 | */ 24 | exports.checkData = function checkData(expected, actual) { 25 | for (const i in expected) { 26 | should.exist(actual[i]); 27 | actual[i].should.eql(expected[i]); 28 | } 29 | }; 30 | 31 | /** 32 | * Limit for maximum query size. 33 | */ 34 | exports.QUERY_MAX = 1000; 35 | 36 | /** 37 | * Helper function for refining error message if both err and result exist. 38 | * @param {*} err The error to check. 39 | * @param {*} result The result to check. 40 | * @returns {Error} The refined Error message. 41 | */ 42 | exports.refinedError = function refinedError(err, result) { 43 | let newErr = null; 44 | if (!!err && result) 45 | newErr = new Error('both err and result were returned!'); 46 | else if (err) newErr = err; 47 | return newErr; 48 | }; 49 | 50 | /** 51 | * Helper function for checking if error or result was returned. 52 | * Note that if both err and result exist, this method will return false! 53 | * @param {*} err The error to check. 54 | * @param {*} result The result to check. 55 | * @returns {Boolean} True if there is a result AND no error. 56 | */ 57 | exports.hasResult = function hasResult(err, result) { 58 | return !err && !!result; 59 | }; 60 | -------------------------------------------------------------------------------- /test/maxrows.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const should = require('should'); 9 | let db, Thing, Foo; 10 | const N = 201; 11 | 12 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 13 | require('./init.js'); 14 | } 15 | 16 | // This test suite creates large number of data, 17 | // require more time to complete data cleanUp 18 | // There is no batchDestroy in CouchDB, so `automigrate` 19 | // fetches all instances then delete them one by one 20 | describe('CouchDB2 max rows', function() { 21 | this.timeout(99999); 22 | 23 | before(function(done) { 24 | db = global.getSchema(); 25 | Foo = db.define('Foo', { 26 | bar: {type: Number, index: true}, 27 | }); 28 | Thing = db.define('Thing', { 29 | title: Number, 30 | }); 31 | Thing.belongsTo('foo', {model: Foo}); 32 | Foo.hasMany('things', {foreignKey: 'fooId'}); 33 | db.automigrate(done); 34 | }); 35 | 36 | it('create two hundred and one', function(done) { 37 | const foos = Array.apply(null, {length: N}).map(function(n, i) { 38 | return {bar: i}; 39 | }); 40 | Foo.create(foos, function(err, entries) { 41 | should.not.exist(err); 42 | entries.should.have.lengthOf(N); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('find all two hundred and one', function(done) { 48 | Foo.all({limit: N}, function(err, entries) { 49 | if (err) return done(err); 50 | entries.should.have.lengthOf(N); 51 | const things = Array.apply(null, {length: N}).map(function(n, i) { 52 | return {title: i, fooId: entries[i].id}; 53 | }); 54 | Thing.create(things, function(err, things) { 55 | if (err) return done(err); 56 | things.should.have.lengthOf(N); 57 | done(); 58 | }); 59 | }); 60 | }); 61 | 62 | it('find all limt ten', function(done) { 63 | Foo.all({limit: 10, order: 'bar'}, function(err, entries) { 64 | if (err) return done(err); 65 | entries.should.have.lengthOf(10); 66 | entries[0].bar.should.equal(0); 67 | done(); 68 | }); 69 | }); 70 | 71 | it('find all skip ten limit ten', function(done) { 72 | Foo.all({skip: 10, limit: 10, order: 'bar'}, function(err, entries) { 73 | if (err) return done(err); 74 | entries.should.have.lengthOf(10); 75 | entries[0].bar.should.equal(10); 76 | done(); 77 | }); 78 | }); 79 | 80 | it('find all skip two hundred', function(done) { 81 | Foo.all({skip: 200, order: 'bar'}, function(err, entries) { 82 | if (err) return done(err); 83 | entries.should.have.lengthOf(1); 84 | entries[0].bar.should.equal(200); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('find all things include foo', function(done) { 90 | Thing.all({include: 'foo'}, function(err, entries) { 91 | if (err) return done(err); 92 | entries.forEach(function(t) { 93 | t.__cachedRelations.should.have.property('foo'); 94 | const foo = t.__cachedRelations.foo; 95 | foo.should.have.property('id'); 96 | }); 97 | done(); 98 | }); 99 | }); 100 | 101 | after(function(done) { 102 | Foo.destroyAll(function() { 103 | Thing.destroyAll(function() { 104 | done(); 105 | }); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/regexp.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | // Comment test cases to get CI pass, 7 | // will recover them when CI config done 8 | 9 | 'use strict'; 10 | 11 | const should = require('should'); 12 | let db; 13 | 14 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 15 | require('./init.js'); 16 | } 17 | 18 | describe('CouchDB2 regexp', function() { 19 | this.timeout(99999); 20 | let Foo; 21 | const N = 10; 22 | 23 | before(function(done) { 24 | db = global.getSchema(); 25 | Foo = db.define('Foo', { 26 | bar: {type: String, index: true}, 27 | }); 28 | db.automigrate(done); 29 | }); 30 | 31 | it('create some foo', function(done) { 32 | const foos = Array.apply(null, {length: N}).map(function(n, i) { 33 | return {bar: String.fromCharCode(97 + i)}; 34 | }); 35 | Foo.create(foos, function(err, entries) { 36 | should.not.exist(err); 37 | entries.should.have.lengthOf(N); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('find all foos beginning with b', function(done) { 43 | Foo.find({where: {bar: {regexp: '^b'}}}, function(err, entries) { 44 | if (err) return done(err); 45 | entries.should.have.lengthOf(1); 46 | entries[0].bar.should.equal('b'); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('find all foos that are case-insensitive B', function(done) { 52 | Foo.find({where: {bar: {regexp: '/B/i'}}}, function(err, entries) { 53 | if (err) return done(err); 54 | entries.should.have.lengthOf(1); 55 | entries[0].bar.should.equal('b'); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('find all foos like b', function(done) { 61 | Foo.find({where: {bar: {like: 'b'}}}, function(err, entries) { 62 | if (err) return done(err); 63 | entries.should.have.lengthOf(1); 64 | entries[0].bar.should.equal('b'); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('find all foos not like b', function(done) { 70 | Foo.find({where: {bar: {nlike: 'b'}}}, function(err, entries) { 71 | if (err) return done(err); 72 | entries.should.have.lengthOf(N - 1); 73 | done(); 74 | }); 75 | }); 76 | 77 | it('find all foos like b with javascript regex', function(done) { 78 | Foo.find({where: {bar: {like: /B/i}}}, function(err, entries) { 79 | if (err) return done(err); 80 | entries.should.have.lengthOf(1); 81 | entries[0].bar.should.equal('b'); 82 | done(); 83 | }); 84 | }); 85 | 86 | it('find all foos not like b with javascript regex', function(done) { 87 | Foo.find({where: {bar: {nlike: /B/i}}}, function(err, entries) { 88 | if (err) return done(err); 89 | entries.should.have.lengthOf(N - 1); 90 | done(); 91 | }); 92 | }); 93 | 94 | after(function(done) { 95 | Foo.destroyAll(function() { 96 | done(); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/regextopcre.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | // Comment test cases to get CI pass, 7 | // will recover them when CI config done 8 | 9 | 'use strict'; 10 | 11 | const should = require('should'); 12 | let db; 13 | 14 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 15 | require('./init.js'); 16 | } 17 | 18 | describe('CouchDB2 regexToPCRE', function() { 19 | before(function() { 20 | db = global.getSchema(); 21 | }); 22 | 23 | it('return regular expression string', function() { 24 | db.connector._regexToPCRE('b', false).should.equal('b'); 25 | }); 26 | 27 | it('return regular expression string as a negitive lookahead', function() { 28 | db.connector._regexToPCRE('b', true).should.equal('[^b]'); 29 | }); 30 | 31 | it('return a pcre compliant regular expression', function() { 32 | db.connector._regexToPCRE(/b/, false).should.equal('b'); 33 | }); 34 | 35 | it('return flags mapped to pcre syntax', function() { 36 | db.connector._regexToPCRE(/b/im, false).should.equal('(?im)b'); 37 | }); 38 | 39 | it('return flags mapped to pcre syntax - negative as false', function() { 40 | db.connector._regexToPCRE(/b/i, false).should.equal('(?i)b'); 41 | }); 42 | 43 | it('return flags mapped to pcre syntax - negative as true', function() { 44 | db.connector._regexToPCRE(/b/i, true).should.equal('(?i)[^b]'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/replace.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const _ = require('lodash'); 9 | const async = require('async'); 10 | const should = require('should'); 11 | const testUtil = require('./lib/test-util'); 12 | const url = require('url'); 13 | let db, Product; 14 | 15 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 16 | require('./init.js'); 17 | } 18 | 19 | function cleanUpData(done) { 20 | Product.destroyAll(done); 21 | } 22 | 23 | const bread = { 24 | name: 'bread', 25 | price: 100, 26 | }; 27 | 28 | describe('replaceOrCreate', function() { 29 | before(function(done) { 30 | db = global.getDataSource(); 31 | 32 | Product = db.define('Product', { 33 | _rev: {type: String}, 34 | name: {type: String}, 35 | description: {type: String}, 36 | price: {type: Number}, 37 | }, {forceId: false}); 38 | 39 | db.automigrate(done); 40 | }); 41 | 42 | it('creates when the instance does not exist', function(done) { 43 | Product.replaceOrCreate(bread, function(err, result) { 44 | err = testUtil.refinedError(err, result); 45 | if (err) return done(err); 46 | testUtil.checkModel(bread, result); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('replaces when the instance exists', function(done) { 52 | // Use create, not replaceOrCreate! 53 | Product.create(bread, function(err, result) { 54 | err = testUtil.refinedError(err, result); 55 | if (err) return done(err); 56 | should.exist(result._rev); 57 | const updatedBread = _.cloneDeep(result); 58 | // Make the new record different a subset of the old one. 59 | delete updatedBread.price; 60 | 61 | Product.replaceOrCreate(updatedBread, function(err, result) { 62 | err = testUtil.refinedError(err, result); 63 | if (err) return done(err); 64 | testUtil.checkModel(updatedBread, result); 65 | should.notDeepEqual(bread, result); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | 71 | it('throws on replace when model exists and _rev is different', 72 | function(done) { 73 | let initialResult; 74 | async.waterfall([ 75 | function(callback) { 76 | return Product.create(bread, callback); 77 | }, 78 | function(result, callback) { 79 | return Product.findById(result.id, callback); 80 | }, 81 | function(result, callback) { 82 | initialResult = _.cloneDeep(result); 83 | // Simulate the idea of another caller changing the record first! 84 | result.price = 250; 85 | return Product.replaceOrCreate(result, callback); 86 | }, 87 | function(result, options, callback) { 88 | initialResult.price = 150; 89 | return Product.replaceOrCreate(initialResult, callback); 90 | }, 91 | ], function(err, result) { 92 | err = testUtil.refinedError(err, result); 93 | should(_.includes(err.message, 'Document update conflict')); 94 | done(); 95 | }); 96 | }); 97 | 98 | afterEach(cleanUpData); 99 | }); 100 | 101 | describe('replaceById', function() { 102 | before(function(done) { 103 | db = global.getDataSource(); 104 | 105 | Product = db.define('Product', { 106 | _rev: {type: String}, 107 | name: {type: String}, 108 | description: {type: String}, 109 | price: {type: Number}, 110 | }, {forceId: false}); 111 | 112 | db.automigrate(function(err) { 113 | Product.create(bread, done); 114 | }); 115 | }); 116 | 117 | afterEach(cleanUpData); 118 | 119 | it('replaces instance by id after finding', function(done) { 120 | Product.find(function(err, result) { 121 | err = testUtil.refinedError(err, result); 122 | if (err) return done(err); 123 | testUtil.hasResult(err, result).should.be.ok(); 124 | const updatedData = _.clone(result); 125 | updatedData.name = 'bread3'; 126 | const id = result[0].id; 127 | const oldRev = result[0]._rev; 128 | Product.replaceById(id, updatedData[0], function(err, result) { 129 | err = testUtil.refinedError(err, result); 130 | if (err) return done(err); 131 | testUtil.hasResult(err, result).should.be.ok(); 132 | oldRev.should.not.equal(result._rev); 133 | testUtil.checkModel(updatedData, result); 134 | done(); 135 | }); 136 | }); 137 | }); 138 | 139 | it('replaces instance by id after creating', function(done) { 140 | const newData = { 141 | name: 'bread2', 142 | price: 100, 143 | }; 144 | Product.create(newData, function(err, result) { 145 | err = testUtil.refinedError(err, result); 146 | if (err) return done(err); 147 | testUtil.hasResult(err, result).should.be.ok(); 148 | const updatedData = _.clone(result); 149 | updatedData.name = 'bread3'; 150 | const id = result.id; 151 | const oldRev = result._rev; 152 | Product.replaceById(id, updatedData, function(err, result) { 153 | err = testUtil.refinedError(err, result); 154 | if (err) return done(err); 155 | testUtil.hasResult(err, result).should.be.ok(); 156 | oldRev.should.not.equal(result._rev); 157 | done(); 158 | }); 159 | }); 160 | }); 161 | 162 | it('replace should remove model view properties (i.e loopback__model__name)', 163 | function(done) { 164 | const newData = { 165 | name: 'bread2', 166 | price: 100, 167 | }; 168 | Product.create(newData, function(err, result) { 169 | err = testUtil.refinedError(err, result); 170 | if (err) return done(err); 171 | testUtil.hasResult(err, result).should.be.ok(); 172 | const updatedData = _.clone(result); 173 | updatedData.name = 'bread3'; 174 | const id = result.id; 175 | Product.replaceById(id, updatedData, function(err, result) { 176 | err = testUtil.refinedError(err, result); 177 | if (err) return done(err); 178 | testUtil.hasResult(err, result).should.be.ok(); 179 | should.not.exist(result['loopback__model__name']); 180 | should.not.exist(result['_id']); 181 | done(); 182 | }); 183 | }); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /test/update.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const _ = require('lodash'); 9 | const async = require('async'); 10 | const should = require('should'); 11 | const testUtil = require('./lib/test-util'); 12 | const url = require('url'); 13 | let db, Product; 14 | 15 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 16 | require('./init.js'); 17 | } 18 | 19 | function cleanUpData(done) { 20 | Product.destroyAll(done); 21 | } 22 | 23 | const bread = { 24 | name: 'bread', 25 | price: 100, 26 | }; 27 | 28 | describe('updateOrCreate', function() { 29 | before(function(done) { 30 | db = global.getDataSource(); 31 | 32 | Product = db.define('Product', { 33 | name: {type: String}, 34 | description: {type: String}, 35 | price: {type: Number}, 36 | }, {forceId: false}); 37 | 38 | db.automigrate(done); 39 | }); 40 | 41 | it('creates when model instance does not exist', function(done) { 42 | Product.updateOrCreate(bread, function(err, result) { 43 | err = testUtil.refinedError(err, result); 44 | if (err) return done(err); 45 | Product.findById(result.id, function(err, result) { 46 | err = testUtil.refinedError(err, result); 47 | if (err) return done(err); 48 | should.exist(result._rev); 49 | testUtil.checkModel(bread, result); 50 | done(); 51 | }); 52 | }); 53 | }); 54 | 55 | it('creates when model instance does not exist but specifies id', 56 | function(done) { 57 | const breadWithId = _.merge({id: 1}, bread); 58 | Product.updateOrCreate(breadWithId, function(err, result) { 59 | err = testUtil.refinedError(err, result); 60 | if (err) return done(err); 61 | Product.findById(result.id, function(err, result) { 62 | err = testUtil.refinedError(err, result); 63 | if (err) return done(err); 64 | should.exist(result._rev); 65 | testUtil.checkModel(breadWithId, result); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | 71 | it('updates when model exists and _rev matches', function(done) { 72 | // Use create, not updateOrCreate! 73 | Product.create(bread, function(err, result) { 74 | err = testUtil.refinedError(err, result); 75 | if (err) return done(err); 76 | should.exist(result._rev); 77 | const updatedBread = _.cloneDeep(result); 78 | // Change the record in some way before updating. 79 | updatedBread.price = 200; 80 | Product.updateOrCreate(updatedBread, function(err, result) { 81 | err = testUtil.refinedError(err, result); 82 | if (err) return done(err); 83 | should.exist(result._rev); 84 | testUtil.checkModel(updatedBread, result); 85 | done(); 86 | }); 87 | }); 88 | }); 89 | 90 | it('throws on update when model exists and _rev is different ', 91 | function(done) { 92 | let initialResult; 93 | async.waterfall([ 94 | function(callback) { 95 | return Product.create(bread, callback); 96 | }, 97 | function(result, callback) { 98 | return Product.findById(result.id, callback); 99 | }, 100 | function(result, callback) { 101 | initialResult = _.cloneDeep(result); 102 | // Simulate the idea of another caller changing the record first! 103 | result.price = 250; 104 | return Product.updateOrCreate(result, callback); 105 | }, 106 | function(result, callback) { 107 | // Someone beat us to it, but we don't know that yet. 108 | initialResult.price = 150; 109 | return Product.updateOrCreate(initialResult, callback); 110 | }, 111 | ], function(err, result) { 112 | err = testUtil.refinedError(err, result); 113 | should(_.includes(err.message, 'Document update conflict')); 114 | done(); 115 | }); 116 | }); 117 | 118 | afterEach(cleanUpData); 119 | }); 120 | 121 | describe('updateAll', function() { 122 | before(function(done) { 123 | db = global.getDataSource(); 124 | 125 | Product = db.define('Product', { 126 | name: {type: String}, 127 | description: {type: String}, 128 | price: {type: Number}, 129 | }, {forceId: false}); 130 | 131 | db.automigrate(done); 132 | }); 133 | 134 | beforeEach(function(done) { 135 | Product.create([{ 136 | name: 'bread', 137 | price: 100, 138 | }, { 139 | name: 'bread-x', 140 | price: 110, 141 | }], done); 142 | }); 143 | 144 | afterEach(cleanUpData); 145 | 146 | it('updates a model instance without `_rev` property', function(done) { 147 | const newData = { 148 | name: 'bread2', 149 | price: 250, 150 | }; 151 | 152 | Product.find(function(err, result) { 153 | err = testUtil.refinedError(err, result); 154 | if (err) return done(err); 155 | testUtil.hasResult(err, result).should.be.ok(); 156 | const id = result[0].id; 157 | Product.update({id: id}, newData, function(err, result) { 158 | err = testUtil.refinedError(err, result); 159 | if (err) return done(err); 160 | testUtil.hasResult(err, result).should.be.ok(); 161 | result.should.have.property('count'); 162 | result.count.should.equal(1); 163 | Product.find(function(err, result) { 164 | err = testUtil.refinedError(err, result); 165 | if (err) return done(err); 166 | testUtil.hasResult(err, result).should.be.ok(); 167 | result.length.should.equal(2); 168 | newData.name.should.be.oneOf(result[0].name, result[1].name); 169 | newData.price.should.be.oneOf(result[0].price, result[1].price); 170 | done(); 171 | }); 172 | }); 173 | }); 174 | }); 175 | 176 | it('updates a model instance with `_rev` property', function(done) { 177 | const newData = { 178 | name: 'bread2', 179 | price: 250, 180 | }; 181 | 182 | Product.find(function(err, result) { 183 | err = testUtil.refinedError(err, result); 184 | if (err) return done(err); 185 | testUtil.hasResult(err, result).should.be.ok(); 186 | const id = result[0].id; 187 | newData._rev = result[0]._rev; 188 | Product.update({id: id}, newData, function(err, result) { 189 | err = testUtil.refinedError(err, result); 190 | if (err) return done(err); 191 | testUtil.hasResult(err, result).should.be.ok(); 192 | result.should.have.property('count'); 193 | result.count.should.equal(1); 194 | Product.find(function(err, result) { 195 | err = testUtil.refinedError(err, result); 196 | if (err) return done(err); 197 | testUtil.hasResult(err, result).should.be.ok(); 198 | result.length.should.equal(2); 199 | newData.name.should.be.oneOf(result[0].name, result[1].name); 200 | newData.price.should.be.oneOf(result[0].price, result[1].price); 201 | done(); 202 | }); 203 | }); 204 | }); 205 | }); 206 | }); 207 | 208 | describe('bulkReplace', function() { 209 | const breads = [{ 210 | name: 'bread1', 211 | price: 10, 212 | }, { 213 | name: 'bread2', 214 | price: 20, 215 | }, { 216 | name: 'bread3', 217 | price: 30, 218 | }, { 219 | name: 'bread4', 220 | price: 40, 221 | }, { 222 | name: 'bread5', 223 | price: 50, 224 | }, { 225 | name: 'bread6', 226 | price: 60, 227 | }, { 228 | name: 'bread7', 229 | price: 70, 230 | }]; 231 | 232 | const dataToBeUpdated = [{ 233 | name: 'bread1-update', 234 | price: 100, 235 | }, { 236 | name: 'bread4-update', 237 | price: 200, 238 | }, { 239 | name: 'bread6-update', 240 | price: 300, 241 | }]; 242 | 243 | before(function(done) { 244 | db = global.getDataSource(); 245 | 246 | Product = db.define('Product', { 247 | name: {type: String}, 248 | description: {type: String}, 249 | price: {type: Number}, 250 | }, {forceId: false}); 251 | db.automigrate(function(err) { 252 | Product.create(breads, done); 253 | }); 254 | }); 255 | 256 | afterEach(cleanUpData); 257 | 258 | it('bulk replaces with an array of data', function(done) { 259 | Product.find(function(err, result) { 260 | err = testUtil.refinedError(err, result); 261 | if (err) return done(err); 262 | testUtil.hasResult(err, result).should.be.ok(); 263 | 264 | dataToBeUpdated[0].id = result[0].id; 265 | dataToBeUpdated[0]._rev = result[0]._rev; 266 | 267 | dataToBeUpdated[1].id = result[3].id; 268 | dataToBeUpdated[1]._rev = result[3]._rev; 269 | 270 | dataToBeUpdated[2].id = result[5].id; 271 | dataToBeUpdated[2]._rev = result[5]._rev; 272 | db.connector.bulkReplace('Product', dataToBeUpdated, 273 | function(err, result) { 274 | err = testUtil.refinedError(err, result); 275 | if (err) return done(err); 276 | testUtil.hasResult(err, result).should.be.ok(); 277 | should.equal(result.length, dataToBeUpdated.length); 278 | Product.find(function(err, result) { 279 | err = testUtil.refinedError(err, result); 280 | if (err) return done(err); 281 | testUtil.hasResult(err, result).should.be.ok(); 282 | result.length.should.equal(breads.length); 283 | done(); 284 | }); 285 | }); 286 | }); 287 | }); 288 | 289 | it('throws error when `_rev` and `_id` is not provided with data', 290 | function(done) { 291 | db.connector.bulkReplace('Product', dataToBeUpdated, 292 | function(err, result) { 293 | err = testUtil.refinedError(err, result); 294 | should.exist(err); 295 | done(); 296 | }); 297 | }); 298 | }); 299 | 300 | describe('updateAttributes', function() { 301 | before(function(done) { 302 | db = global.getDataSource(); 303 | 304 | Product = db.define('Product', { 305 | name: {type: String}, 306 | description: {type: String}, 307 | price: {type: Number}, 308 | }, {forceId: false, updateOnLoad: true}); 309 | 310 | db.automigrate(function(err) { 311 | Product.create(bread, done); 312 | }); 313 | }); 314 | 315 | after(cleanUpData); 316 | 317 | it('update an attribute for a model instance', function(done) { 318 | const updateFields = { 319 | name: 'bread2', 320 | }; 321 | 322 | Product.find(function(err, result) { 323 | err = testUtil.refinedError(err, result); 324 | if (err) return done(err); 325 | testUtil.hasResult(err, result).should.be.ok(); 326 | const id = result[0].id; 327 | const oldRev = result[0]._rev; 328 | const newData = _.cloneDeep(result[0]); 329 | newData.name = updateFields.name; 330 | const product = new Product(result[0]); 331 | product.updateAttributes(newData, function(err, result) { 332 | err = testUtil.refinedError(err, result); 333 | if (err) return done(err); 334 | testUtil.hasResult(err, result).should.be.ok(); 335 | const newRev = result._rev; 336 | oldRev.should.not.equal(newRev); 337 | Product.find(function(err, result) { 338 | err = testUtil.refinedError(err, result); 339 | if (err) return done(err); 340 | testUtil.hasResult(err, result).should.be.ok(); 341 | newRev.should.equal(result[0]._rev); 342 | done(); 343 | }); 344 | }); 345 | }); 346 | }); 347 | }); 348 | -------------------------------------------------------------------------------- /test/view.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-couchdb2 3 | // This file is licensed under the Apache License 2.0. 4 | // License text available at https://opensource.org/licenses/Apache-2.0 5 | 6 | 'use strict'; 7 | 8 | const _ = require('lodash'); 9 | const async = require('async'); 10 | const should = require('should'); 11 | const url = require('url'); 12 | 13 | if (!process.env.COUCHDB2_TEST_SKIP_INIT) { 14 | require('./init.js'); 15 | } 16 | 17 | let db, sampleData; 18 | 19 | describe('couchdb2 view', function() { 20 | describe('viewDocs', function(done) { 21 | before(function(done) { 22 | db = global.getDataSource(); 23 | const connector = db.connector; 24 | 25 | db.once('connected', function(err) { 26 | async.series([insertSampleData, insertViewDdoc], done); 27 | }); 28 | 29 | function insertSampleData(cb) { 30 | sampleData = generateSamples(); 31 | connector[connector.name].use(connector.getDbName(connector)) 32 | .bulk({docs: sampleData}, cb); 33 | } 34 | 35 | function insertViewDdoc(cb) { 36 | const viewFunction = 'function(doc) { if (doc.model) ' + 37 | '{ emit(doc.model, doc); }}'; 38 | 39 | const ddoc = { 40 | _id: '_design/model', 41 | views: { 42 | getModel: { 43 | map: viewFunction, 44 | }, 45 | }, 46 | }; 47 | 48 | connector[connector.name].use(connector.getDbName(connector)).insert( 49 | JSON.parse(JSON.stringify(ddoc)), cb, 50 | ); 51 | } 52 | }); 53 | 54 | it('returns result by quering a view', function(done) { 55 | db.connector.viewDocs('model', 'getModel', 56 | function(err, results) { 57 | results.total_rows.should.equal(4); 58 | results.rows.forEach(hasModelName); 59 | done(err); 60 | 61 | function hasModelName(elem) { 62 | elem.value.hasOwnProperty('model'). 63 | should.equal(true); 64 | } 65 | }); 66 | }); 67 | 68 | it('queries a view with key filter', function(done) { 69 | db.connector.viewDocs('model', 'getModel', { 70 | 'key': 'customer', 71 | }, function(err, results) { 72 | const expectedNames = ['Zoe', 'Jack']; 73 | results.rows.forEach(belongsToModelCustomer); 74 | done(err); 75 | 76 | function belongsToModelCustomer(elem) { 77 | elem.key.should.equal('customer'); 78 | _.indexOf(expectedNames, elem.value.name).should.not.equal(-1); 79 | } 80 | }); 81 | }); 82 | }); 83 | }); 84 | 85 | function generateSamples() { 86 | const samples = [ 87 | { 88 | model: 'purchase', 89 | customerId: 1, 90 | basket: ['food', 'drink'], 91 | }, 92 | { 93 | model: 'purchase', 94 | customerId: 2, 95 | basket: ['book', 'video'], 96 | }, 97 | { 98 | model: 'customer', 99 | customerId: 1, 100 | name: 'Zoe', 101 | }, 102 | { 103 | model: 'customer', 104 | customerId: 2, 105 | name: 'Jack', 106 | }, 107 | ]; 108 | 109 | return samples; 110 | } 111 | --------------------------------------------------------------------------------