├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .template-lintrc.js ├── .travis.yml ├── .watchmanconfig ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── addon ├── .gitkeep ├── adapters │ └── pouch.js ├── index.js ├── model.js ├── serializers │ └── pouch.js ├── transforms │ ├── attachment.js │ └── attachments.js └── utils.js ├── app ├── .gitkeep ├── serializers │ └── application.js └── transforms │ ├── attachment.js │ └── attachments.js ├── blueprints ├── ember-pouch │ └── index.js ├── pouch-adapter │ ├── files │ │ └── __root__ │ │ │ └── adapters │ │ │ └── __name__.js │ └── index.js └── pouch-model │ ├── files │ └── __root__ │ │ └── models │ │ └── __name__.js │ └── index.js ├── codemods.log ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js ├── tests ├── dummy │ ├── app │ │ ├── adapters │ │ │ ├── application.js │ │ │ └── taco-salad.js │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── food-item.js │ │ │ ├── smasher.js │ │ │ ├── taco-recipe.js │ │ │ ├── taco-salad.js │ │ │ └── taco-soup.js │ │ ├── pouchdb.js │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── serializers │ │ │ └── taco-recipe.js │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ └── application.hbs │ ├── config │ │ ├── ember-cli-update.json │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ ├── crossdomain.xml │ │ └── robots.txt ├── helpers │ ├── .gitkeep │ └── module-for-pouch-acceptance.js ├── index.html ├── integration │ ├── .gitkeep │ ├── adapters │ │ ├── pouch-basics-test.js │ │ └── pouch-default-change-watcher-test.js │ └── serializers │ │ └── pouch-test.js ├── test-helper.js └── unit │ ├── .gitkeep │ └── transforms │ └── attachments-test.js └── vendor ├── .gitkeep └── pouchdb └── shims.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /package.json.ember-try 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true, 11 | }, 12 | }, 13 | plugins: ['ember'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended', 17 | 'plugin:prettier/recommended', 18 | ], 19 | env: { 20 | browser: true, 21 | }, 22 | rules: { 23 | 'qunit/resolve-async': 0, 24 | 'ember/no-test-module-for': 0, 25 | 'ember/no-classic-classes': 0, 26 | }, 27 | overrides: [ 28 | // node files 29 | { 30 | files: [ 31 | './.eslintrc.js', 32 | './.prettierrc.js', 33 | './.template-lintrc.js', 34 | './ember-cli-build.js', 35 | './index.js', 36 | './testem.js', 37 | './blueprints/*/index.js', 38 | './config/**/*.js', 39 | './tests/dummy/config/**/*.js', 40 | ], 41 | parserOptions: { 42 | sourceType: 'script', 43 | }, 44 | env: { 45 | browser: false, 46 | node: true, 47 | }, 48 | plugins: ['node'], 49 | extends: ['plugin:node/recommended'], 50 | rules: Object.assign( 51 | {}, 52 | require('eslint-plugin-node').configs.recommended.rules, 53 | { 54 | // add your custom rules and overrides for node files here 55 | 56 | // this can be removed once the following is fixed 57 | // https://github.com/mysticatea/eslint-plugin-node/issues/77 58 | 'node/no-unpublished-require': 'off', 59 | } 60 | ), 61 | }, 62 | { 63 | // Test files: 64 | files: ['tests/**/*-test.{js,ts}'], 65 | extends: ['plugin:qunit/recommended'], 66 | rules: { 67 | 'qunit/resolve-async': 0, 68 | 'qunit/no-assert-logical-expression': 0, 69 | 'qunit/no-ok-equality': 0, 70 | 'qunit/no-negated-ok': 0, 71 | // 'qunit/require-expect': 0, 72 | }, 73 | }, 74 | ], 75 | }; 76 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: {} 11 | 12 | concurrency: 13 | group: ci-${{ github.head_ref || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | test: 18 | name: "Tests" 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Install Nod 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 14.x 27 | # cache: npm 28 | - name: Install dependencies 29 | run: | 30 | if [ -e yarn.lock ]; then 31 | yarn install --frozen-lockfile 32 | elif [ -e package-lock.json ]; then 33 | npm ci 34 | else 35 | npm i 36 | fi 37 | - name: Lint 38 | run: npm run lint 39 | - name: Run Tests 40 | run: npm run test:ember 41 | 42 | floating: 43 | name: "Floating Dependencies" 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: actions/setup-node@v3 49 | with: 50 | node-version: 14.x 51 | # cache: npm 52 | - name: Install Dependencies 53 | run: npm install --no-shrinkwrap 54 | - name: Run Tests 55 | run: npm run test:ember 56 | 57 | try-scenarios: 58 | name: ${{ matrix.try-scenario }} 59 | runs-on: ubuntu-latest 60 | needs: "test" 61 | 62 | strategy: 63 | fail-fast: false 64 | matrix: 65 | try-scenario: 66 | - ember-lts-3.16 67 | - ember-lts-3.24 68 | - ember-lts-3.28 69 | - ember-lts-4.4 70 | - ember-release 71 | - ember-beta 72 | - ember-canary 73 | - ember-classic 74 | - embroider-safe 75 | - embroider-optimized 76 | 77 | steps: 78 | - uses: actions/checkout@v3 79 | - name: Install Node 80 | uses: actions/setup-node@v3 81 | with: 82 | node-version: 14.x 83 | # cache: npm 84 | - name: Install dependencies 85 | run: | 86 | if [ -e yarn.lock ]; then 87 | yarn install --frozen-lockfile 88 | elif [ -e package-lock.json ]; then 89 | npm ci 90 | else 91 | npm i 92 | fi 93 | - name: Run Tests 94 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | /package-lock.json 24 | /yarn.lock 25 | 26 | # ember-try 27 | /.node_modules.ember-try/ 28 | /bower.json.ember-try 29 | /package.json.ember-try 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.gitignore 18 | /.prettierignore 19 | /.prettierrc.js 20 | /.template-lintrc.js 21 | /.travis.yml 22 | /.watchmanconfig 23 | /bower.json 24 | /config/ember-try.js 25 | /CONTRIBUTING.md 26 | /ember-cli-build.js 27 | /testem.js 28 | /tests/ 29 | /yarn-error.log 30 | /yarn.lock 31 | .gitkeep 32 | 33 | # ember-try 34 | /.node_modules.ember-try/ 35 | /bower.json.ember-try 36 | /package.json.ember-try 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | overrides: [ 6 | { 7 | files: '*.hbs', 8 | options: { 9 | singleQuote: false, 10 | }, 11 | }, 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - '12' 7 | 8 | dist: xenial 9 | 10 | addons: 11 | chrome: stable 12 | 13 | cache: 14 | yarn: true 15 | 16 | env: 17 | global: 18 | # See https://github.com/ember-cli/ember-cli/blob/master/docs/build-concurrency.md for details. 19 | - JOBS=1 20 | 21 | branches: 22 | only: 23 | - master 24 | # npm version tags 25 | - /^v\d+\.\d+\.\d+/ 26 | 27 | jobs: 28 | fast_finish: false 29 | allow_failures: 30 | - env: EMBER_TRY_SCENARIO=ember-beta 31 | - env: EMBER_TRY_SCENARIO=ember-canary 32 | 33 | include: 34 | # runs linting and tests with current locked deps 35 | # runs linting and tests with current locked deps 36 | - stage: 'Tests' 37 | name: 'Tests' 38 | script: 39 | - yarn lint 40 | - yarn test:ember 41 | 42 | - stage: 'Additional Tests' 43 | name: 'Floating Dependencies' 44 | install: 45 | - yarn install --no-lockfile --non-interactive 46 | script: 47 | - yarn test:ember 48 | 49 | # we recommend new addons test the current and previous LTS 50 | # as well as latest stable release (bonus points to beta/canary) 51 | - env: EMBER_TRY_SCENARIO=ember-lts-3.16 52 | - env: EMBER_TRY_SCENARIO=ember-lts-3.24 53 | - env: EMBER_TRY_SCENARIO=ember-lts-3.28 54 | - env: EMBER_TRY_SCENARIO=ember-release 55 | - env: EMBER_TRY_SCENARIO=ember-beta 56 | - env: EMBER_TRY_SCENARIO=ember-canary 57 | - env: EMBER_TRY_SCENARIO=ember-classic 58 | 59 | before_install: 60 | - curl -o- -L https://yarnpkg.com/install.sh | bash 61 | - export PATH=$HOME/.yarn/bin:$PATH 62 | 63 | script: 64 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 65 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | - `git clone ` 6 | - `cd my-addon` 7 | - `npm install` 8 | 9 | ## Linting 10 | 11 | - `npm run lint` 12 | - `npm run lint:fix` 13 | 14 | ## Running tests 15 | 16 | - `ember test` – Runs the test suite on the current Ember version 17 | - `ember test --server` – Runs the test suite in "watch mode" 18 | - `ember try:each` – Runs the test suite against multiple Ember versions 19 | 20 | ## Running the dummy application 21 | 22 | - `ember serve` 23 | - Visit the dummy application at [http://localhost:4200](http://localhost:4200). 24 | 25 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember Pouch [![Build Status](https://travis-ci.org/pouchdb-community/ember-pouch.svg)](https://travis-ci.org/pouchdb-community/ember-pouch) [![GitHub version](https://badge.fury.io/gh/pouchdb-community%2Fember-pouch.svg)](https://badge.fury.io/gh/pouchdb-community%2Fember-pouch) [![Ember Observer Score](https://emberobserver.com/badges/ember-pouch.svg)](https://emberobserver.com/addons/ember-pouch) 2 | 3 | - [**Changelog**](#changelog) 4 | - [**Upgrading**](#upgrading) 5 | 6 | Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 3.16+. For older Ember Data versions down to 2.0+ use Ember Pouch version 7.0 For Ember Data versions lower than 2.0+ use Ember Pouch version 3.2.2. 7 | 8 | With Ember Pouch, all of your app's data is automatically saved on the client-side using IndexedDB or WebSQL, and you just keep using the regular [Ember Data `store` API](http://emberjs.com/api/data/classes/DS.Store.html#method_all). This data may be automatically synced to a remote CouchDB (or compatible servers) using PouchDB replication. 9 | 10 | What's the point? 11 | 12 | 1. You don't need to write any server-side logic. Just use CouchDB. 13 | 14 | 2. Data syncs automatically. 15 | 16 | 3. Your app works offline, and requests are super fast, because they don't need the network. 17 | 18 | For more on PouchDB, check out [pouchdb.com](http://pouchdb.com). 19 | 20 | ## Install and setup 21 | 22 | ```bash 23 | ember install ember-pouch 24 | ``` 25 | 26 | For ember-data < 2.0: 27 | 28 | ```bash 29 | ember install ember-pouch@3.2.2 30 | ``` 31 | 32 | For ember-cli < 1.13.0: 33 | 34 | ```bash 35 | npm install ember-pouch@3.2.2 --save-dev 36 | ``` 37 | 38 | This provides 39 | 40 | - `import {Model, Adapter, Serializer} from 'ember-pouch'` 41 | 42 | `Ember-Pouch` requires you to add a `@attr('string') rev` field to all your models. This is for PouchDB/CouchDB to handle revisions: 43 | 44 | ```javascript 45 | // app/models/todo.js 46 | 47 | import Model, { attr } from '@ember-data/model'; 48 | 49 | export default class TodoModel extends Model { 50 | @attr('string') title; 51 | @attr('boolean') isCompleted; 52 | @attr('string') rev; // <-- Add this to all your models 53 | } 54 | ``` 55 | 56 | If you like, you can also use `Model` from `Ember-Pouch` that ships with the `rev` attribute: 57 | 58 | ```javascript 59 | // app/models/todo.js 60 | 61 | import { attr } from '@ember-data/model'; 62 | import { Model } from 'ember-pouch'; 63 | 64 | export default class TodoModel extends Model { 65 | @attr('string') title; 66 | @attr('boolean') isCompleted; 67 | } 68 | ``` 69 | 70 | The installation creates a file `adapters/application.js` that you can use by default to setup the database connection. Look at the [Adapter blueprint](#adapter) section to see the settings that you have to set in your config file to work with this adapter. 71 | It also installs the required packages. 72 | 73 | ## Configuring /app/adapters/application.js 74 | 75 | A local PouchDB that syncs with a remote CouchDB looks like this: 76 | 77 | ```javascript 78 | // app/adapters/application.js 79 | 80 | import PouchDB from 'ember-pouch/pouchdb'; 81 | import { Adapter } from 'ember-pouch'; 82 | 83 | let remote = new PouchDB('http://localhost:5984/my_couch'); 84 | let db = new PouchDB('local_pouch'); 85 | 86 | db.sync(remote, { 87 | live: true, // do a live, ongoing sync 88 | retry: true, // retry if the connection is lost 89 | }); 90 | 91 | export default class ApplicationAdapter extends Adapter { 92 | db = db; 93 | } 94 | ``` 95 | 96 | You can also turn on debugging: 97 | 98 | ```javascript 99 | import PouchDB from 'ember-pouch/pouchdb'; 100 | 101 | // For v7.0.0 and newer you must first load the 'pouchdb-debug' plugin 102 | // see https://github.com/pouchdb/pouchdb/tree/39ac9a7a1f582cf7a8d91c6bf9caa936632283a6/packages/node_modules/pouchdb-debug 103 | import pouchDebugPlugin from 'pouchdb-debug'; // (assumed available via ember-auto-import or shim) 104 | PouchDB.plugin(pouchDebugPlugin); 105 | 106 | PouchDB.debug.enable('*'); 107 | ``` 108 | 109 | See the [PouchDB sync API](http://pouchdb.com/api.html#sync) for full usage instructions. 110 | 111 | ## EmberPouch Blueprints 112 | 113 | ### Model 114 | 115 | In order to create a model run the following command from the command line: 116 | 117 | ``` 118 | ember g pouch-model 119 | ``` 120 | 121 | Replace `` with the name of your model and the file will automatically be generated for you. 122 | 123 | ### Adapter 124 | 125 | You can now create an adapter using ember-cli's blueprint functionality. Once you've installed `ember-pouch` into your ember-cli app you can run the following command to automatically generate an adapter. 126 | 127 | ``` 128 | ember g pouch-adapter foo 129 | ``` 130 | 131 | Now you can store your localDb and remoteDb names in your ember-cli's config. Just add the following keys to the `ENV` object: 132 | 133 | ```javascript 134 | ENV.emberPouch.localDb = 'test'; 135 | ENV.emberPouch.remoteDb = 'http://localhost:5984/my_couch'; 136 | ``` 137 | 138 | This blueprint is run on installation for the `application` adapter. 139 | 140 | You can use multiple adapters, but be warned that doing the `.plugin` calls in multiple adapter files will result in errors: `TypeError: Cannot redefine property: replicate`. In this case it is better to move the `PouchDB.plugin` calls to a separate file. 141 | 142 | ## Relationships 143 | 144 | EmberPouch supports both `hasMany` and `belongsTo` relationships. 145 | 146 | ### Don't save hasMany child ids 147 | 148 | To be more in line with the normal ember data way of saving `hasMany` - `belongsTo` relationships, ember-pouch now has an option to not save the child ids on the `hasMany` side. This prevents the extra need to save the `hasMany` side as explained below. For a more detailed explanation please read the [relational-pouch documentation](https://github.com/pouchdb-community/relational-pouch#dont-save-hasmany) 149 | 150 | This new mode can be disabled for a `hasMany` relationship by specifying the option `save: true` on the relationship. An application wide setting named `ENV.emberPouch.saveHasMany` can also be set to `true` to make all `hasMany` relationships behave the old way. 151 | 152 | Using this mode does impose a slight runtime overhead, since this will use `db.find` and database indexes to search for the child ids. The indexes are created automatically for you. But large changes to the model might require you to clean up old, unused indexes. 153 | 154 | ℹ️ This mode is the default from version 5 onwards. Before that it was called `dontsave` and `dontsavehasmany` 155 | 156 | ### Saving child ids 157 | 158 | When you do save child ids on the `hasMany` side, you have to follow the directions below to make sure the data is saved correctly. 159 | 160 | #### Adding entries 161 | 162 | When saving a `hasMany` - `belongsTo` relationship, both sides of the relationship (the child and the parent) must be saved. Note that the parent needs to have been saved at least once prior to adding children to it. 163 | 164 | ```javascript 165 | // app/controllers/posts/post.js 166 | import Controller from '@ember/controller'; 167 | import { action } from '@ember/object'; 168 | 169 | export default class PostController extends Controller { 170 | @action addComment(comment, author) { 171 | //Create the comment 172 | const comment = this.store.createRecord('comment', { 173 | comment: comment, 174 | author: author, 175 | }); 176 | //Add our comment to our existing post 177 | this.model.comments.pushObject(comment); 178 | //Save the child then the parent 179 | comment.save().then(() => this.model.save()); 180 | } 181 | } 182 | ``` 183 | 184 | #### Removing child ids 185 | 186 | When removing a `hasMany` - `belongsTo` relationship, the children must be removed prior to the parent being removed. 187 | 188 | ```javascript 189 | // app/controller/posts/admin.js 190 | import Controller from '@ember/controller'; 191 | import { action } from '@ember/object'; 192 | import { all } from 'rsvp'; 193 | 194 | export default class AdminController extends Controller { 195 | @action deletePost(post) { 196 | //collect the promises for deletion 197 | let deletedComments = []; 198 | //get and destroy the posts comments 199 | post.comments.then((comments) => { 200 | comments.map((comment) => { 201 | deletedComments.push(comment.destroyRecord()); 202 | }); 203 | }); 204 | //Wait for comments to be destroyed then destroy the post 205 | all(deletedComments).then(() => { 206 | post.destroyRecord(); 207 | }); 208 | } 209 | } 210 | ``` 211 | 212 | ### Query and QueryRecord 213 | 214 | query and queryRecord are relying on [pouchdb-find](https://github.com/pouchdb/pouchdb/tree/master/packages/node_modules/pouchdb-find) 215 | 216 | ### db.createIndex(index [, callback]) 217 | 218 | Create an index if it doesn't exist. 219 | 220 | ```javascript 221 | // app/adapters/application.js 222 | function createDb() { 223 | ... 224 | 225 | db.createIndex({ 226 | index: { 227 | fields: ['data.name'] 228 | } 229 | }).then((result) => { 230 | // {'result': 'created'} index was created 231 | }); 232 | 233 | return db; 234 | }; 235 | ``` 236 | 237 | ### store.query(model, options) 238 | 239 | Find all docs where doc.name === 'Mario' 240 | 241 | ```javascript 242 | // app/routes/smasher.js 243 | import Route from '@ember/routing/route'; 244 | 245 | export default class SmasherRoute extends Route { 246 | model() { 247 | return this.store.query('smasher', { 248 | filter: { name: 'Mario' }, 249 | }); 250 | } 251 | } 252 | ``` 253 | 254 | Find all docs where doc.name === 'Mario' and doc.debut > 1990: 255 | 256 | ```javascript 257 | // app/routes/smasher.js 258 | import Route from '@ember/routing/route'; 259 | 260 | export default class SmasherRoute extends Route { 261 | model() { 262 | return this.store.query('smasher', { 263 | filter: { 264 | name: 'Mario' 265 | debut: { $gt: 1990 } 266 | } 267 | }); 268 | } 269 | } 270 | ``` 271 | 272 | Sorted by doc.debut descending. 273 | 274 | ```javascript 275 | // app/routes/smasher.js 276 | import Route from '@ember/routing/route'; 277 | 278 | export default class SmasherRoute extends Route { 279 | model() { 280 | return this.store.query('smasher', { 281 | filter: { 282 | name: 'Mario', 283 | debut: { $gte: null }, 284 | }, 285 | sort: [{ debut: 'desc' }], 286 | }); 287 | } 288 | } 289 | ``` 290 | 291 | Limit to 5 documents. 292 | 293 | ```javascript 294 | // app/routes/smasher.js 295 | import Route from '@ember/routing/route'; 296 | 297 | export default class SmasherRoute extends Route { 298 | model() { 299 | return this.store.query('smasher', { 300 | filter: { 301 | name: 'Mario', 302 | debut: { $gte: null }, 303 | }, 304 | sort: [{ debut: 'desc' }], 305 | limit: 5, 306 | }); 307 | } 308 | } 309 | ``` 310 | 311 | Skip the first 5 documents 312 | 313 | ```javascript 314 | // app/routes/smasher.js 315 | import Route from '@ember/routing/route'; 316 | 317 | export default class SmasherRoute extends Route { 318 | model() { 319 | return this.store.query('smasher', { 320 | filter: { 321 | name: 'Mario', 322 | debut: { $gte: null }, 323 | }, 324 | sort: [{ debut: 'desc' }], 325 | skip: 5, 326 | }); 327 | } 328 | } 329 | ``` 330 | 331 | Note that this query would require a custom index including both fields `data.name` and `data.debut`. Any field in `sort` must also be included in `filter`. Only `$eq`, `$gt`, `$gte`, `$lt`, and `$lte` can be used when matching a custom index. 332 | 333 | ### store.queryRecord(model, options) 334 | 335 | Find one document where doc.name === 'Mario' 336 | 337 | ```javascript 338 | // app/routes/smasher.js 339 | import Route from '@ember/routing/route'; 340 | 341 | export default class SmasherRoute extends Route { 342 | model() { 343 | return this.store.queryRecord('smasher', { 344 | filter: { name: 'Mario' }, 345 | }); 346 | } 347 | } 348 | ``` 349 | 350 | ## Attachments 351 | 352 | `Ember-Pouch` provides an `attachments` transform for your models, which makes working with attachments as simple as working with any other field. 353 | 354 | Add a `DS.attr('attachments')` field to your model. Provide a default value for it to be an empty array. 355 | 356 | ```javascript 357 | // myapp/models/photo-album.js 358 | import { attr } from '@ember-data/model'; 359 | import { Model } from 'ember-pouch'; 360 | 361 | export default class PhotoAlbumModel extends Model { 362 | @attr('attachments', { 363 | defaultValue: function () { 364 | return []; 365 | }, 366 | }) 367 | photos; 368 | } 369 | ``` 370 | 371 | Here, instances of `PhotoAlbum` have a `photos` field, which is an array of plain `Ember.Object`s, which have a `.name` and `.content_type`. Non-stubbed attachment also have a `.data` field; and stubbed attachments have a `.stub` instead. 372 | 373 | ```handlebars 374 |
    375 | {{#each myalbum.photos as |photo|}} 376 |
  • {{photo.name}}
  • 377 | {{/each}} 378 |
379 | ``` 380 | 381 | Attach new files by adding an `Ember.Object` with a `.name`, `.content_type` and `.data` to array of attachments. 382 | 383 | ```javascript 384 | // somewhere in your controller/component: 385 | myAlbum.photos.addObject( 386 | Ember.Object.create({ 387 | name: 'kitten.jpg', 388 | content_type: 'image/jpg', 389 | data: btoa('hello world'), // base64-encoded `String`, or a DOM `Blob`, or a `File` 390 | }) 391 | ); 392 | ``` 393 | 394 | ## Sample app 395 | 396 | Tom Dale's blog example using Ember CLI and EmberPouch: [broerse/ember-cli-blog](https://github.com/broerse/ember-cli-blog) 397 | 398 | ## Notes 399 | 400 | ### LocalStorage 401 | 402 | Currently PouchDB doesn't use LocalStorage unless you include an experimental plugin. Amazingly, this is only necessary to support IE ≤ 9.0 and Opera Mini. It's recommended you read more about this, what storage mechanisms modern browsers now support, and using SQLite in Cordova on [the PouchDB adapters page](http://pouchdb.com/adapters.html). 403 | 404 | ### CouchDB 405 | 406 | From day one, CouchDB and its protocol have been designed to be always **A**vailable and handle **P**artitioning over the network well (AP in the CAP theorem). PouchDB/CouchDB gives you a solid way to manage conflicts. It is "eventually consistent," but CouchDB has an API for listening to changes to the database, which can be then pushed down to the client in real-time. 407 | 408 | To learn more about how CouchDB sync works, check out [the PouchDB guide to replication](http://pouchdb.com/guides/replication.html). 409 | 410 | ### Sync and the ember-data store 411 | 412 | Out of the box, ember-pouch includes a PouchDB [change listener](http://pouchdb.com/guides/changes.html) that automatically updates any records your app has loaded when they change due to a sync. It also unloads records that are removed due to a sync. 413 | 414 | However, ember-pouch does not automatically load new records that arrive during a sync. The records are saved in the local database, but **ember-data is not told to load them into memory**. Automatically loading every new record works well with a small number of records and a limited number of models. As an app grows, automatically loading every record will negatively impact app responsiveness during syncs (especially the first sync). To avoid puzzling slowdowns, ember-pouch only automatically reloads records you have already used ember-data to load. 415 | 416 | If you have a model or two that you know will always have a small number of records, you can tell ember-data to automatically load them into memory as they arrive. Your PouchAdapter subclass has a method `unloadedDocumentChanged`, which is called when a document is received during sync that has not been loaded into the ember-data store. In your subclass, you can implement the following to load it automatically: 417 | 418 | ```javascript 419 | unloadedDocumentChanged: function(obj) { 420 | let recordTypeName = this.getRecordTypeName(this.store.modelFor(obj.type)); 421 | this.db.rel.find(recordTypeName, obj.id).then((doc) => { 422 | this.store.pushPayload(recordTypeName, doc); 423 | }); 424 | }, 425 | ``` 426 | 427 | ### Plugins 428 | 429 | With PouchDB, you also get access to a whole host of [PouchDB plugins](http://pouchdb.com/external.html). 430 | 431 | For example, to use the `pouchdb-authentication` plugin like this using `ember-auto-import`: 432 | 433 | ```javascript 434 | import PouchDB from 'ember-pouch/pouchdb'; 435 | import auth from 'pouchdb-authentication'; 436 | 437 | PouchDB.plugin(auth); 438 | ``` 439 | 440 | ### Relational Pouch 441 | 442 | Ember Pouch is really just a thin layer of Ember-y goodness over [Relational Pouch](https://github.com/pouchdb-community/relational-pouch). Before you file an issue, check to see if it's more appropriate to file over there. 443 | 444 | ### Offline First 445 | 446 | Saving data locally using PouchDB is one part of making a web application [Offline First](http://offlinefirst.org/). However, you will also need to make your static assets available offline. 447 | 448 | There are two possible approaches to this. The first one is using the Application Cache (AP) feature. The second one is using Service Workers (SW). The Application Cache specification has been [removed from the Web standards](https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache). Mozilla now recommends to use [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) instead. 449 | 450 | Most browser vendors still provide support for Application Cache and are in the process of implementing Service Workers. So depending on the browsers you target, you should go for one or the other. You can track the progress via [caniuse.com](https://caniuse.com/#feat=serviceworkers). 451 | 452 | #### 1. Application Cache 453 | 454 | You can use [broccoli-manifest](https://github.com/racido/broccoli-manifest) to create an HTML5 `appcache.manifest` file. This By default, will allow your index.html and `assets` directory to load even if the user is offline. 455 | 456 | #### 2. Service Workers 457 | 458 | We recommend using [Ember Service Worker](http://ember-service-worker.com) to get started with Service Workers for your web application. The website provide's an easy to follow guide on getting started with the addon. 459 | 460 | You can also take a look at Martin Broerse his [ember-cli-blog](https://github.com/broerse/ember-cli-blog/blob/14b95b443b851afa3632be3cbe631f055664b340/ember-cli-build.js) configuration for the plugin. 461 | 462 | ⚠️ iOS does not yet support Service Workers. If you want to make your assets available offline for an iPhone or iPad, you have to go for the Application Cache strategy. Since Jan 10, 2018, [Safari Technology Preview does support Service Workers](https://webkit.org/blog/8060/release-notes-for-safari-technology-preview-47/). It's expected to land in iOS 12, but there's no certainity about that. 463 | 464 | ### Security 465 | 466 | An easy way to secure your Ember Pouch-using app is to ensure that data can only be fetched from CouchDB – not from some other server (e.g. in an [XSS attack](https://en.wikipedia.org/wiki/Cross-site_scripting)). 467 | 468 | You can use the [content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy) plugin to enable Content Security Policy in Ember CLI. You also will have to set the CSP HTTP header on your backend in production. 469 | 470 | To use, add a Content Security Policy whitelist entry to `/config/environment.js`: 471 | 472 | ```js 473 | ENV.contentSecurityPolicy = { 474 | 'connect-src': "'self' http://your_couch_host.com:5984", 475 | }; 476 | ``` 477 | 478 | ### CORS setup (important!) 479 | 480 | To automatically set up your remote CouchDB to use CORS, you can use the plugin [add-cors-to-couchdb](https://github.com/pouchdb/add-cors-to-couchdb): 481 | 482 | ``` 483 | npm install -g add-cors-to-couchdb 484 | add-cors-to-couchdb http://your_couch_host.com:5984 -u your_username -p your_password 485 | ``` 486 | 487 | ### Multiple models for the same data 488 | 489 | Ember-data can be slow to load large numbers of records which have lots of relationships. If you run into this problem, you can define multiple models and have them all point to the same set of records by defining `documentType` on the model class. Example (in an ember-cli app): 490 | 491 | ```javascript 492 | // app/models/post.js 493 | 494 | import { attr, belongsTo, hasMany } from '@ember-data/model'; 495 | import { Model } from 'ember-pouch'; 496 | 497 | export default class PostModel extends Model { 498 | @attr('string') title; 499 | @attr('string') text; 500 | 501 | @belongsTo('author') author; 502 | @hasMany('comments') comments; 503 | } 504 | 505 | // app/models/post-summary.js 506 | 507 | import { attr } from '@ember-data/model'; 508 | import { Model } from 'ember-pouch'; 509 | 510 | export default class PostSummaryModel extends Model { 511 | @attr('string') title; 512 | } 513 | 514 | PostSummary.reopenClass({ 515 | documentType: 'post' 516 | }) 517 | 518 | export default PostSummary; 519 | ``` 520 | 521 | The value for `documentType` is the camelCase version of the primary model name. 522 | 523 | For best results, only create/update records using the full model definition. Treat the others as read-only. 524 | 525 | ## Multiple databases for the same model 526 | 527 | In some cases it might be desirable (security related, where you want a given user to only have some informations stored on his computer) to have multiple databases for the same model of data. 528 | 529 | `Ember-Pouch` allows you to dynamically change the database a model is using by calling the function `changeDb` on the adapter. 530 | 531 | ```javascript 532 | function changeProjectDatabase(dbName, dbUser, dbPassword) { 533 | // CouchDB is serving at http://localhost:5455 534 | let remote = new PouchDB('http://localhost:5455/' + dbName); 535 | // here we are using pouchdb-authentication for credential supports 536 | remote.login(dbUser, dbPassword).then(function (user) { 537 | let db = new PouchDB(dbName); 538 | db.sync(remote, { live: true, retry: true }); 539 | // grab the adapter, it can be any ember-pouch adapter. 540 | let adapter = this.store.adapterFor('project'); 541 | // this is where we told the adapter to change the current database. 542 | adapter.changeDb(db); 543 | }); 544 | } 545 | ``` 546 | 547 | ## Eventually Consistent 548 | 549 | Following the CouchDB consistency model, we have introduced `ENV.emberPouch.eventuallyConsistent`. This feature is on by default. So if you want the old behavior you'll have to disable this flag. 550 | 551 | `findRecord` now returns a long running Promise if the record is not found. It only rejects the promise if a deletion of the record is found. Otherwise this promise will wait for eternity to resolve. 552 | This makes sure that belongsTo relations that have been loaded in an unexpected order will still resolve correctly. This makes sure that ember-data does not set the belongsTo to null if the Pouch replicate would have loaded the related object later on. (This only works for async belongsTo, sync versions will need this to be implemented in relational-pouch) 553 | 554 | ## Upgrading 555 | 556 | ### Version 8 557 | 558 | Version 8 introduces the custom PouchDB setup in the adapter instead of having a default setup in `addon/pouchdb.js`. So if you used `import PouchDB from 'ember-pouch/pouchdb'` in your files, you now have to make your own 'PouchDB bundle' in the same way we do it in the default adapter blueprint. The simplest way to do this is to run the blueprint by doing `ember g ember-pouch` (which will overwrite your application adapter, so make sure to commit that file first) and take the `import PouchDB from...` until the final `.plugin(...)` line and put that into your original adapter (or a separate file if you use more than one adapter). 559 | You can also copy the lines from [the blueprint file in git](https://github.com/pouchdb-community/ember-pouch/blob/master/blueprints/pouch-adapter/files/__root__/adapters/__name__.js) 560 | 561 | We also removed the pouchdb-browser package and relational-pouch as a package.json dependency, so you will have to install the packages since the lines above depend upon. 562 | `npm install pouchdb-core pouchdb-adapter-indexeddb pouchdb-adapter-http pouchdb-mapreduce pouchdb-replication pouchdb-find relational-pouch --save-dev` 563 | 564 | This way you can now decide for yourself which PouchDB plugins you want to use. You can even remove the http or indexeddb ones if you just want to work offline or online. 565 | 566 | ## Installation 567 | 568 | - `git clone` this repository 569 | - `npm install` 570 | 571 | ## Running 572 | 573 | - `ember server` 574 | - Visit your app at http://localhost:4200. 575 | 576 | ## Running Tests 577 | 578 | - `ember test` 579 | - `ember test --server` 580 | 581 | ## Building 582 | 583 | - `ember build` 584 | 585 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 586 | 587 | ## Credits 588 | 589 | This project was originally based on the [ember-data-hal-adapter](https://github.com/locks/ember-data-hal-adapter) by [@locks](https://github.com/locks), and I really benefited from his guidance during its creation. 590 | 591 | And of course thanks to all our wonderful contributors, [here](https://github.com/pouchdb-community/ember-pouch/graphs/contributors) and [in Relational Pouch](https://github.com/pouchdb-community/relational-pouch/graphs/contributors)! 592 | 593 | ## Changelog 594 | - **8.0.0-beta.2** 595 | - Set PouchDb indexeddb adapter as default instead of idb adapter to use native views 596 | - Generate adapters/application.js at installation 597 | - move package.json dependencies to default blueprint 598 | - removed Adapter `fixDeleteBug` flag. We now do a client side `destroyRecord` with custom `adapterOptions` to signal to the ember-data store that the record is deleted. So no more hacking ember-data is needed in the addon to support the server pushed deletes. 599 | - **8.0.0-beta.1** 600 | - Updated to support latest Ember 4.x (fixed isDeleted issues) 601 | - Switch to PouchDB 7.3.0. Getting ready to use the indexeddb-adapter 602 | - Embroider safe and compatible scenarios supported. Ember Pouch works fine with Ember 4.x projects. 603 | - Update application adapter example to use class property #262 604 | - Fix Adapter Blueprint for Ember Octane #257 605 | - **7.0.0** 606 | - Use ember-auto-import and pouchdb-browser to ease the installation process 607 | - relational-pouch@4.0.0 608 | - Use Octane Blueprints 609 | - **6.0.0** 610 | - Switch to PouchDB 7.0.0 611 | - **5.1.0** 612 | - Don't unloadRecord a deleted document in onChange, only mark as deleted. This fixes some bugs with hasMany arrays corrupting in newer ember-data versions. Not unloading records also seems safer for routes that have that model active. 613 | - **5.0.1** 614 | - Adapter `fixDeleteBug` flag. Defaults to `true`. Fixes [https://github.com/emberjs/data/issues/4963](https://github.com/emberjs/data/issues/4963) and related issues that don't seem to work well with server side delete notifications. 615 | - Track newly inserted records, so `unloadedDocumentChanged` is not called for those. Otherwise a race-condition can occur where onChange is faster than the save. This can result in the document being inserted in the store via `unloadedDocumentChanged` before the save returns to ember-data. This will result in an assert that the id is already present in the store. 616 | - **5.0.0** 617 | - Add warning for old `dontsavehasmany` use [#216](https://github.com/pouchdb-community/ember-pouch/pull/216) 618 | - forcing the default serializer [#215](https://github.com/pouchdb-community/ember-pouch/pull/215) 619 | - test + flag + doc for eventually-consistent [#214](https://github.com/pouchdb-community/ember-pouch/pull/214) 620 | - config changes [#213](https://github.com/pouchdb-community/ember-pouch/pull/213) 621 | - Update pouchdb to version 6.4.2 [#211](https://github.com/pouchdb-community/ember-pouch/pull/211) 622 | - **5.0.0-beta.6** 623 | - Add register-version.js to vendor/ember-pouch [#210](https://github.com/pouchdb-community/ember-pouch/pull/210) 624 | - Update documentation about Offline First [#209](https://github.com/pouchdb-community/ember-pouch/pull/209) 625 | - **5.0.0-beta.5** 626 | - Add pouchdb.find.js from pouchdb [#208](https://github.com/pouchdb-community/ember-pouch/pull/208) 627 | - createIndex promises should be done before removing [#208](https://github.com/pouchdb-community/ember-pouch/pull/208) 628 | - Change sudo to required (see travis-ci/travis-ci#8836) [#208](https://github.com/pouchdb-community/ember-pouch/pull/208) 629 | - Ignore same revision changes [#189](https://github.com/pouchdb-community/ember-pouch/pull/189) 630 | - **5.0.0-beta.4** 631 | - Resolve Ember.String.pluralize() deprecation [#206](https://github.com/pouchdb-community/ember-pouch/pull/206) 632 | - allow usage of skip parameter in pouchdb adapter queries [#198](https://github.com/pouchdb-community/ember-pouch/pull/198) 633 | - **5.0.0-beta.3** 634 | - Fix Ember Data canary ember-try scenario [#202](https://github.com/pouchdb-community/ember-pouch/pull/202) 635 | - Restore ember-try configuration for Ember Data [#201](https://github.com/pouchdb-community/ember-pouch/pull/201) 636 | - Fix some jobs on Travis Trusty [#187](https://github.com/pouchdb-community/ember-pouch/pull/187) 637 | - clean up db changes listener [#195](https://github.com/pouchdb-community/ember-pouch/pull/195) 638 | - filter results of pouch adapter query by correct type [#194](https://github.com/pouchdb-community/ember-pouch/pull/194) 639 | - allow usage of limit parameter in pouchdb adapter queries [#193](https://github.com/pouchdb-community/ember-pouch/pull/193) 640 | - **5.0.0-beta.2** 641 | - version fix [#196](https://github.com/pouchdb-community/ember-pouch/pull/196) 642 | - **5.0.0-beta.1** 643 | - Eventually consistency added: documents that are not in the database will result in an 'eternal' promise. This promise will only resolve when an entry for that document is found. Deleted documents will also satisfy this promise. This mirrors the way that couchdb replication works, because the changes might not come in the order that ember-data expects. Foreign keys might therefor point to documents that have not been loaded yet. Ember-data normally resets these to null, but keeping the promise in a loading state will keep the relations intact until the actual data is loaded. 644 | - **4.3.0** 645 | - Bundle pouchdb-find [#191](https://github.com/pouchdb-community/ember-pouch/pull/191) 646 | - **4.2.9** 647 | - Lock relational-pouch version until pouchdb-find bugs are solved 648 | - **4.2.8** 649 | - Update Ember CLI and PouchDB [#186](https://github.com/pouchdb-community/ember-pouch/pull/186) 650 | - **4.2.7** 651 | - Fix `_shouldSerializeHasMany` deprecation [#185](https://github.com/pouchdb-community/ember-pouch/pull/185) 652 | - **4.2.6** 653 | - Fixes queryRecord deprecation [#152](https://github.com/pouchdb-community/ember-pouch/pull/152) 654 | - Change links to `pouchdb-community` 655 | - Use npm for ember-source [#183](https://github.com/pouchdb-community/ember-pouch/pull/183) 656 | - **4.2.5** 657 | - Correct Security documentation [#177](https://github.com/pouchdb-community/ember-pouch/pull/177) 658 | - Fix sort documentation and add additional notes [#176](https://github.com/pouchdb-community/ember-pouch/pull/176) 659 | - update ember-getowner-polyfill to remove deprecation warnings [#174](https://github.com/pouchdb-community/ember-pouch/pull/174) 660 | - **4.2.4** 661 | - Fix attachments typo in README [#170](https://github.com/pouchdb-community/ember-pouch/pull/170) 662 | - **4.2.3** 663 | - Update pouchdb to the latest version 664 | - Minor typofix [#166](https://github.com/pouchdb-community/ember-pouch/pull/166) 665 | - **4.2.2** 666 | - Update pouchdb to the latest version 667 | - **4.2.1** 668 | - Fix `Init` some more 669 | - Fix `Init` `_super.Init` error 670 | - **4.2.0** 671 | - Switch to npm versions 672 | - **4.1.0** 673 | - async is now true when not specified for relationships 674 | - hasMany relationship can have option dontsave 675 | - **4.0.3** 676 | - Fixes [#158](https://github.com/pouchdb-community/ember-pouch/pull/158) 677 | - **4.0.2** 678 | - Updated ember-cli fixes and some minor changes [#147](https://github.com/pouchdb-community/ember-pouch/pull/147) 679 | - Added Version badge and Ember Observer badge [#142](https://github.com/pouchdb-community/ember-pouch/pull/142) 680 | - **4.0.0** 681 | - Add support for Attachments [#135](https://github.com/pouchdb-community/ember-pouch/pull/135) 682 | - Implement glue code for query and queryRecord using pouchdb-find [#130](https://github.com/pouchdb-community/ember-pouch/pull/130) 683 | - **3.2.2** 684 | - Update Bower dependencies [#137](https://github.com/pouchdb-community/ember-pouch/pull/137) 685 | - Correct import of Ember Data model blueprint [#131](https://github.com/pouchdb-community/ember-pouch/pull/131) 686 | - **3.2.1** 687 | - Fix(Addon): Call super in init [#129](https://github.com/pouchdb-community/ember-pouch/pull/129) 688 | - **3.2.0** 689 | - Make adapter call a hook when encountering a change for a record that is not yet loaded [#108](https://github.com/pouchdb-community/ember-pouch/pull/108) 690 | - **3.1.1** 691 | - Bugfix for hasMany relations by [@backspace](https://github.com/backspace) ([#111](https://github.com/pouchdb-community/ember-pouch/pull/111)). 692 | - **3.1.0** 693 | - Database can now be dynamically switched on the adapter ([#89](https://github.com/pouchdb-community/ember-pouch/pull/89)). Thanks to [@olivierchatry](https://github.com/olivierchatry) for this! 694 | - Various bugfixes by [@backspace](https://github.com/backspace), [@jkleinsc](https://github.com/jkleinsc), [@rsutphin](https://github.com/rsutphin), [@mattmarcum](https://github.com/mattmarcum), [@broerse](https://github.com/broerse), and [@olivierchatry](https://github.com/olivierchatry). See [the full commit log](https://github.com/pouchdb-community/ember-pouch/compare/7c216311ffacd2f08b57df4fe34d49f4e7c373f1...v3.1.0) for details. Thank you! 695 | - **3.0.1** 696 | - Add blueprints for model and adapter (see above for details). Thanks [@mattmarcum](https://github.com/mattmarcum) ([#101](https://github.com/pouchdb-community/ember-pouch/issues/101), [#102](https://github.com/pouchdb-community/ember-pouch/issues/102)) and [@backspace](https://github.com/backspace) ([#103](https://github.com/pouchdb-community/ember-pouch/issues/103)). 697 | - **3.0.0** 698 | - Update for compatibility with Ember & Ember-Data 2.0+. The adapter now supports Ember & Ember-Data 1.13.x and 2.x only. 699 | - **2.0.3** 700 | - Use Ember.get to reference the PouchDB instance property in the adapter (`db`), allowing it to be injected ([#84](https://github.com/pouchdb-community/ember-pouch/issues/84)). Thanks to [@jkleinsc](https://github.com/jkleinsc)! 701 | - Indicate to ember-data 1.13+ that reloading individual ember-pouch records is never necessary (due to the change 702 | watcher that keeps them up to date as they are modified) ([#79](https://github.com/pouchdb-community/ember-pouch/issues/79), [#83](https://github.com/pouchdb-community/ember-pouch/issues/83)). 703 | - **2.0.2** - Use provide `findRecord` for ember-data 1.13 and later thanks to [@OleRoel](https://github.com/OleRoel) ([#72](https://github.com/pouchdb-community/ember-pouch/issues/72)) 704 | - **2.0.1** - Fixed [#62](https://github.com/pouchdb-community/ember-pouch/issues/62) thanks to [@rsutphin](https://github.com/rsutphin) (deprecated `typekey` in Ember-Data 1.0.0-beta.18) 705 | - **2.0.0** - Ember CLI support, due to some amazing support by [@fsmanuel](https://github.com/fsmanuel)! Bower and npm support are deprecated now; you are recommended to use Ember CLI instead. 706 | - **1.2.5** - Last release with regular Bower/npm support via bundle javascript in the `dist/` directory. 707 | - **1.0.0** - First release 708 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/addon/.gitkeep -------------------------------------------------------------------------------- /addon/adapters/pouch.js: -------------------------------------------------------------------------------- 1 | import RESTAdapter from '@ember-data/adapter/rest'; 2 | import { assert } from '@ember/debug'; 3 | import { isEmpty } from '@ember/utils'; 4 | import { all, defer } from 'rsvp'; 5 | import { getOwner } from '@ember/application'; 6 | import { bind } from '@ember/runloop'; 7 | import { on } from '@ember/object/evented'; 8 | import { classify, camelize } from '@ember/string'; 9 | import { pluralize } from 'ember-inflector'; 10 | //import BelongsToRelationship from 'ember-data/-private/system/relationships/state/belongs-to'; 11 | 12 | import { 13 | extractDeleteRecord, 14 | shouldSaveRelationship, 15 | configFlagDisabled, 16 | } from '../utils'; 17 | 18 | //BelongsToRelationship.reopen({ 19 | // findRecord() { 20 | // return this._super().catch(() => { 21 | // //not found: deleted 22 | // this.clear(); 23 | // }); 24 | // } 25 | //}); 26 | 27 | export default class PouchAdapter extends RESTAdapter.extend({ 28 | coalesceFindRequests: false, 29 | 30 | // The change listener ensures that individual records are kept up to date 31 | // when the data in the database changes. This makes ember-data 2.0's record 32 | // reloading redundant. 33 | shouldReloadRecord: function () { 34 | return false; 35 | }, 36 | shouldBackgroundReloadRecord: function () { 37 | return false; 38 | }, 39 | _onInit: on('init', function () { 40 | this._startChangesToStoreListener(); 41 | }), 42 | _startChangesToStoreListener: function () { 43 | var db = this.db; 44 | if (db && !this.changes) { 45 | // only run this once 46 | var onChangeListener = bind(this, 'onChange'); 47 | this.onChangeListener = onChangeListener; 48 | this.changes = db.changes({ 49 | since: 'now', 50 | live: true, 51 | returnDocs: false, 52 | }); 53 | this.changes.on('change', onChangeListener); 54 | } 55 | }, 56 | 57 | _stopChangesListener: function () { 58 | if (this.changes) { 59 | var onChangeListener = this.onChangeListener; 60 | this.changes.removeListener('change', onChangeListener); 61 | this.changes.cancel(); 62 | this.changes = undefined; 63 | } 64 | }, 65 | changeDb: function (db) { 66 | this._stopChangesListener(); 67 | 68 | var store = this.store; 69 | var schema = this._schema || []; 70 | 71 | for (var i = 0, len = schema.length; i < len; i++) { 72 | store.unloadAll(schema[i].singular); 73 | } 74 | 75 | this._schema = null; 76 | this.db = db; 77 | this._startChangesToStoreListener(); 78 | }, 79 | onChange: function (change) { 80 | // If relational_pouch isn't initialized yet, there can't be any records 81 | // in the store to update. 82 | if (!this.db.rel) { 83 | return; 84 | } 85 | 86 | var obj = this.db.rel.parseDocID(change.id); 87 | // skip changes for non-relational_pouch docs. E.g., design docs. 88 | if (!obj.type || !obj.id || obj.type === '') { 89 | return; 90 | } 91 | 92 | var store = this.store; 93 | 94 | if (this.waitingForConsistency[change.id]) { 95 | let promise = this.waitingForConsistency[change.id]; 96 | delete this.waitingForConsistency[change.id]; 97 | if (change.deleted) { 98 | promise.reject('deleted'); 99 | } else { 100 | promise.resolve(this._findRecord(obj.type, obj.id)); 101 | } 102 | return; 103 | } 104 | 105 | try { 106 | store.modelFor(obj.type); 107 | } catch (e) { 108 | // The record refers to a model which this version of the application 109 | // does not have. 110 | return; 111 | } 112 | 113 | var recordInStore = store.peekRecord(obj.type, obj.id); 114 | if (!recordInStore) { 115 | // The record hasn't been loaded into the store; no need to reload its data. 116 | if (this.createdRecords[obj.id]) { 117 | delete this.createdRecords[obj.id]; 118 | } else { 119 | this.unloadedDocumentChanged(obj); 120 | } 121 | return; 122 | } 123 | if ( 124 | !recordInStore.get('isLoaded') || 125 | recordInStore.get('rev') === change.changes[0].rev || 126 | recordInStore.get('hasDirtyAttributes') 127 | ) { 128 | // The record either hasn't loaded yet or has unpersisted local changes. 129 | // In either case, we don't want to refresh it in the store 130 | // (and for some substates, attempting to do so will result in an error). 131 | // We also ignore the change if we already have the latest revision 132 | return; 133 | } 134 | 135 | if (change.deleted) { 136 | if (!recordInStore.isSaving && !recordInStore.isDeleted) 137 | return recordInStore.destroyRecord({ 138 | adapterOptions: { serverPush: true }, 139 | }); 140 | } else { 141 | return recordInStore.reload(); 142 | } 143 | }, 144 | 145 | unloadedDocumentChanged: function (/* obj */) { 146 | /* 147 | * For performance purposes, we don't load records into the store that haven't previously been loaded. 148 | * If you want to change this, subclass this method, and push the data into the store. e.g. 149 | * 150 | * let store = this.get('store'); 151 | * let recordTypeName = this.getRecordTypeName(store.modelFor(obj.type)); 152 | * this.get('db').rel.find(recordTypeName, obj.id).then(function(doc){ 153 | * store.pushPayload(recordTypeName, doc); 154 | * }); 155 | */ 156 | }, 157 | 158 | willDestroy: function () { 159 | this._stopChangesListener(); 160 | }, 161 | 162 | init() { 163 | this._indexPromises = []; 164 | this.waitingForConsistency = {}; 165 | this.createdRecords = {}; 166 | }, 167 | 168 | _indexPromises: null, 169 | 170 | _init: function (store, type, indexPromises) { 171 | var self = this, 172 | recordTypeName = this.getRecordTypeName(type); 173 | if (!this.db || typeof this.db !== 'object') { 174 | throw new Error('Please set the `db` property on the adapter.'); 175 | } 176 | 177 | if (!type.attributes.has('rev')) { 178 | var modelName = classify(recordTypeName); 179 | throw new Error( 180 | 'Please add a `rev` attribute of type `string`' + 181 | ' on the ' + 182 | modelName + 183 | ' model.' 184 | ); 185 | } 186 | 187 | this._schema = this._schema || []; 188 | 189 | var singular = recordTypeName; 190 | var plural = pluralize(recordTypeName); 191 | 192 | // check that we haven't already registered this model 193 | for (var i = 0, len = this._schema.length; i < len; i++) { 194 | var currentSchemaDef = this._schema[i]; 195 | if (currentSchemaDef.singular === singular) { 196 | return all(this._indexPromises); 197 | } 198 | } 199 | 200 | var schemaDef = { 201 | singular: singular, 202 | plural: plural, 203 | }; 204 | 205 | if (type.documentType) { 206 | schemaDef['documentType'] = type.documentType; 207 | } 208 | 209 | let config = getOwner(this).resolveRegistration('config:environment'); 210 | // else it's new, so update 211 | this._schema.push(schemaDef); 212 | // check all the subtypes 213 | // We check the type of `rel.type`because with ember-data beta 19 214 | // `rel.type` switched from DS.Model to string 215 | 216 | var rels = []; //extra array is needed since type.relationships/byName return a Map that is not iterable 217 | type.eachRelationship((_relName, rel) => rels.push(rel)); 218 | 219 | let rootCall = indexPromises == undefined; 220 | if (rootCall) { 221 | indexPromises = []; 222 | } 223 | 224 | for (let rel of rels) { 225 | if (rel.kind !== 'belongsTo' && rel.kind !== 'hasMany') { 226 | // TODO: support inverse as well 227 | continue; // skip 228 | } 229 | var relDef = {}, 230 | relModel = 231 | typeof rel.type === 'string' ? store.modelFor(rel.type) : rel.type; 232 | if (relModel) { 233 | let includeRel = true; 234 | if (!('options' in rel)) rel.options = {}; 235 | 236 | if (typeof rel.options.async === 'undefined') { 237 | rel.options.async = 238 | config.emberPouch && !isEmpty(config.emberPouch.async) 239 | ? config.emberPouch.async 240 | : true; //default true from https://github.com/emberjs/data/pull/3366 241 | } 242 | let options = Object.create(rel.options); 243 | if (rel.kind === 'hasMany' && !shouldSaveRelationship(self, rel)) { 244 | let inverse = type.inverseFor(rel.key, store); 245 | if (inverse) { 246 | if (inverse.kind === 'belongsTo') { 247 | indexPromises.push( 248 | self.get('db').createIndex({ 249 | index: { fields: ['data.' + inverse.name, '_id'] }, 250 | }) 251 | ); 252 | if (options.async) { 253 | includeRel = false; 254 | } else { 255 | options.queryInverse = inverse.name; 256 | } 257 | } 258 | } 259 | } 260 | 261 | if (includeRel) { 262 | relDef[rel.kind] = { 263 | type: self.getRecordTypeName(relModel), 264 | options: options, 265 | }; 266 | if (!schemaDef.relations) { 267 | schemaDef.relations = {}; 268 | } 269 | schemaDef.relations[rel.key] = relDef; 270 | } 271 | 272 | self._init(store, relModel, indexPromises); 273 | } 274 | } 275 | 276 | this.db.setSchema(this._schema); 277 | 278 | if (rootCall) { 279 | this._indexPromises = this._indexPromises.concat(indexPromises); 280 | return all(indexPromises).then(() => { 281 | this._indexPromises = this._indexPromises.filter( 282 | (x) => !indexPromises.includes(x) 283 | ); 284 | }); 285 | } 286 | }, 287 | 288 | _recordToData: function (store, type, record) { 289 | var data = {}; 290 | // Though it would work to use the default recordTypeName for modelName & 291 | // serializerKey here, these uses are conceptually distinct and may vary 292 | // independently. 293 | var modelName = type.modelName || type.typeKey; 294 | var serializerKey = camelize(modelName); 295 | var serializer = store.serializerFor(modelName); 296 | 297 | serializer.serializeIntoHash(data, type, record, { includeId: true }); 298 | 299 | data = data[serializerKey]; 300 | 301 | // ember sets it to null automatically. don't need it. 302 | if (data.rev === null) { 303 | delete data.rev; 304 | } 305 | 306 | return data; 307 | }, 308 | 309 | /** 310 | * Return key that conform to data adapter 311 | * ex: 'name' become 'data.name' 312 | */ 313 | _dataKey: function (key) { 314 | var dataKey = 'data.' + key; 315 | return '' + dataKey + ''; 316 | }, 317 | 318 | /** 319 | * Returns the modified selector key to comform data key 320 | * Ex: selector: {name: 'Mario'} wil become selector: {'data.name': 'Mario'} 321 | */ 322 | _buildSelector: function (selector) { 323 | var dataSelector = {}; 324 | var selectorKeys = []; 325 | 326 | for (var key in selector) { 327 | if (Object.prototype.hasOwnProperty.call(selector, key)) { 328 | selectorKeys.push(key); 329 | } 330 | } 331 | 332 | selectorKeys.forEach( 333 | function (key) { 334 | var dataKey = this._dataKey(key); 335 | dataSelector[dataKey] = selector[key]; 336 | }.bind(this) 337 | ); 338 | 339 | return dataSelector; 340 | }, 341 | 342 | /** 343 | * Returns the modified sort key 344 | * Ex: sort: ['series'] will become ['data.series'] 345 | * Ex: sort: [{series: 'desc'}] will became [{'data.series': 'desc'}] 346 | */ 347 | _buildSort: function (sort) { 348 | return sort.map( 349 | function (value) { 350 | var sortKey = {}; 351 | if (typeof value === 'object' && value !== null) { 352 | for (var key in value) { 353 | if (Object.prototype.hasOwnProperty.call(value, key)) { 354 | sortKey[this._dataKey(key)] = value[key]; 355 | } 356 | } 357 | } else { 358 | return this._dataKey(value); 359 | } 360 | return sortKey; 361 | }.bind(this) 362 | ); 363 | }, 364 | 365 | /** 366 | * Returns the string to use for the model name part of the PouchDB document 367 | * ID for records of the given ember-data type. 368 | * 369 | * This method uses the camelized version of the model name in order to 370 | * preserve data compatibility with older versions of ember-pouch. See 371 | * pouchdb-community/ember-pouch#63 for a discussion. 372 | * 373 | * You can override this to change the behavior. If you do, be aware that you 374 | * need to execute a data migration to ensure that any existing records are 375 | * moved to the new IDs. 376 | */ 377 | getRecordTypeName(type) { 378 | return camelize(type.modelName); 379 | }, 380 | 381 | findAll: async function (store, type /*, sinceToken */) { 382 | // TODO: use sinceToken 383 | await this._init(store, type); 384 | return this.db.rel.find(this.getRecordTypeName(type)); 385 | }, 386 | 387 | findMany: async function (store, type, ids) { 388 | await this._init(store, type); 389 | return this.db.rel.find(this.getRecordTypeName(type), ids); 390 | }, 391 | 392 | findHasMany: async function (store, record, link, rel) { 393 | await this._init(store, record.type); 394 | let inverse = record.type.inverseFor(rel.key, store); 395 | if (inverse && inverse.kind === 'belongsTo') { 396 | return this.db.rel.findHasMany( 397 | camelize(rel.type), 398 | inverse.name, 399 | record.id 400 | ); 401 | } else { 402 | let result = {}; 403 | result[pluralize(rel.type)] = []; 404 | return result; //data; 405 | } 406 | }, 407 | 408 | query: async function (store, type, query) { 409 | await this._init(store, type); 410 | 411 | var recordTypeName = this.getRecordTypeName(type); 412 | var db = this.db; 413 | 414 | var queryParams = { 415 | selector: this._buildSelector(query.filter), 416 | }; 417 | 418 | if (!isEmpty(query.sort)) { 419 | queryParams.sort = this._buildSort(query.sort); 420 | } 421 | 422 | if (!isEmpty(query.limit)) { 423 | queryParams.limit = query.limit; 424 | } 425 | 426 | if (!isEmpty(query.skip)) { 427 | queryParams.skip = query.skip; 428 | } 429 | 430 | let pouchRes = await db.find(queryParams); 431 | return db.rel.parseRelDocs(recordTypeName, pouchRes.docs); 432 | }, 433 | 434 | queryRecord: async function (store, type, query) { 435 | let results = await this.query(store, type, query); 436 | let recordType = this.getRecordTypeName(type); 437 | let recordTypePlural = pluralize(recordType); 438 | if (results[recordTypePlural].length > 0) { 439 | results[recordType] = results[recordTypePlural][0]; 440 | } else { 441 | results[recordType] = null; 442 | } 443 | delete results[recordTypePlural]; 444 | return results; 445 | }, 446 | 447 | /** 448 | * `find` has been deprecated in ED 1.13 and is replaced by 'new store 449 | * methods', see: https://github.com/emberjs/data/pull/3306 450 | * We keep the method for backward compatibility and forward calls to 451 | * `findRecord`. This can be removed when the library drops support 452 | * for deprecated methods. 453 | */ 454 | find: function (store, type, id) { 455 | return this.findRecord(store, type, id); 456 | }, 457 | 458 | findRecord: async function (store, type, id) { 459 | await this._init(store, type); 460 | var recordTypeName = this.getRecordTypeName(type); 461 | return this._findRecord(recordTypeName, id); 462 | }, 463 | 464 | async _findRecord(recordTypeName, id) { 465 | let payload = await this.db.rel.find(recordTypeName, id); 466 | // Ember Data chokes on empty payload, this function throws 467 | // an error when the requested data is not found 468 | if (typeof payload === 'object' && payload !== null) { 469 | var singular = recordTypeName; 470 | var plural = pluralize(recordTypeName); 471 | 472 | var results = payload[singular] || payload[plural]; 473 | if (results && results.length > 0) { 474 | return payload; 475 | } 476 | } 477 | 478 | if (configFlagDisabled(this, 'eventuallyConsistent')) 479 | throw new Error( 480 | "Document of type '" + 481 | recordTypeName + 482 | "' with id '" + 483 | id + 484 | "' not found." 485 | ); 486 | else return this._eventuallyConsistent(recordTypeName, id); 487 | }, 488 | 489 | //TODO: cleanup promises on destroy or db change? 490 | waitingForConsistency: null, 491 | _eventuallyConsistent: function (type, id) { 492 | let pouchID = this.db.rel.makeDocID({ type, id }); 493 | let defered = defer(); 494 | this.waitingForConsistency[pouchID] = defered; 495 | 496 | return this.db.rel.isDeleted(type, id).then((deleted) => { 497 | //TODO: should we test the status of the promise here? Could it be handled in onChange already? 498 | if (deleted) { 499 | delete this.waitingForConsistency[pouchID]; 500 | throw new Error( 501 | "Document of type '" + type + "' with id '" + id + "' is deleted." 502 | ); 503 | } else if (deleted === null) { 504 | return defered.promise; 505 | } else { 506 | assert('Status should be existing', deleted === false); 507 | //TODO: should we reject or resolve the promise? or does JS GC still clean it? 508 | if (this.waitingForConsistency[pouchID]) { 509 | delete this.waitingForConsistency[pouchID]; 510 | return this._findRecord(type, id); 511 | } else { 512 | //findRecord is already handled by onChange 513 | return defered.promise; 514 | } 515 | } 516 | }); 517 | }, 518 | 519 | createdRecords: null, 520 | createRecord: async function (store, type, record) { 521 | await this._init(store, type); 522 | var data = this._recordToData(store, type, record); 523 | let rel = this.db.rel; 524 | 525 | let id = data.id; 526 | if (!id) { 527 | id = data.id = rel.uuid(); 528 | } 529 | this.createdRecords[id] = true; 530 | 531 | let typeName = this.getRecordTypeName(type); 532 | try { 533 | let saved = await rel.save(typeName, data); 534 | Object.assign(data, saved); 535 | let result = {}; 536 | result[pluralize(typeName)] = [data]; 537 | return result; 538 | } catch (e) { 539 | delete this.createdRecords[id]; 540 | throw e; 541 | } 542 | }, 543 | 544 | updateRecord: async function (store, type, record) { 545 | await this._init(store, type); 546 | var data = this._recordToData(store, type, record); 547 | let typeName = this.getRecordTypeName(type); 548 | let saved = await this.db.rel.save(typeName, data); 549 | Object.assign(data, saved); //TODO: could only set .rev 550 | let result = {}; 551 | result[pluralize(typeName)] = [data]; 552 | return result; 553 | }, 554 | 555 | deleteRecord: async function (store, type, record) { 556 | if (record.adapterOptions && record.adapterOptions.serverPush) return; 557 | 558 | await this._init(store, type); 559 | var data = this._recordToData(store, type, record); 560 | return this.db.rel 561 | .del(this.getRecordTypeName(type), data) 562 | .then(extractDeleteRecord); 563 | }, 564 | }) {} 565 | -------------------------------------------------------------------------------- /addon/index.js: -------------------------------------------------------------------------------- 1 | import Model from './model'; 2 | import Adapter from './adapters/pouch'; 3 | import Serializer from './serializers/pouch'; 4 | 5 | export { Model, Adapter, Serializer }; 6 | -------------------------------------------------------------------------------- /addon/model.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from '@ember-data/model'; 2 | 3 | export default Model.extend({ 4 | rev: attr('string'), 5 | }); 6 | -------------------------------------------------------------------------------- /addon/serializers/pouch.js: -------------------------------------------------------------------------------- 1 | import JSONSerializer from '@ember-data/serializer/json'; 2 | import RESTSerializer from '@ember-data/serializer/rest'; 3 | import { keys as EmberKeys } from '@ember/polyfills'; 4 | 5 | import { shouldSaveRelationship } from '../utils'; 6 | 7 | const keys = Object.keys || EmberKeys; 8 | 9 | var Serializer = RESTSerializer.extend({ 10 | init: function () { 11 | this._super(...arguments); 12 | }, 13 | 14 | shouldSerializeHasMany: function (snapshot, key, relationship) { 15 | let result = shouldSaveRelationship(this, relationship); 16 | return result; 17 | }, 18 | 19 | // This fixes a failure in Ember Data 1.13 where an empty hasMany 20 | // was saving as undefined rather than []. 21 | serializeHasMany(snapshot, json, relationship) { 22 | if ( 23 | this._shouldSerializeHasMany(snapshot, relationship.key, relationship) 24 | ) { 25 | this._super.apply(this, arguments); 26 | 27 | const key = relationship.key; 28 | 29 | if (!json[key]) { 30 | json[key] = []; 31 | } 32 | } 33 | }, 34 | 35 | _isAttachment(attribute) { 36 | return ['attachment', 'attachments'].indexOf(attribute.type) !== -1; 37 | }, 38 | 39 | serializeAttribute(snapshot, json, key, attribute) { 40 | this._super(snapshot, json, key, attribute); 41 | if (this._isAttachment(attribute)) { 42 | // if provided, use the mapping provided by `attrs` in the serializer 43 | var payloadKey = this._getMappedKey(key, snapshot.type); 44 | if (payloadKey === key && this.keyForAttribute) { 45 | payloadKey = this.keyForAttribute(key, 'serialize'); 46 | } 47 | 48 | // Merge any attachments in this attribute into the `attachments` property. 49 | // relational-pouch will put these in the special CouchDB `_attachments` property 50 | // of the document. 51 | // This will conflict with any 'attachments' attr in the model. Suggest that 52 | // #toRawDoc in relational-pouch should allow _attachments to be specified 53 | json.attachments = Object.assign( 54 | {}, 55 | json.attachments || {}, 56 | json[payloadKey] 57 | ); // jshint ignore:line 58 | json[payloadKey] = keys(json[payloadKey]).reduce((attr, fileName) => { 59 | attr[fileName] = Object.assign({}, json[payloadKey][fileName]); // jshint ignore:line 60 | delete attr[fileName].data; 61 | delete attr[fileName].content_type; 62 | return attr; 63 | }, {}); 64 | } 65 | }, 66 | 67 | extractAttributes(modelClass, resourceHash) { 68 | let attributes = this._super(modelClass, resourceHash); 69 | let modelAttrs = modelClass.attributes; 70 | modelClass.eachTransformedAttribute((key) => { 71 | let attribute = modelAttrs.get(key); 72 | if (this._isAttachment(attribute)) { 73 | // put the corresponding _attachments entries from the response into the attribute 74 | let fileNames = keys(attributes[key]); 75 | fileNames.forEach((fileName) => { 76 | attributes[key][fileName] = resourceHash.attachments[fileName]; 77 | }); 78 | } 79 | }); 80 | return attributes; 81 | }, 82 | 83 | extractRelationships(modelClass) { 84 | let relationships = this._super(...arguments); 85 | 86 | modelClass.eachRelationship((key, relationshipMeta) => { 87 | if ( 88 | relationshipMeta.kind === 'hasMany' && 89 | !shouldSaveRelationship(this, relationshipMeta) && 90 | !!relationshipMeta.options.async 91 | ) { 92 | relationships[key] = { links: { related: key } }; 93 | } 94 | }); 95 | 96 | return relationships; 97 | }, 98 | }); 99 | 100 | // DEPRECATION: The private method _shouldSerializeHasMany has been promoted to the public API 101 | // See https://www.emberjs.com/deprecations/ember-data/v2.x/#toc_jsonserializer-shouldserializehasmany 102 | if (!JSONSerializer.prototype.shouldSerializeHasMany) { 103 | Serializer.reopen({ 104 | _shouldSerializeHasMany(snapshot, key, relationship) { 105 | return this.shouldSerializeHasMany(snapshot, key, relationship); 106 | }, 107 | }); 108 | } 109 | 110 | export default Serializer; 111 | -------------------------------------------------------------------------------- /addon/transforms/attachment.js: -------------------------------------------------------------------------------- 1 | import { isNone } from '@ember/utils'; 2 | import AttachmentsTransform from './attachments'; 3 | 4 | export default AttachmentsTransform.extend({ 5 | deserialize: function (serialized) { 6 | return this._super(serialized).pop(); 7 | }, 8 | serialize: function (deserialized) { 9 | if (isNone(deserialized)) { 10 | return null; 11 | } 12 | return this._super([deserialized]); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /addon/transforms/attachments.js: -------------------------------------------------------------------------------- 1 | import Transform from '@ember-data/serializer/transform'; 2 | import { isArray } from '@ember/array'; 3 | import { keys as EmberKeys } from '@ember/polyfills'; 4 | import EmberObject from '@ember/object'; 5 | import { isNone } from '@ember/utils'; 6 | 7 | const keys = Object.keys || EmberKeys; 8 | 9 | export default Transform.extend({ 10 | deserialize: function (serialized) { 11 | if (isNone(serialized)) { 12 | return []; 13 | } 14 | 15 | return keys(serialized).map(function (attachmentName) { 16 | let attachment = serialized[attachmentName]; 17 | return EmberObject.create({ 18 | name: attachmentName, 19 | content_type: attachment.content_type, 20 | data: attachment.data, 21 | stub: attachment.stub, 22 | length: attachment.length, 23 | digest: attachment.digest, 24 | }); 25 | }); 26 | }, 27 | 28 | serialize: function (deserialized) { 29 | if (!isArray(deserialized)) { 30 | return null; 31 | } 32 | 33 | return deserialized.reduce(function (acc, attachment) { 34 | const serialized = { 35 | content_type: attachment.content_type, 36 | }; 37 | if (attachment.stub) { 38 | serialized.stub = true; 39 | serialized.length = attachment.length; 40 | serialized.digest = attachment.digest; 41 | } else { 42 | serialized.data = attachment.data; 43 | serialized.length = attachment.length; 44 | } 45 | acc[attachment.name] = serialized; 46 | return acc; 47 | }, {}); 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /addon/utils.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from '@ember/application'; 2 | 3 | // ember-data doesn't like getting a json response of {deleted: true} 4 | export function extractDeleteRecord() { 5 | return null; 6 | } 7 | 8 | //should this take a config? 9 | export function shouldSaveRelationship(container, relationship) { 10 | if (typeof relationship.options.save !== 'undefined') 11 | return relationship.options.save; 12 | 13 | if (relationship.kind === 'belongsTo') return true; 14 | 15 | //TODO: save default locally? probably on container? 16 | let saveDefault = configFlagEnabled(container, 'saveHasMany'); //default is false if not specified 17 | 18 | return saveDefault; 19 | } 20 | 21 | export function configFlagDisabled(container, key) { 22 | //default is on 23 | let config = getOwner(container).resolveRegistration('config:environment'); 24 | let result = 25 | config['emberPouch'] && 26 | typeof config['emberPouch'][key] !== 'undefined' && 27 | !config['emberPouch'][key]; 28 | 29 | return result; 30 | } 31 | 32 | export function configFlagEnabled(container, key) { 33 | //default is off 34 | let config = getOwner(container).resolveRegistration('config:environment'); 35 | let result = config['emberPouch'] && config['emberPouch'][key]; 36 | 37 | return result; 38 | } 39 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/app/.gitkeep -------------------------------------------------------------------------------- /app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import { Serializer } from 'ember-pouch'; 2 | 3 | export default Serializer.extend(); 4 | -------------------------------------------------------------------------------- /app/transforms/attachment.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-pouch/transforms/attachment'; 2 | -------------------------------------------------------------------------------- /app/transforms/attachments.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-pouch/transforms/attachments'; 2 | -------------------------------------------------------------------------------- /blueprints/ember-pouch/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | normalizeEntityName() { 5 | return 'application'; 6 | }, 7 | 8 | filesPath() { 9 | return path.join(this.path, '../pouch-adapter/files'); 10 | }, 11 | 12 | afterInstall(/*options*/) { 13 | this.addPackagesToProject([ 14 | { name: 'pouchdb-core' }, 15 | { name: 'pouchdb-adapter-indexeddb' }, 16 | { name: 'pouchdb-adapter-http' }, 17 | { name: 'pouchdb-mapreduce' }, 18 | { name: 'pouchdb-replication' }, 19 | { name: 'pouchdb-find' }, 20 | { name: 'relational-pouch' }, 21 | ]); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /blueprints/pouch-adapter/files/__root__/adapters/__name__.js: -------------------------------------------------------------------------------- 1 | import config from '<%= dasherizedPackageName %>/config/environment'; 2 | import { Adapter } from 'ember-pouch'; 3 | import { assert } from '@ember/debug'; 4 | import { isEmpty } from '@ember/utils'; 5 | 6 | import PouchDB from 'pouchdb-core'; 7 | import PouchDBFind from 'pouchdb-find'; 8 | import PouchDBRelational from 'relational-pouch'; 9 | import indexeddb from 'pouchdb-adapter-indexeddb'; 10 | import HttpPouch from 'pouchdb-adapter-http'; 11 | import mapreduce from 'pouchdb-mapreduce'; 12 | import replication from 'pouchdb-replication'; 13 | 14 | PouchDB.plugin(PouchDBFind) 15 | .plugin(PouchDBRelational) 16 | .plugin(indexeddb) 17 | .plugin(HttpPouch) 18 | .plugin(mapreduce) 19 | .plugin(replication); 20 | 21 | export default class ApplicationAdapter extends Adapter { 22 | 23 | constructor() { 24 | super(...arguments); 25 | 26 | const localDb = config.emberPouch.localDb; 27 | 28 | assert('emberPouch.localDb must be set', !isEmpty(localDb)); 29 | 30 | const db = new PouchDB(localDb); 31 | this.db = db; 32 | 33 | // If we have specified a remote CouchDB instance, then replicate our local database to it 34 | if (config.emberPouch.remoteDb) { 35 | let remoteDb = new PouchDB(config.emberPouch.remoteDb); 36 | 37 | db.sync(remoteDb, { 38 | live: true, 39 | retry: true 40 | }); 41 | } 42 | 43 | return this; 44 | } 45 | 46 | unloadedDocumentChanged(obj) { 47 | let recordTypeName = this.getRecordTypeName(this.store.modelFor(obj.type)); 48 | this.db.rel.find(recordTypeName, obj.id).then((doc) => { 49 | this.store.pushPayload(recordTypeName, doc); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /blueprints/pouch-adapter/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: '', 3 | 4 | // locals: function(options) { 5 | // // Return custom template variables here. 6 | // return { 7 | // foo: options.entity.options.foo 8 | // }; 9 | // } 10 | 11 | // afterInstall: function(options) { 12 | // // Perform extra work here. 13 | // } 14 | }; 15 | -------------------------------------------------------------------------------- /blueprints/pouch-model/files/__root__/models/__name__.js: -------------------------------------------------------------------------------- 1 | import { attr, belongsTo, hasMany } from '@ember-data/model'; 2 | import { Model } from 'ember-pouch'; 3 | 4 | export default class <%= camelizedModuleName %>Model extends Model { 5 | // @attr('string') name; 6 | // @belongsTo('author') author; 7 | // @hasMany('comments') comments; 8 | } 9 | -------------------------------------------------------------------------------- /blueprints/pouch-model/index.js: -------------------------------------------------------------------------------- 1 | var ModelBlueprint; 2 | 3 | try { 4 | ModelBlueprint = require('ember-data/blueprints/model'); 5 | } catch (e) { 6 | //eslint-disable-next-line node/no-missing-require 7 | ModelBlueprint = require('ember-cli/blueprints/model'); 8 | } 9 | 10 | module.exports = ModelBlueprint; 11 | -------------------------------------------------------------------------------- /codemods.log: -------------------------------------------------------------------------------- 1 | 2022-02-24T12:05:03.079Z [warn] WARNING: {{page-title}} was not converted as it has positional parameters which can't be automatically converted. Source: tests/dummy/app/templates/application.hbs 2 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | useYarn: false, 9 | scenarios: [ 10 | { 11 | name: 'ember-lts-3.16', 12 | npm: { 13 | devDependencies: { 14 | 'ember-source': '~3.16.0', 15 | 'ember-data': '~3.16.0', 16 | }, 17 | }, 18 | }, 19 | { 20 | name: 'ember-lts-3.24', 21 | npm: { 22 | devDependencies: { 23 | 'ember-source': '~3.24.3', 24 | 'ember-data': '~3.24.0', 25 | }, 26 | }, 27 | }, 28 | { 29 | name: 'ember-lts-3.28', 30 | npm: { 31 | devDependencies: { 32 | 'ember-source': '~3.28.0', 33 | 'ember-data': '~3.28.7', 34 | }, 35 | }, 36 | }, 37 | { 38 | name: 'ember-lts-4.4', 39 | npm: { 40 | devDependencies: { 41 | 'ember-source': '~4.4.0', 42 | 'ember-data': '~4.4.0', 43 | }, 44 | }, 45 | }, 46 | { 47 | name: 'ember-release', 48 | npm: { 49 | devDependencies: { 50 | 'ember-data': 'latest', 51 | 'ember-source': await getChannelURL('release'), 52 | }, 53 | }, 54 | }, 55 | { 56 | name: 'ember-beta', 57 | npm: { 58 | devDependencies: { 59 | 'ember-data': 'beta', 60 | 'ember-source': await getChannelURL('beta'), 61 | }, 62 | }, 63 | }, 64 | { 65 | name: 'ember-canary', 66 | npm: { 67 | devDependencies: { 68 | 'ember-data': 'canary', 69 | 'ember-source': await getChannelURL('canary'), 70 | }, 71 | }, 72 | }, 73 | // The default `.travis.yml` runs this scenario via `npm test`, 74 | // not via `ember try`. It's still included here so that running 75 | // `ember try:each` manually or from a customized CI config will run it 76 | // along with all the other scenarios. 77 | { 78 | name: 'ember-default', 79 | npm: { 80 | devDependencies: {}, 81 | }, 82 | }, 83 | { 84 | name: 'ember-classic', 85 | env: { 86 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 87 | 'application-template-wrapper': true, 88 | 'default-async-observers': false, 89 | 'template-only-glimmer-components': false, 90 | }), 91 | }, 92 | npm: { 93 | devDependencies: { 94 | 'ember-source': '~3.28.0', 95 | 'ember-data': '~3.28.7', 96 | }, 97 | ember: { 98 | edition: 'classic', 99 | }, 100 | }, 101 | }, 102 | embroiderSafe(), 103 | embroiderOptimized(), 104 | ], 105 | }; 106 | }; 107 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function (/* environment, appConfig */) { 5 | return {}; 6 | }; 7 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | const { maybeEmbroider } = require('@embroider/test-setup'); 5 | 6 | /** 7 | * `EMBROIDER_TEST_SETUP_OPTIONS` is set by the Embroider scenarios for `ember-try`: 8 | * https://github.com/embroider-build/embroider/blob/v0.47.1/packages/test-setup/src/index.ts#L48-L90 9 | */ 10 | const IS_EMBROIDER_ENABLED = Boolean(process.env.EMBROIDER_TEST_SETUP_OPTIONS); 11 | 12 | module.exports = function (defaults) { 13 | let app = new EmberAddon(defaults, { 14 | // Add options here 15 | }); 16 | 17 | /* 18 | This build file specifies the options for the dummy test app of this 19 | addon, located in `/tests/dummy` 20 | This build file does *not* influence how the addon or the app using it 21 | behave. You most likely want to be modifying `./index.js` or app's build file 22 | */ 23 | 24 | return maybeEmbroider(app, { 25 | skipBabel: [ 26 | { 27 | package: 'qunit', 28 | }, 29 | ], 30 | /* eslint-disable prettier/prettier */ 31 | packagerOptions: { 32 | webpackConfig: IS_EMBROIDER_ENABLED === false ? {} : { 33 | node: { 34 | global: true, 35 | }, 36 | }, 37 | }, 38 | /* eslint-disable prettier/prettier */ 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: require('./package').name, 6 | options: { 7 | autoImport: { 8 | webpack: { 9 | node: { 10 | global: true, 11 | }, 12 | }, 13 | }, 14 | }, 15 | 16 | init: function () { 17 | this._super.init && this._super.init.apply(this, arguments); 18 | }, 19 | 20 | included(app) { 21 | this._super.included.apply(this, arguments); 22 | 23 | // see: https://github.com/ember-cli/ember-cli/issues/3718 24 | if (typeof app.import !== 'function' && app.app) { 25 | app = app.app; 26 | } 27 | 28 | let env = this.project.config(app.env); 29 | if (env.emberpouch) { 30 | if ( 31 | Object.prototype.hasOwnProperty.call(env.emberpouch, 'dontsavehasmany') 32 | ) { 33 | this.ui.writeWarnLine( 34 | 'The `dontsavehasmany` flag is no longer needed in `config/environment.js`' 35 | ); 36 | } 37 | } 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-pouch", 3 | "version": "8.0.0-beta.2", 4 | "description": "PouchDB adapter for Ember Data", 5 | "keywords": [ 6 | "ember-addon", 7 | "Ember", 8 | "Octane", 9 | "Data", 10 | "adapter", 11 | "PouchDB", 12 | "CouchDB" 13 | ], 14 | "bugs": { 15 | "url": "https://github.com/pouchdb-community/ember-pouch/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/pouchdb-community/ember-pouch.git" 20 | }, 21 | "license": "Apache-2.0", 22 | "author": "Nolan Lawson", 23 | "directories": { 24 | "doc": "doc", 25 | "test": "tests" 26 | }, 27 | "scripts": { 28 | "build": "ember build --environment=production", 29 | "license": "license-checker --summary --failOn GPL", 30 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", 31 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 32 | "lint:hbs": "ember-template-lint .", 33 | "lint:hbs:fix": "ember-template-lint . --fix", 34 | "lint:js": "eslint . --cache", 35 | "lint:js:fix": "eslint . --fix", 36 | "start": "ember server", 37 | "test": "npm-run-all lint test:*", 38 | "test:ember": "ember test", 39 | "test:ember-compatibility": "ember try:each" 40 | }, 41 | "dependencies": { 42 | "ember-auto-import": "~2.4.2", 43 | "ember-cli-babel": "^7.26.11" 44 | }, 45 | "devDependencies": { 46 | "@ember/optional-features": "^2.0.0", 47 | "@ember/test-helpers": "^2.6.0", 48 | "@embroider/test-setup": "^1.5.0", 49 | "@glimmer/component": "^1.0.4", 50 | "@glimmer/tracking": "^1.0.4", 51 | "babel-eslint": "^10.1.0", 52 | "broccoli-asset-rev": "^3.0.0", 53 | "ember-cli": "~4.3.0", 54 | "ember-cli-app-version": "~5.0.0", 55 | "ember-cli-dependency-checker": "^3.2.0", 56 | "ember-cli-htmlbars": "^6.0.1", 57 | "ember-cli-inject-live-reload": "^2.1.0", 58 | "ember-cli-release": "^0.2.9", 59 | "ember-cli-sri": "^2.1.1", 60 | "ember-cli-terser": "^4.0.2", 61 | "ember-data": "~4.4.0", 62 | "ember-disable-prototype-extensions": "^1.1.3", 63 | "ember-export-application-global": "^2.0.1", 64 | "ember-inflector": "~4.0.2", 65 | "ember-load-initializers": "^2.1.2", 66 | "ember-maybe-import-regenerator": "^1.0.0", 67 | "ember-qunit": "^5.1.5", 68 | "ember-resolver": "^8.0.3", 69 | "ember-source": "~4.3.0", 70 | "ember-source-channel-url": "^3.0.0", 71 | "ember-template-lint": "^4.3.0", 72 | "ember-try": "^2.0.0", 73 | "eslint": "^7.32.0", 74 | "eslint-config-prettier": "^8.5.0", 75 | "eslint-plugin-ember": "^10.5.9", 76 | "eslint-plugin-node": "^11.1.0", 77 | "eslint-plugin-prettier": "^4.0.0", 78 | "eslint-plugin-qunit": "^7.2.0", 79 | "license-checker": "^25.0.1", 80 | "loader.js": "^4.7.0", 81 | "npm-run-all": "^4.1.5", 82 | "pouchdb-core": "^7.3.1", 83 | "pouchdb-adapter-indexeddb": "^7.3.1", 84 | "pouchdb-adapter-http": "^7.3.1", 85 | "pouchdb-mapreduce": "^7.3.1", 86 | "pouchdb-replication": "^7.3.1", 87 | "pouchdb-find": "^7.3.1", 88 | "relational-pouch": "^4.0.1", 89 | "prettier": "^2.6.1", 90 | "qunit": "^2.18.0", 91 | "qunit-dom": "^2.0.0", 92 | "webpack": "^5.70.0" 93 | }, 94 | "engines": { 95 | "node": "12.* || 14.* || >= 16" 96 | }, 97 | "ember": { 98 | "edition": "octane" 99 | }, 100 | "ember-addon": { 101 | "configPath": "tests/dummy/config" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import { defer } from 'rsvp'; 2 | import { assert } from '@ember/debug'; 3 | import { isEmpty } from '@ember/utils'; 4 | import { Adapter } from 'ember-pouch'; 5 | import PouchDB from 'dummy/pouchdb'; 6 | import config from 'dummy/config/environment'; 7 | 8 | function createDb() { 9 | let localDb = config.emberPouch.localDb; 10 | 11 | assert('emberPouch.localDb must be set', !isEmpty(localDb)); 12 | 13 | let db = new PouchDB(localDb); 14 | 15 | if (config.emberPouch.remote) { 16 | let remoteDb = new PouchDB(config.emberPouch.remoteDb); 17 | 18 | db.sync(remoteDb, { 19 | live: true, 20 | retry: true, 21 | }); 22 | } 23 | 24 | return db; 25 | } 26 | 27 | export default class ApplicationAdapter extends Adapter { 28 | constructor(owner, args) { 29 | super(owner, args); 30 | this.db = createDb(); 31 | } 32 | 33 | _init(store, type) { 34 | type.eachRelationship((name, rel) => { 35 | rel.options.async = config.emberPouch.async; 36 | if (rel.kind === 'hasMany') { 37 | rel.options.save = config.emberPouch.saveHasMany; 38 | } 39 | }); 40 | if (super._init) { 41 | return super._init(...arguments); 42 | } 43 | } 44 | 45 | onChangeListenerTest = null; 46 | async onChange() { 47 | if (super.onChange) { 48 | await super.onChange(...arguments); 49 | } 50 | if (this.onChangeListenerTest) { 51 | this.onChangeListenerTest(...arguments); 52 | } 53 | } 54 | 55 | waitForChangeWithID(id) { 56 | let defered = defer(); 57 | this.onChangeListenerTest = (c) => { 58 | if (c.id === id) { 59 | this.onChangeListenerTest = null; 60 | defered.resolve(c); 61 | } 62 | }; 63 | return defered.promise; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/taco-salad.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { assert } from '@ember/debug'; 3 | import { isEmpty } from '@ember/utils'; 4 | import { Adapter } from 'ember-pouch'; 5 | import PouchDB from 'dummy/pouchdb'; 6 | import config from 'dummy/config/environment'; 7 | 8 | function createDb() { 9 | let localDb = config.emberPouch.localDb; 10 | 11 | assert('emberPouch.localDb must be set', !isEmpty(localDb)); 12 | 13 | let db = new PouchDB(localDb); 14 | 15 | if (config.emberPouch.remote) { 16 | let remoteDb = new PouchDB(config.emberPouch.remoteDb); 17 | 18 | db.sync(remoteDb, { 19 | live: true, 20 | retry: true, 21 | }); 22 | } 23 | 24 | return db; 25 | } 26 | 27 | export default class TacoSaladAdapter extends Adapter { 28 | constructor(owner, args) { 29 | super(owner, args); 30 | this.db = createDb(); 31 | } 32 | 33 | _init(store, type) { 34 | type.eachRelationship((name, rel) => { 35 | rel.options.async = config.emberPouch.async; 36 | if (rel.kind === 'hasMany') { 37 | rel.options.save = config.emberPouch.saveHasMany; 38 | } 39 | }); 40 | if (super._init) { 41 | return super._init(...arguments); 42 | } 43 | } 44 | 45 | unloadedDocumentChanged(obj) { 46 | let store = this.store; 47 | let recordTypeName = this.getRecordTypeName(store.modelFor(obj.type)); 48 | this.db.rel.find(recordTypeName, obj.id).then(function (doc) { 49 | run(function () { 50 | store.pushPayload(recordTypeName, doc); 51 | }); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver, 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/models/food-item.js: -------------------------------------------------------------------------------- 1 | import { attr, belongsTo } from '@ember-data/model'; 2 | import { Model } from 'ember-pouch'; 3 | 4 | // N.b.: awkward model name is to test getRecordTypeName 5 | 6 | export default Model.extend({ 7 | name: attr('string'), 8 | soup: belongsTo('taco-soup'), 9 | }); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/models/smasher.js: -------------------------------------------------------------------------------- 1 | import { attr } from '@ember-data/model'; 2 | import { Model } from 'ember-pouch'; 3 | 4 | export default Model.extend({ 5 | name: attr('string'), 6 | series: attr('string'), 7 | debut: attr(), 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dummy/app/models/taco-recipe.js: -------------------------------------------------------------------------------- 1 | import { attr } from '@ember-data/model'; 2 | import { Model } from 'ember-pouch'; 3 | 4 | export default Model.extend({ 5 | coverImage: attr('attachment'), 6 | photos: attr('attachments'), 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/models/taco-salad.js: -------------------------------------------------------------------------------- 1 | import { attr, hasMany } from '@ember-data/model'; 2 | import { Model } from 'ember-pouch'; 3 | 4 | export default Model.extend({ 5 | flavor: attr('string'), 6 | ingredients: hasMany('food-item'), 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/models/taco-soup.js: -------------------------------------------------------------------------------- 1 | import { attr, hasMany } from '@ember-data/model'; 2 | import { Model } from 'ember-pouch'; 3 | 4 | export default Model.extend({ 5 | flavor: attr('string'), 6 | ingredients: hasMany('food-item'), 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/pouchdb.js: -------------------------------------------------------------------------------- 1 | import PouchDB from 'pouchdb-core'; 2 | import PouchDBFind from 'pouchdb-find'; 3 | import PouchDBRelational from 'relational-pouch'; 4 | import indexeddb from 'pouchdb-adapter-indexeddb'; 5 | import HttpPouch from 'pouchdb-adapter-http'; 6 | import mapreduce from 'pouchdb-mapreduce'; 7 | import replication from 'pouchdb-replication'; 8 | 9 | PouchDB.plugin(PouchDBFind) 10 | .plugin(PouchDBRelational) 11 | .plugin(indexeddb) 12 | .plugin(HttpPouch) 13 | .plugin(mapreduce) 14 | .plugin(replication); 15 | 16 | export default PouchDB; 17 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'dummy/config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () {}); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/serializers/taco-recipe.js: -------------------------------------------------------------------------------- 1 | import ApplicationSerializer from './application'; 2 | 3 | export default ApplicationSerializer.extend({ 4 | attrs: { 5 | coverImage: 'cover_image', 6 | photos: { key: 'photo_gallery' }, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

Welcome to Ember

2 | {{outlet}} -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.28.5", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": ["--no-welcome"] 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function (environment) { 5 | let ENV = { 6 | modulePrefix: 'dummy', 7 | environment, 8 | emberPouch: { localDb: 'ember-pouch-test' }, 9 | rootURL: '/', 10 | locationType: 'history', 11 | EmberENV: { 12 | FEATURES: { 13 | // Here you can enable experimental features on an ember canary build 14 | // e.g. 'with-controller': true 15 | 'ds-references': true, 16 | }, 17 | EXTEND_PROTOTYPES: { 18 | // Prevent Ember Data from overriding Date.parse. 19 | Date: false, 20 | }, 21 | }, 22 | 23 | APP: { 24 | // Here you can pass flags/options to your application instance 25 | // when it is created 26 | }, 27 | }; 28 | 29 | if (environment === 'development') { 30 | // ENV.APP.LOG_RESOLVER = true; 31 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 32 | // ENV.APP.LOG_TRANSITIONS = true; 33 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 34 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 35 | } 36 | 37 | if (environment === 'test') { 38 | // Testem prefers this... 39 | ENV.locationType = 'history'; 40 | 41 | // keep test console output quieter 42 | ENV.APP.LOG_ACTIVE_GENERATION = false; 43 | ENV.APP.LOG_VIEW_LOOKUPS = false; 44 | 45 | ENV.APP.rootElement = '#ember-testing'; 46 | ENV.APP.autoboot = false; 47 | } 48 | 49 | if (environment === 'production') { 50 | // here you can enable a production-specific feature 51 | } 52 | 53 | return ENV; 54 | }; 55 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | // Ember's browser support policy is changing, and IE11 support will end in 10 | // v4.0 onwards. 11 | // 12 | // See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy 13 | // 14 | // If you need IE11 support on a version of Ember that still offers support 15 | // for it, uncomment the code block below. 16 | // 17 | // const isCI = Boolean(process.env.CI); 18 | // const isProduction = process.env.EMBER_ENV === 'production'; 19 | // 20 | // if (isCI || isProduction) { 21 | // browsers.push('ie 11'); 22 | // } 23 | 24 | module.exports = { 25 | browsers, 26 | }; 27 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/helpers/module-for-pouch-acceptance.js: -------------------------------------------------------------------------------- 1 | import { Promise, all, resolve } from 'rsvp'; 2 | 3 | export default function (hooks) { 4 | hooks.beforeEach(function () { 5 | return Promise.resolve().then(() => { 6 | this.store = function store() { 7 | return this.owner.lookup('service:store'); 8 | }; 9 | 10 | // At the container level, adapters are not singletons (ember-data 11 | // manages them). To get the instance that the app is using, we have to 12 | // go through the store. 13 | this.adapter = function adapter() { 14 | return this.store().adapterFor('taco-soup'); 15 | }; 16 | 17 | this.db = function db() { 18 | return this.adapter().get('db'); 19 | }; 20 | }); 21 | }); 22 | 23 | hooks.afterEach(function () { 24 | let db = this.db(); 25 | return all(this.adapter()._indexPromises) 26 | .then(() => { 27 | return db.getIndexes().then((data) => { 28 | return all( 29 | data.indexes.map((index) => { 30 | return index.ddoc ? db.deleteIndex(index) : resolve(); 31 | }) 32 | ); 33 | }); 34 | }) 35 | .then(() => db.destroy()); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} {{content-for "test-head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} {{content-for "test-head-footer"}} 17 | 18 | 19 | {{content-for "body"}} {{content-for "test-body"}} 20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {{content-for "body-footer"}} {{content-for "test-body-footer"}} 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/integration/adapters/pouch-basics-test.js: -------------------------------------------------------------------------------- 1 | import { later, run } from '@ember/runloop'; 2 | import { Promise, all } from 'rsvp'; 3 | import { module, test } from 'qunit'; 4 | import { setupTest } from 'ember-qunit'; 5 | 6 | // import DS from 'ember-data'; 7 | import moduleForIntegration from '../../helpers/module-for-pouch-acceptance'; 8 | 9 | import config from 'dummy/config/environment'; 10 | 11 | function promiseToRunLater(timeout) { 12 | return new Promise((resolve) => { 13 | later(() => { 14 | resolve(); 15 | }, timeout); 16 | }); 17 | } 18 | 19 | //function delayPromise(timeout) { 20 | // return function(res) { 21 | // return promiseToRunLater(timeout).then(() => res); 22 | // } 23 | //} 24 | 25 | function savingHasMany() { 26 | return config.emberPouch.saveHasMany; 27 | } 28 | 29 | function getDocsForRelations() { 30 | let result = []; 31 | 32 | let c = { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }; 33 | if (savingHasMany()) { 34 | c.data.ingredients = ['X', 'Y']; 35 | } 36 | result.push(c); 37 | 38 | let d = { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }; 39 | if (savingHasMany()) { 40 | d.data.ingredients = ['Z']; 41 | } 42 | result.push(d); 43 | 44 | result.push({ _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' } }); 45 | result.push({ _id: 'foodItem_2_Y', data: { name: 'pork loin', soup: 'C' } }); 46 | result.push({ 47 | _id: 'foodItem_2_Z', 48 | data: { name: 'black beans', soup: 'D' }, 49 | }); 50 | 51 | return result; 52 | } 53 | 54 | /* 55 | * Tests basic CRUD behavior for an app using the ember-pouch adapter. 56 | */ 57 | 58 | module('Integration | Adapter | Basic CRUD Ops', {}, function (hooks) { 59 | setupTest(hooks); 60 | moduleForIntegration(hooks); 61 | 62 | let allTests = function () { 63 | test('can find all', function (assert) { 64 | assert.expect(3); 65 | 66 | var done = assert.async(); 67 | Promise.resolve() 68 | .then(() => { 69 | return this.db().bulkDocs([ 70 | { _id: 'tacoSoup_2_A', data: { flavor: 'al pastor' } }, 71 | { _id: 'tacoSoup_2_B', data: { flavor: 'black bean' } }, 72 | { _id: 'burritoShake_2_X', data: { consistency: 'smooth' } }, 73 | ]); 74 | }) 75 | .then(() => { 76 | return this.store().findAll('taco-soup'); 77 | }) 78 | .then((found) => { 79 | assert.strictEqual( 80 | found.get('length'), 81 | 2, 82 | 'should have found the two taco soup items only' 83 | ); 84 | assert.deepEqual( 85 | found.mapBy('id'), 86 | ['A', 'B'], 87 | 'should have extracted the IDs correctly' 88 | ); 89 | assert.deepEqual( 90 | found.mapBy('flavor'), 91 | ['al pastor', 'black bean'], 92 | 'should have extracted the attributes also' 93 | ); 94 | }) 95 | .finally(done); 96 | }); 97 | 98 | test('can find one', function (assert) { 99 | assert.expect(2); 100 | 101 | var done = assert.async(); 102 | Promise.resolve() 103 | .then(() => { 104 | return this.db().bulkDocs([ 105 | { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, 106 | { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, 107 | ]); 108 | }) 109 | .then(() => { 110 | return this.store().find('taco-soup', 'D'); 111 | }) 112 | .then((found) => { 113 | assert.strictEqual( 114 | found.get('id'), 115 | 'D', 116 | 'should have found the requested item' 117 | ); 118 | assert.deepEqual( 119 | found.get('flavor'), 120 | 'black bean', 121 | 'should have extracted the attributes also' 122 | ); 123 | }) 124 | .finally(done); 125 | }); 126 | 127 | test('can query with sort', function (assert) { 128 | assert.expect(3); 129 | var done = assert.async(); 130 | Promise.resolve() 131 | .then(() => { 132 | return this.db() 133 | .createIndex({ 134 | index: { 135 | fields: ['data.name'], 136 | }, 137 | }) 138 | .then(() => { 139 | return this.db().bulkDocs([ 140 | { 141 | _id: 'smasher_2_mario', 142 | data: { name: 'Mario', series: 'Mario', debut: 1981 }, 143 | }, 144 | { 145 | _id: 'smasher_2_puff', 146 | data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }, 147 | }, 148 | { 149 | _id: 'smasher_2_link', 150 | data: { name: 'Link', series: 'Zelda', debut: 1986 }, 151 | }, 152 | { 153 | _id: 'smasher_2_dk', 154 | data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }, 155 | }, 156 | { 157 | _id: 'smasher_2_pika', 158 | data: { 159 | name: 'Pikachu', 160 | series: 'Pokemon', 161 | _id: 'pikachu', 162 | debut: 1996, 163 | }, 164 | }, 165 | ]); 166 | }); 167 | }) 168 | .then(() => { 169 | return this.store().query('smasher', { 170 | filter: { name: { $gt: '' } }, 171 | sort: ['name'], 172 | }); 173 | }) 174 | .then((found) => { 175 | assert.strictEqual( 176 | found.get('length'), 177 | 5, 178 | 'should returns all the smashers ' 179 | ); 180 | assert.deepEqual( 181 | found.mapBy('id'), 182 | ['dk', 'puff', 'link', 'mario', 'pika'], 183 | 'should have extracted the IDs correctly' 184 | ); 185 | assert.deepEqual( 186 | found.mapBy('name'), 187 | ['Donkey Kong', 'Jigglypuff', 'Link', 'Mario', 'Pikachu'], 188 | 'should have extracted the attributes also' 189 | ); 190 | }) 191 | .finally(done); 192 | }); 193 | 194 | test('can query multi-field queries', function (assert) { 195 | assert.expect(3); 196 | var done = assert.async(); 197 | Promise.resolve() 198 | .then(() => { 199 | return this.db() 200 | .createIndex({ 201 | index: { 202 | fields: ['data.series', 'data.debut'], 203 | }, 204 | }) 205 | .then(() => { 206 | return this.db().bulkDocs([ 207 | { 208 | _id: 'smasher_2_mario', 209 | data: { name: 'Mario', series: 'Mario', debut: 1981 }, 210 | }, 211 | { 212 | _id: 'smasher_2_puff', 213 | data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }, 214 | }, 215 | { 216 | _id: 'smasher_2_link', 217 | data: { name: 'Link', series: 'Zelda', debut: 1986 }, 218 | }, 219 | { 220 | _id: 'smasher_2_dk', 221 | data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }, 222 | }, 223 | { 224 | _id: 'smasher_2_pika', 225 | data: { 226 | name: 'Pikachu', 227 | series: 'Pokemon', 228 | _id: 'pikachu', 229 | debut: 1996, 230 | }, 231 | }, 232 | ]); 233 | }); 234 | }) 235 | .then(() => { 236 | return this.store().query('smasher', { 237 | filter: { series: 'Mario' }, 238 | sort: [{ series: 'desc' }, { debut: 'desc' }], 239 | }); 240 | }) 241 | .then((found) => { 242 | assert.strictEqual( 243 | found.get('length'), 244 | 2, 245 | 'should have found the two smashers' 246 | ); 247 | assert.deepEqual( 248 | found.mapBy('id'), 249 | ['mario', 'dk'], 250 | 'should have extracted the IDs correctly' 251 | ); 252 | assert.deepEqual( 253 | found.mapBy('name'), 254 | ['Mario', 'Donkey Kong'], 255 | 'should have extracted the attributes also' 256 | ); 257 | }) 258 | .finally(done); 259 | }); 260 | 261 | test('queryRecord returns null when no record is found', function (assert) { 262 | assert.expect(1); 263 | var done = assert.async(); 264 | Promise.resolve() 265 | .then(() => { 266 | return this.db() 267 | .createIndex({ 268 | index: { 269 | fields: ['data.flavor'], 270 | }, 271 | }) 272 | .then(() => { 273 | return this.db().bulkDocs([ 274 | { 275 | _id: 'tacoSoup_2_C', 276 | data: { flavor: 'al pastor', ingredients: ['X', 'Y'] }, 277 | }, 278 | { 279 | _id: 'tacoSoup_2_D', 280 | data: { flavor: 'black bean', ingredients: ['Z'] }, 281 | }, 282 | { _id: 'foodItem_2_X', data: { name: 'pineapple' } }, 283 | { _id: 'foodItem_2_Y', data: { name: 'pork loin' } }, 284 | { _id: 'foodItem_2_Z', data: { name: 'black beans' } }, 285 | ]); 286 | }); 287 | }) 288 | .then(() => { 289 | return this.store().queryRecord('taco-soup', { 290 | filter: { flavor: 'all pastor' }, 291 | }); 292 | }) 293 | .then((found) => { 294 | assert.strictEqual(found, null, 'should be null'); 295 | done(); 296 | }) 297 | .catch((error) => { 298 | assert.ok(false, 'error in test:' + error); 299 | done(); 300 | }); 301 | }); 302 | 303 | test('can query one record', function (assert) { 304 | assert.expect(1); 305 | 306 | var done = assert.async(); 307 | Promise.resolve() 308 | .then(() => { 309 | return this.db() 310 | .createIndex({ 311 | index: { 312 | fields: ['data.flavor'], 313 | }, 314 | }) 315 | .then(() => { 316 | return this.db().bulkDocs(getDocsForRelations()); 317 | }); 318 | }) 319 | .then(() => { 320 | return this.store().queryRecord('taco-soup', { 321 | filter: { flavor: 'al pastor' }, 322 | }); 323 | }) 324 | .then((found) => { 325 | assert.strictEqual( 326 | found.get('flavor'), 327 | 'al pastor', 328 | 'should have found the requested item' 329 | ); 330 | }) 331 | .finally(done); 332 | }); 333 | 334 | test('can query one associated records', function (assert) { 335 | assert.expect(3); 336 | var done = assert.async(); 337 | Promise.resolve() 338 | .then(() => { 339 | return this.db() 340 | .createIndex({ 341 | index: { 342 | fields: ['data.flavor'], 343 | }, 344 | }) 345 | .then(() => { 346 | return this.db().bulkDocs(getDocsForRelations()); 347 | }); 348 | }) 349 | .then(() => { 350 | return this.store().queryRecord('taco-soup', { 351 | filter: { flavor: 'al pastor' }, 352 | }); 353 | }) 354 | .then((found) => { 355 | assert.strictEqual( 356 | found.get('flavor'), 357 | 'al pastor', 358 | 'should have found the requested item' 359 | ); 360 | return found.get('ingredients'); 361 | }) 362 | .then((foundIngredients) => { 363 | assert.deepEqual( 364 | foundIngredients.mapBy('id'), 365 | ['X', 'Y'], 366 | 'should have found both associated items' 367 | ); 368 | assert.deepEqual( 369 | foundIngredients.mapBy('name'), 370 | ['pineapple', 'pork loin'], 371 | 'should have fully loaded the associated items' 372 | ); 373 | }) 374 | .finally(done); 375 | }); 376 | 377 | test('can find associated records', function (assert) { 378 | assert.expect(3); 379 | 380 | var done = assert.async(); 381 | Promise.resolve() 382 | .then(() => { 383 | return this.db().bulkDocs(getDocsForRelations()); 384 | }) 385 | .then(() => { 386 | return this.store().find('taco-soup', 'C'); 387 | }) 388 | .then((found) => { 389 | assert.strictEqual( 390 | found.get('id'), 391 | 'C', 392 | 'should have found the requested item' 393 | ); 394 | return found.get('ingredients'); 395 | }) 396 | .then((foundIngredients) => { 397 | assert.deepEqual( 398 | foundIngredients.mapBy('id'), 399 | ['X', 'Y'], 400 | 'should have found both associated items' 401 | ); 402 | assert.deepEqual( 403 | foundIngredients.mapBy('name'), 404 | ['pineapple', 'pork loin'], 405 | 'should have fully loaded the associated items' 406 | ); 407 | }) 408 | .finally(done); 409 | }); 410 | 411 | test('create a new record', function (assert) { 412 | assert.expect(2); 413 | 414 | var done = assert.async(); 415 | Promise.resolve() 416 | .then(() => { 417 | var newSoup = this.store().createRecord('taco-soup', { 418 | id: 'E', 419 | flavor: 'balsamic', 420 | }); 421 | return newSoup.save(); 422 | }) 423 | .then(() => { 424 | return this.db().get('tacoSoup_2_E'); 425 | }) 426 | .then((newDoc) => { 427 | assert.strictEqual( 428 | newDoc.data.flavor, 429 | 'balsamic', 430 | 'should have saved the attribute' 431 | ); 432 | 433 | var recordInStore = this.store().peekRecord('tacoSoup', 'E'); 434 | assert.strictEqual( 435 | newDoc._rev, 436 | recordInStore.get('rev'), 437 | 'should have associated the ember-data record with the rev for the new record' 438 | ); 439 | }) 440 | .finally(done); 441 | }); 442 | 443 | test('creating an associated record stores a reference to it in the parent', function (assert) { 444 | assert.expect(1); 445 | 446 | var done = assert.async(); 447 | Promise.resolve() 448 | .then(() => { 449 | var s = { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }; 450 | if (savingHasMany()) { 451 | s.data.ingredients = []; 452 | } 453 | return this.db().bulkDocs([s]); 454 | }) 455 | .then(() => { 456 | return this.store().findRecord('taco-soup', 'C'); 457 | }) 458 | .then((tacoSoup) => { 459 | var newIngredient = this.store().createRecord('food-item', { 460 | name: 'pineapple', 461 | soup: tacoSoup, 462 | }); 463 | 464 | //tacoSoup.save() actually not needed in !savingHasmany mode, but should still work 465 | return newIngredient 466 | .save() 467 | .then(() => (savingHasMany() ? tacoSoup.save() : tacoSoup)); 468 | }) 469 | .then(() => { 470 | run(() => this.store().unloadAll()); 471 | return this.store().findRecord('taco-soup', 'C'); 472 | }) 473 | .then((tacoSoup) => { 474 | return tacoSoup.get('ingredients'); 475 | }) 476 | .then((foundIngredients) => { 477 | assert.deepEqual( 478 | foundIngredients.mapBy('name'), 479 | ['pineapple'], 480 | 'should have fully loaded the associated items' 481 | ); 482 | }) 483 | .finally(done); 484 | }); 485 | 486 | // This test fails due to a bug in ember data 487 | // (https://github.com/emberjs/data/issues/3736) 488 | // starting with ED v2.0.0-beta.1. It works again with ED v2.1.0. 489 | // if (!DS.VERSION.match(/^2\.0/)) { 490 | test('update an existing record', function (assert) { 491 | assert.expect(2); 492 | 493 | var done = assert.async(); 494 | Promise.resolve() 495 | .then(() => { 496 | return this.db().bulkDocs([ 497 | { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, 498 | { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, 499 | ]); 500 | }) 501 | .then(() => { 502 | return this.store().find('taco-soup', 'C'); 503 | }) 504 | .then((found) => { 505 | found.set('flavor', 'pork'); 506 | return found.save(); 507 | }) 508 | .then(() => { 509 | return this.db().get('tacoSoup_2_C'); 510 | }) 511 | .then((updatedDoc) => { 512 | assert.strictEqual( 513 | updatedDoc.data.flavor, 514 | 'pork', 515 | 'should have updated the attribute' 516 | ); 517 | 518 | var recordInStore = this.store().peekRecord('tacoSoup', 'C'); 519 | assert.strictEqual( 520 | updatedDoc._rev, 521 | recordInStore.get('rev'), 522 | 'should have associated the ember-data record with the updated rev' 523 | ); 524 | }) 525 | .finally(done); 526 | }); 527 | // } 528 | 529 | test('delete an existing record', function (assert) { 530 | assert.expect(1); 531 | 532 | var done = assert.async(); 533 | Promise.resolve() 534 | .then(() => { 535 | return this.db().bulkDocs([ 536 | { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, 537 | { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, 538 | ]); 539 | }) 540 | .then(() => { 541 | return this.store().find('taco-soup', 'C'); 542 | }) 543 | .then((found) => { 544 | return found.destroyRecord(); 545 | }) 546 | .then(() => { 547 | return this.db().get('tacoSoup_2_C'); 548 | }) 549 | .then( 550 | (doc) => { 551 | assert.notOk(doc, 'document should no longer exist'); 552 | }, 553 | (result) => { 554 | assert.strictEqual( 555 | result.status, 556 | 404, 557 | 'document should no longer exist' 558 | ); 559 | } 560 | ) 561 | .finally(done); 562 | }); 563 | }; 564 | 565 | let asyncTests = function () { 566 | test('eventually consistency - success', function (assert) { 567 | assert.expect(1); 568 | assert.timeout(5000); 569 | var done = assert.async(); 570 | Promise.resolve() 571 | .then(() => { 572 | return this.db().bulkDocs([ 573 | { _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' } }, 574 | //{_id: 'tacoSoup_2_C', data: { flavor: 'test' } } 575 | ]); 576 | }) 577 | .then(() => this.store().findRecord('food-item', 'X')) 578 | .then((foodItem) => { 579 | let result = [ 580 | foodItem 581 | .get('soup') 582 | .then((soup) => assert.strictEqual(soup.id, 'C')), 583 | 584 | promiseToRunLater(0).then(() => { 585 | return this.db().bulkDocs([ 586 | { _id: 'tacoSoup_2_C', data: { flavor: 'test' } }, 587 | ]); 588 | }), 589 | ]; 590 | 591 | return all(result); 592 | }) 593 | .finally(done); 594 | }); 595 | 596 | test('eventually consistency - deleted', function (assert) { 597 | assert.expect(1); 598 | assert.timeout(5000); 599 | var done = assert.async(); 600 | Promise.resolve() 601 | .then(() => { 602 | return this.db().bulkDocs([ 603 | { _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' } }, 604 | //{_id: 'tacoSoup_2_C', data: { flavor: 'test' } } 605 | ]); 606 | }) 607 | .then(() => this.store().findRecord('food-item', 'X')) 608 | .then((foodItem) => { 609 | let result = [ 610 | foodItem 611 | .get('soup') 612 | .then((soup) => assert.strictEqual(soup, null, 'isDeleted')) 613 | .catch(() => assert.ok(true, 'isDeleted')), 614 | 615 | promiseToRunLater(100).then(() => 616 | this.db().bulkDocs([{ _id: 'tacoSoup_2_C', _deleted: true }]) 617 | ), 618 | ]; 619 | 620 | return all(result); 621 | }) 622 | .finally(done); 623 | }); 624 | 625 | test('_init should work', function (assert) { 626 | let db = this.db(); 627 | 628 | assert.strictEqual(db.rel, undefined, 'should start without schema'); 629 | 630 | let promises = []; 631 | 632 | let adapter = this.adapter(); 633 | promises.push( 634 | adapter._init(this.store(), this.store().modelFor('taco-soup')) 635 | ); 636 | 637 | //this tests _init synchronously by design, as re-entry and infitinite loop detection works this way 638 | assert.notEqual(db.rel, undefined, '_init should set schema'); 639 | assert.strictEqual( 640 | this.adapter()._schema.length, 641 | 2, 642 | 'should have set all relationships on the schema' 643 | ); 644 | 645 | promises.push( 646 | adapter._init(this.store(), this.store().modelFor('taco-soup')) 647 | ); 648 | 649 | return all(promises); 650 | }); 651 | 652 | //TODO: only do this for async or dontsavehasmany? 653 | test('delete cascade null', function (assert) { 654 | assert.timeout(5000); 655 | assert.expect(2); 656 | 657 | var done = assert.async(); 658 | Promise.resolve() 659 | .then(() => { 660 | return this.db().bulkDocs(getDocsForRelations()); 661 | }) 662 | // .then(() => this.store().findRecord('food-item', 'Z'))//prime ember-data store with Z 663 | // .then(found => found.get('soup'))//prime belongsTo 664 | .then(() => this.store().findRecord('taco-soup', 'D')) 665 | .then((found) => { 666 | return found.destroyRecord(); 667 | }) 668 | .then(() => { 669 | run(() => this.store().unloadAll()); // normally this would be done by onChange listener 670 | return this.store().findRecord('food-item', 'Z'); //Z should be updated now 671 | }) 672 | .then((found) => { 673 | return Promise.resolve(found.get('soup')) 674 | .catch(() => null) 675 | .then((soup) => { 676 | assert.ok( 677 | !found.belongsTo || found.belongsTo('soup').value() === null, 678 | 'should set value of belongsTo to null' 679 | ); 680 | return soup; 681 | }); 682 | }) 683 | .then((soup) => { 684 | assert.ok( 685 | soup === null, 686 | 'deleted soup should have cascaded to a null value for the belongsTo' 687 | ); 688 | }) 689 | .finally(done); 690 | }); 691 | 692 | test('remote delete removes belongsTo relationship', function (assert) { 693 | assert.timeout(5000); 694 | assert.expect(2); 695 | 696 | var done = assert.async(); 697 | Promise.resolve() 698 | .then(() => { 699 | return this.db().bulkDocs(getDocsForRelations()); 700 | }) 701 | .then(() => this.store().findRecord('food-item', 'Z')) //prime ember-data store with Z 702 | .then((found) => found.get('soup')) //prime belongsTo 703 | .then((found) => { 704 | let id = 'tacoSoup_2_' + found.id; 705 | let promise = this.adapter().waitForChangeWithID(id); 706 | 707 | this.db().remove(id, found.get('rev')); 708 | 709 | return promise; 710 | }) 711 | .then(() => { 712 | return this.store().findRecord('food-item', 'Z'); //Z should be updated now 713 | }) 714 | .then((found) => { 715 | return Promise.resolve(found.get('soup')) 716 | .catch(() => null) 717 | .then((soup) => { 718 | assert.ok( 719 | !found.belongsTo || found.belongsTo('soup').value() === null, 720 | 'should set value of belongsTo to null' 721 | ); 722 | return soup; 723 | }); 724 | }) 725 | .then((soup) => { 726 | assert.ok( 727 | soup === null, 728 | 'deleted soup should have cascaded to a null value for the belongsTo' 729 | ); 730 | }) 731 | .finally(done); 732 | }); 733 | 734 | test('remote delete removes hasMany relationship', function (assert) { 735 | assert.timeout(5000); 736 | assert.expect(3); 737 | 738 | let liveIngredients = null; 739 | 740 | var done = assert.async(); 741 | Promise.resolve() 742 | .then(() => { 743 | return this.db().bulkDocs(getDocsForRelations()); 744 | }) 745 | .then(() => this.store().findRecord('taco-soup', 'C')) //prime ember-data store with C 746 | .then((found) => found.get('ingredients')) //prime hasMany 747 | .then((ingredients) => { 748 | liveIngredients = ingredients; //save for later 749 | 750 | assert.strictEqual( 751 | ingredients.length, 752 | 2, 753 | 'should be 2 food items initially' 754 | ); 755 | 756 | let itemToDelete = ingredients.toArray()[0]; 757 | let id = 'foodItem_2_' + itemToDelete.id; 758 | let promise = this.adapter().waitForChangeWithID(id); 759 | 760 | this.db().remove(id, itemToDelete.get('rev')); 761 | 762 | return promise; 763 | }) 764 | .then(() => { 765 | return this.store().findRecord('taco-soup', 'C'); //get updated soup.ingredients 766 | }) 767 | .then((found) => found.get('ingredients')) 768 | .then((ingredients) => { 769 | assert.strictEqual( 770 | ingredients.length, 771 | 1, 772 | '1 food item should be removed from the relationship' 773 | ); 774 | assert.strictEqual( 775 | liveIngredients.length, 776 | 1, 777 | '1 food item should be removed from the live relationship' 778 | ); 779 | }) 780 | .finally(done); 781 | }); 782 | 783 | module( 784 | 'not eventually consistent', 785 | { 786 | beforeEach: function () { 787 | config.emberPouch.eventuallyConsistent = false; 788 | }, 789 | afterEach: function () { 790 | config.emberPouch.eventuallyConsistent = true; 791 | }, 792 | }, 793 | function () { 794 | test('not found', function (assert) { 795 | assert.expect(2); 796 | assert.false( 797 | config.emberPouch.eventuallyConsistent, 798 | 'eventuallyConsistent is false' 799 | ); 800 | let done = assert.async(); 801 | 802 | Promise.resolve().then(() => 803 | this.store() 804 | .findRecord('food-item', 'non-existent') 805 | .then(() => assert.ok(false)) 806 | .catch(() => { 807 | assert.ok(true, 'item is not found'); 808 | done(); 809 | }) 810 | ); 811 | }); 812 | } 813 | ); 814 | }; 815 | 816 | let syncAsync = function () { 817 | module( 818 | 'async', 819 | { 820 | beforeEach: function () { 821 | config.emberPouch.async = true; 822 | }, 823 | }, 824 | () => { 825 | allTests(); 826 | asyncTests(); 827 | } 828 | ); 829 | module( 830 | 'sync', 831 | { 832 | beforeEach: function () { 833 | config.emberPouch.async = false; 834 | }, 835 | }, 836 | allTests 837 | ); 838 | }; 839 | 840 | module( 841 | 'dont save hasMany', 842 | { 843 | beforeEach: function () { 844 | config.emberPouch.saveHasMany = false; 845 | }, 846 | }, 847 | syncAsync 848 | ); 849 | 850 | module( 851 | 'save hasMany', 852 | { 853 | beforeEach: function () { 854 | config.emberPouch.saveHasMany = true; 855 | }, 856 | }, 857 | syncAsync 858 | ); 859 | }); 860 | -------------------------------------------------------------------------------- /tests/integration/adapters/pouch-default-change-watcher-test.js: -------------------------------------------------------------------------------- 1 | import { later } from '@ember/runloop'; 2 | import { Promise, resolve } from 'rsvp'; 3 | import { module, test } from 'qunit'; 4 | import moduleForIntegration from '../../helpers/module-for-pouch-acceptance'; 5 | import { setupTest } from 'ember-qunit'; 6 | 7 | /* 8 | * Tests for the default automatic change listener. 9 | */ 10 | 11 | function promiseToRunLater(callback, timeout) { 12 | return new Promise((resolve) => { 13 | later(() => { 14 | callback(); 15 | resolve(); 16 | }, timeout); 17 | }); 18 | } 19 | 20 | module('Integration | Adapter | Default Change Watcher', function (hooks) { 21 | setupTest(hooks); 22 | moduleForIntegration(hooks); 23 | 24 | hooks.beforeEach(function (assert) { 25 | var done = assert.async(); 26 | 27 | Promise.resolve() 28 | .then(() => { 29 | return this.db().bulkDocs([ 30 | { 31 | _id: 'tacoSoup_2_A', 32 | data: { flavor: 'al pastor', ingredients: ['X', 'Y'] }, 33 | }, 34 | { 35 | _id: 'tacoSoup_2_B', 36 | data: { flavor: 'black bean', ingredients: ['Z'] }, 37 | }, 38 | { _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'A' } }, 39 | { _id: 'foodItem_2_Y', data: { name: 'pork loin', soup: 'A' } }, 40 | { _id: 'foodItem_2_Z', data: { name: 'black beans', soup: 'B' } }, 41 | ]); 42 | }) 43 | .finally(done); 44 | }); 45 | 46 | test('a loaded instance automatically reflects directly-made database changes', function (assert) { 47 | assert.expect(2); 48 | var done = assert.async(); 49 | 50 | resolve() 51 | .then(() => { 52 | return this.store().find('taco-soup', 'B'); 53 | }) 54 | .then((soupB) => { 55 | assert.strictEqual( 56 | soupB.get('flavor'), 57 | 'black bean', 58 | 'the loaded instance should reflect the initial test data' 59 | ); 60 | 61 | return this.db().get('tacoSoup_2_B'); 62 | }) 63 | .then((soupBRecord) => { 64 | soupBRecord.data.flavor = 'carnitas'; 65 | return this.db().put(soupBRecord); 66 | }) 67 | .then(() => { 68 | return promiseToRunLater(() => { 69 | var alreadyLoadedSoupB = this.store().peekRecord('taco-soup', 'B'); 70 | assert.strictEqual( 71 | alreadyLoadedSoupB.get('flavor'), 72 | 'carnitas', 73 | 'the loaded instance should automatically reflect the change in the database' 74 | ); 75 | }, 100); 76 | }) 77 | .finally(done); 78 | }); 79 | 80 | test('a record that is not loaded stays not loaded when it is changed', function (assert) { 81 | assert.expect(2); 82 | var done = assert.async(); 83 | 84 | resolve() 85 | .then(() => { 86 | assert.strictEqual( 87 | this.store().peekRecord('taco-soup', 'A'), 88 | null, 89 | 'test setup: record should not be loaded already' 90 | ); 91 | 92 | return this.db().get('tacoSoup_2_A'); 93 | }) 94 | .then((soupARecord) => { 95 | soupARecord.data.flavor = 'barbacoa'; 96 | return this.db().put(soupARecord); 97 | }) 98 | .then(() => { 99 | return promiseToRunLater(() => { 100 | assert.strictEqual( 101 | this.store().peekRecord('taco-soup', 'A'), 102 | null, 103 | 'the corresponding instance should still not be loaded' 104 | ); 105 | }, 15); 106 | }) 107 | .finally(done); 108 | }); 109 | 110 | test('a new record is not automatically loaded', function (assert) { 111 | assert.expect(2); 112 | var done = assert.async(); 113 | 114 | resolve() 115 | .then(() => { 116 | assert.strictEqual( 117 | this.store().peekRecord('taco-soup', 'C'), 118 | null, 119 | 'test setup: record should not be loaded already' 120 | ); 121 | 122 | return this.db().put({ 123 | _id: 'tacoSoup_2_C', 124 | data: { flavor: 'sofritas' }, 125 | }); 126 | }) 127 | .then(() => { 128 | return promiseToRunLater(() => { 129 | assert.strictEqual( 130 | this.store().peekRecord('taco-soup', 'C'), 131 | null, 132 | 'the corresponding instance should still not be loaded' 133 | ); 134 | }, 15); 135 | }) 136 | .finally(done); 137 | }); 138 | 139 | test('a deleted record is automatically marked deleted', function (assert) { 140 | assert.expect(2); 141 | var done = assert.async(); 142 | 143 | let initialRecord = null; 144 | 145 | resolve() 146 | .then(() => { 147 | return this.store().find('taco-soup', 'B'); 148 | }) 149 | .then((soupB) => { 150 | initialRecord = soupB; 151 | assert.strictEqual( 152 | soupB.get('flavor'), 153 | 'black bean', 154 | 'the loaded instance should reflect the initial test data' 155 | ); 156 | return this.db().get('tacoSoup_2_B'); 157 | }) 158 | .then((soupBRecord) => { 159 | return this.db().remove(soupBRecord); 160 | }) 161 | .then(() => { 162 | return promiseToRunLater(() => { 163 | assert.ok( 164 | initialRecord.get('isDeleted'), 165 | 'the corresponding instance should now be deleted ' 166 | ); 167 | }, 100); 168 | }) 169 | .finally(done); 170 | }); 171 | 172 | test('a change to a record with a non-relational-pouch ID does not cause an error', function (assert) { 173 | assert.expect(0); 174 | var done = assert.async(); 175 | 176 | resolve() 177 | .then(() => { 178 | // do some op to cause relational-pouch to be initialized 179 | return this.store().find('taco-soup', 'B'); 180 | }) 181 | .then(() => { 182 | return this.db().put({ 183 | _id: '_design/ingredient-use', 184 | }); 185 | }) 186 | .finally(done); 187 | }); 188 | 189 | test('a change to a record of an unknown type does not cause an error', function (assert) { 190 | assert.expect(0); 191 | var done = assert.async(); 192 | 193 | resolve() 194 | .then(() => { 195 | // do some op to cause relational-pouch to be initialized 196 | return this.store().find('taco-soup', 'B'); 197 | }) 198 | .then(() => { 199 | return this.db().put({ 200 | _id: 'burritoShake_2_X', 201 | data: { consistency: 'chunky' }, 202 | }); 203 | }) 204 | .finally(done); 205 | }); 206 | }); 207 | 208 | module( 209 | 'Integration | Adapter | With unloadedDocumentChanged implementation to load new docs into store', 210 | function (hooks) { 211 | setupTest(hooks); 212 | moduleForIntegration(hooks); 213 | 214 | hooks.beforeEach(function (assert) { 215 | var done = assert.async(); 216 | this.adapter = function adapter() { 217 | return this.store().adapterFor('taco-salad'); 218 | }; 219 | this.db = function db() { 220 | return this.adapter().get('db'); 221 | }; 222 | 223 | Promise.resolve() 224 | .then(() => { 225 | return this.db().bulkDocs([ 226 | { 227 | _id: 'tacoSalad_2_A', 228 | data: { flavor: 'al pastor', ingredients: ['X', 'Y'] }, 229 | }, 230 | { 231 | _id: 'tacoSalad_2_B', 232 | data: { flavor: 'black bean', ingredients: ['Z'] }, 233 | }, 234 | { _id: 'foodItem_2_X', data: { name: 'pineapple' } }, 235 | { _id: 'foodItem_2_Y', data: { name: 'pork loin' } }, 236 | { _id: 'foodItem_2_Z', data: { name: 'black beans' } }, 237 | ]); 238 | }) 239 | .finally(done); 240 | }); 241 | 242 | test('a new record is automatically loaded', function (assert) { 243 | assert.expect(4); 244 | var done = assert.async(); 245 | 246 | resolve() 247 | .then(() => { 248 | return this.store().find('taco-salad', 'B'); 249 | }) 250 | .then((soupB) => { 251 | assert.strictEqual( 252 | soupB.get('flavor'), 253 | 'black bean', 254 | 'the loaded instance should reflect the initial test data' 255 | ); 256 | }) 257 | .then(() => { 258 | assert.strictEqual( 259 | this.store().peekRecord('taco-salad', 'C'), 260 | null, 261 | 'test setup: record should not be loaded already' 262 | ); 263 | 264 | return this.db().put({ 265 | _id: 'tacoSalad_2_C', 266 | data: { flavor: 'sofritas' }, 267 | }); 268 | }) 269 | .then(() => { 270 | return promiseToRunLater(() => { 271 | var alreadyLoadedSaladC = this.store().peekRecord( 272 | 'taco-salad', 273 | 'C' 274 | ); 275 | assert.ok( 276 | alreadyLoadedSaladC, 277 | 'the corresponding instance should now be loaded' 278 | ); 279 | //if (alreadyLoadedSaladC) { 280 | assert.strictEqual( 281 | alreadyLoadedSaladC.get('flavor'), 282 | 'sofritas', 283 | 'the corresponding instance should now be loaded with the right data' 284 | ); 285 | //} 286 | }, 15); 287 | }) 288 | .finally(done); 289 | }); 290 | } 291 | ); 292 | -------------------------------------------------------------------------------- /tests/integration/serializers/pouch-test.js: -------------------------------------------------------------------------------- 1 | import { Promise } from 'rsvp'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | import moduleForIntegration from '../../helpers/module-for-pouch-acceptance'; 6 | 7 | /* 8 | * Tests attachments behavior for an app using the ember-pouch serializer. 9 | */ 10 | 11 | module('Integration | Serializer | Attachments', function (hooks) { 12 | setupTest(hooks); 13 | moduleForIntegration(hooks); 14 | 15 | let id = 'E'; 16 | let coverImage = { 17 | name: 'cover.jpg', 18 | content_type: 'image/jpeg', 19 | data: window.btoa('cover.jpg'), 20 | length: 9, 21 | }; 22 | let photo1 = { 23 | name: 'photo-1.jpg', 24 | content_type: 'image/jpeg', 25 | data: window.btoa('photo-1.jpg'), 26 | }; 27 | let photo2 = { 28 | name: 'photo-2.jpg', 29 | content_type: 'image/jpeg', 30 | data: window.btoa('photo-2.jpg'), 31 | }; 32 | 33 | test('puts attachments into the `attachments` property when saving', function (assert) { 34 | assert.expect(11); 35 | 36 | var done = assert.async(); 37 | Promise.resolve() 38 | .then(() => { 39 | var newRecipe = this.store().createRecord('taco-recipe', { 40 | id, 41 | coverImage: coverImage, 42 | photos: [photo1, photo2], 43 | }); 44 | return newRecipe.save(); 45 | }) 46 | .then(() => { 47 | return this.db().get('tacoRecipe_2_E'); 48 | }) 49 | .then((newDoc) => { 50 | function checkAttachment(attachments, fileName, value, message) { 51 | delete attachments[fileName].revpos; 52 | assert.deepEqual(attachments[fileName], value, message); 53 | } 54 | checkAttachment( 55 | newDoc._attachments, 56 | 'cover.jpg', 57 | { 58 | digest: 'md5-SxxZx3KOKxy2X2yyCq9c+Q==', 59 | content_type: 'image/jpeg', 60 | stub: true, 61 | length: 9, 62 | }, 63 | 'attachments are placed into the _attachments property of the doc' 64 | ); 65 | assert.deepEqual( 66 | Object.keys(newDoc._attachments).sort(), 67 | [coverImage.name, photo1.name, photo2.name].sort(), 68 | 'all attachments are included in the _attachments property of the doc' 69 | ); 70 | assert.true( 71 | 'cover_image' in newDoc.data, 72 | 'respects the mapping provided by the serializer `attrs`' 73 | ); 74 | assert.deepEqual( 75 | newDoc.data.cover_image, 76 | { 77 | 'cover.jpg': { 78 | length: 9, 79 | }, 80 | }, 81 | 'the attribute contains the file name' 82 | ); 83 | assert.strictEqual( 84 | newDoc.data.cover_image['cover.jpg'].length, 85 | 9, 86 | 'the attribute contains the length to avoid empty length when File objects are ' + 87 | 'saved and have not been reloaded' 88 | ); 89 | assert.deepEqual(newDoc.data.photo_gallery, { 90 | 'photo-1.jpg': {}, 91 | 'photo-2.jpg': {}, 92 | }); 93 | 94 | var recordInStore = this.store().peekRecord('tacoRecipe', 'E'); 95 | let coverAttr = recordInStore.get('coverImage'); 96 | assert.strictEqual(coverAttr.get('name'), coverImage.name); 97 | assert.strictEqual(coverAttr.get('data'), coverImage.data); 98 | 99 | let photosAttr = recordInStore.get('photos'); 100 | assert.strictEqual(photosAttr.length, 2, '2 photos'); 101 | assert.strictEqual(photosAttr[0].get('name'), photo1.name); 102 | assert.strictEqual(photosAttr[0].get('data'), photo1.data); 103 | 104 | done(); 105 | }) 106 | .catch((error) => { 107 | assert.ok(false, 'error in test:' + error); 108 | done(); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/transforms/attachments-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | 3 | import { module, test } from 'qunit'; 4 | 5 | import { setupTest } from 'ember-qunit'; 6 | 7 | let testSerializedData = { 8 | 'hello.txt': { 9 | content_type: 'text/plain', 10 | data: 'aGVsbG8gd29ybGQ=', 11 | digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', 12 | // CouchDB doesn't add 'length' 13 | }, 14 | 'stub.txt': { 15 | stub: true, 16 | content_type: 'text/plain', 17 | digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', 18 | length: 11, 19 | }, 20 | }; 21 | 22 | let testDeserializedData = [ 23 | EmberObject.create({ 24 | name: 'hello.txt', 25 | content_type: 'text/plain', 26 | data: 'aGVsbG8gd29ybGQ=', 27 | digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', 28 | }), 29 | EmberObject.create({ 30 | name: 'stub.txt', 31 | content_type: 'text/plain', 32 | stub: true, 33 | digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', 34 | length: 11, 35 | }), 36 | ]; 37 | 38 | module('Unit | Transform | attachments', function (hooks) { 39 | setupTest(hooks); 40 | 41 | test('it serializes an attachment', function (assert) { 42 | let transform = this.owner.lookup('transform:attachments'); 43 | assert.strictEqual(transform.serialize(null), null); 44 | assert.strictEqual(transform.serialize(undefined), null); 45 | assert.deepEqual(transform.serialize([]), {}); 46 | 47 | let serializedData = transform.serialize(testDeserializedData); 48 | 49 | let hello = testDeserializedData[0].get('name'); 50 | assert.strictEqual(hello, 'hello.txt'); 51 | assert.strictEqual( 52 | serializedData[hello].content_type, 53 | testSerializedData[hello].content_type 54 | ); 55 | assert.strictEqual( 56 | serializedData[hello].data, 57 | testSerializedData[hello].data 58 | ); 59 | 60 | let stub = testDeserializedData[1].get('name'); 61 | assert.strictEqual(stub, 'stub.txt'); 62 | assert.strictEqual( 63 | serializedData[stub].content_type, 64 | testSerializedData[stub].content_type 65 | ); 66 | assert.true(serializedData[stub].stub); 67 | }); 68 | 69 | test('it deserializes an attachment', function (assert) { 70 | let transform = this.owner.lookup('transform:attachments'); 71 | assert.deepEqual(transform.deserialize(null), []); 72 | assert.deepEqual(transform.deserialize(undefined), []); 73 | 74 | let deserializedData = transform.deserialize(testSerializedData); 75 | 76 | assert.strictEqual( 77 | deserializedData[0].get('name'), 78 | testDeserializedData[0].get('name') 79 | ); 80 | assert.strictEqual( 81 | deserializedData[0].get('content_type'), 82 | testDeserializedData[0].get('content_type') 83 | ); 84 | assert.strictEqual( 85 | deserializedData[0].get('data'), 86 | testDeserializedData[0].get('data') 87 | ); 88 | assert.strictEqual( 89 | deserializedData[0].get('digest'), 90 | testDeserializedData[0].get('digest') 91 | ); 92 | 93 | assert.strictEqual( 94 | deserializedData[1].get('name'), 95 | testDeserializedData[1].get('name') 96 | ); 97 | assert.strictEqual( 98 | deserializedData[1].get('content_type'), 99 | testDeserializedData[1].get('content_type') 100 | ); 101 | assert.true(deserializedData[1].get('stub')); 102 | assert.strictEqual( 103 | deserializedData[1].get('digest'), 104 | testDeserializedData[1].get('digest') 105 | ); 106 | assert.strictEqual( 107 | deserializedData[1].get('length'), 108 | testDeserializedData[1].get('length') 109 | ); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouchdb-community/ember-pouch/0b7ebc6ee2dc45534acb38e1b5afc29bb11b125e/vendor/.gitkeep -------------------------------------------------------------------------------- /vendor/pouchdb/shims.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function vendorModule() { 3 | 'use strict'; 4 | 5 | return { 'default': self['PouchDB'] }; 6 | } 7 | 8 | define('pouchdb', [], vendorModule); 9 | })(); 10 | --------------------------------------------------------------------------------