diff --git a/.eslintignore b/.eslintignore index 72df373..701947e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,6 +13,8 @@ # misc /coverage/ !.* +.*/ +.eslintcache # ember-try /.node_modules.ember-try/ diff --git a/.eslintrc.js b/.eslintrc.js index c4daa3d..798486e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = { root: true, parser: 'babel-eslint', @@ -5,53 +7,69 @@ module.exports = { ecmaVersion: 2018, sourceType: 'module', ecmaFeatures: { - legacyDecorators: true - } + legacyDecorators: true, + }, }, + plugins: ['ember'], extends: [ 'eslint:recommended', - 'plugin:ember/recommended' + 'plugin:ember/recommended', + 'plugin:prettier/recommended', ], env: { - browser: true + browser: true, }, rules: { - 'ember/no-jquery': 'error' + 'qunit/resolve-async': 0, + 'ember/no-test-module-for': 0, + 'ember/no-classic-classes': 0, }, overrides: [ // node files { files: [ - '.eslintrc.js', - '.template-lintrc.js', - 'ember-cli-build.js', - 'index.js', - 'testem.js', - 'blueprints/*/index.js', - 'config/**/*.js', - 'tests/dummy/config/**/*.js' - ], - excludedFiles: [ - 'addon/**', - 'addon-test-support/**', - 'app/**', - 'tests/dummy/app/**' + './.eslintrc.js', + './.prettierrc.js', + './.template-lintrc.js', + './ember-cli-build.js', + './index.js', + './testem.js', + './blueprints/*/index.js', + './config/**/*.js', + './tests/dummy/config/**/*.js', ], parserOptions: { - sourceType: 'script' + sourceType: 'script', }, env: { browser: false, - node: true + node: true, }, plugins: ['node'], - rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { - // add your custom rules and overrides for node files here + extends: ['plugin:node/recommended'], + rules: Object.assign( + {}, + require('eslint-plugin-node').configs.recommended.rules, + { + // add your custom rules and overrides for node files here - // this can be removed once the following is fixed - // https://github.com/mysticatea/eslint-plugin-node/issues/77 - 'node/no-unpublished-require': 'off' - }) - } - ] + // this can be removed once the following is fixed + // https://github.com/mysticatea/eslint-plugin-node/issues/77 + 'node/no-unpublished-require': 'off', + } + ), + }, + { + // Test files: + files: ['tests/**/*-test.{js,ts}'], + extends: ['plugin:qunit/recommended'], + rules: { + 'qunit/resolve-async': 0, + 'qunit/no-assert-logical-expression': 0, + 'qunit/no-ok-equality': 0, + 'qunit/no-negated-ok': 0, + // 'qunit/require-expect': 0, + }, + }, + ], }; diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..636818f --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,93 @@ +# 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 +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: "Tests" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install Nod + uses: actions/setup-node@v3 + with: + node-version: 12.x + # cache: npm + - name: Install dependencies + run: | + if [ -e yarn.lock ]; then + yarn install --frozen-lockfile + elif [ -e package-lock.json ]; then + npm ci + else + npm i + fi + - name: Lint + run: npm run lint + - name: Run Tests + run: npm run test:ember + + floating: + name: "Floating Dependencies" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 12.x + # cache: npm + - name: Install Dependencies + run: npm install --no-shrinkwrap + - name: Run Tests + run: npm run test:ember + + try-scenarios: + name: ${{ matrix.try-scenario }} + runs-on: ubuntu-latest + needs: "test" + + strategy: + fail-fast: false + matrix: + try-scenario: + - ember-lts-3.16 + - ember-lts-3.24 + - ember-lts-3.28 + - ember-release + - ember-beta + - ember-canary + - ember-classic + - embroider-safe + - embroider-optimized + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 12.x + # cache: npm + - name: Install dependencies + run: | + if [ -e yarn.lock ]; then + yarn install --frozen-lockfile + elif [ -e package-lock.json ]; then + npm ci + else + npm i + fi + - name: Run Tests + run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c40a1b2..5de79b0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /.env* /.pnp* /.sass-cache +/.eslintcache /connect.lock /coverage/ /libpeerconnection.log @@ -19,6 +20,9 @@ /testem.log /yarn-error.log +/package-lock.json +/yarn.lock + # ember-try /.node_modules.ember-try/ /bower.json.ember-try diff --git a/.npmignore b/.npmignore index bd09adf..f30effe 100644 --- a/.npmignore +++ b/.npmignore @@ -10,10 +10,13 @@ /.editorconfig /.ember-cli /.env* +/.eslintcache /.eslintignore /.eslintrc.js /.git/ /.gitignore +/.prettierignore +/.prettierrc.js /.template-lintrc.js /.travis.yml /.watchmanconfig @@ -23,6 +26,7 @@ /ember-cli-build.js /testem.js /tests/ +/yarn-error.log /yarn.lock .gitkeep diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9221655 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,21 @@ +# unconventional js +/blueprints/*/files/ +/vendor/ + +# compiled output +/dist/ +/tmp/ + +# dependencies +/bower_components/ +/node_modules/ + +# misc +/coverage/ +!.* +.eslintcache + +# ember-try +/.node_modules.ember-try/ +/bower.json.ember-try +/package.json.ember-try diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..09f6af7 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = { + singleQuote: true, + overrides: [ + { + files: '*.hbs', + options: { + singleQuote: false, + }, + }, + ], +}; diff --git a/.template-lintrc.js b/.template-lintrc.js index f387370..f35f61c 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - extends: 'octane' + extends: 'recommended', }; diff --git a/.travis.yml b/.travis.yml index 2799309..e655f07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,20 +3,19 @@ language: node_js node_js: # we recommend testing addons with the same minimum supported node version as Ember CLI # so that your addon works for all apps - - "10" + - '12' -dist: trusty +dist: xenial addons: chrome: stable cache: - directories: - - $HOME/.npm - + yarn: true + env: global: - # See https://git.io/vdao3 for details. + # See https://github.com/ember-cli/ember-cli/blob/master/docs/build-concurrency.md for details. - JOBS=1 branches: @@ -25,49 +24,41 @@ branches: # npm version tags - /^v\d+\.\d+\.\d+/ -before_install: - - npm --version - jobs: - fast_finish: true + fast_finish: false allow_failures: + - env: EMBER_TRY_SCENARIO=ember-beta - env: EMBER_TRY_SCENARIO=ember-canary include: # runs linting and tests with current locked deps - - stage: "Tests" - name: "Tests" + # runs linting and tests with current locked deps + - stage: 'Tests' + name: 'Tests' script: - - npm run lint:hbs - - npm run lint:js - - npm test + - yarn lint + - yarn test:ember - - stage: "Additional Tests" - name: "Floating Dependencies" + - stage: 'Additional Tests' + name: 'Floating Dependencies' install: - - npm install --no-package-lock + - yarn install --no-lockfile --non-interactive script: - - npm test + - yarn test:ember # we recommend new addons test the current and previous LTS # as well as latest stable release (bonus points to beta/canary) - - env: EMBER_TRY_SCENARIO=ember-2.4-stack - - env: EMBER_TRY_SCENARIO=ember-lts-2.8 - - env: EMBER_TRY_SCENARIO=ember-lts-2.12 - - env: EMBER_TRY_SCENARIO=ember-lts-2.16 - - env: EMBER_TRY_SCENARIO=ember-lts-2.18 - - env: EMBER_TRY_SCENARIO=ember-lts-3.4 - - env: EMBER_TRY_SCENARIO=ember-lts-3.8 - - env: EMBER_TRY_SCENARIO=ember-lts-3.12 - env: EMBER_TRY_SCENARIO=ember-lts-3.16 + - env: EMBER_TRY_SCENARIO=ember-lts-3.24 + - env: EMBER_TRY_SCENARIO=ember-lts-3.28 - env: EMBER_TRY_SCENARIO=ember-release - env: EMBER_TRY_SCENARIO=ember-beta - env: EMBER_TRY_SCENARIO=ember-canary - - env: EMBER_TRY_SCENARIO=ember-default-with-jquery - env: EMBER_TRY_SCENARIO=ember-classic +before_install: + - curl -o- -L https://yarnpkg.com/install.sh | bash + - export PATH=$HOME/.yarn/bin:$PATH + script: - - npm run license - # Usually, it's ok to finish the test scenario without reverting - # to the addon's original dependency state, skipping "cleanup". - - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO --skip-cleanup + - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d14b95..6bd10bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,25 +2,24 @@ ## Installation -* `git clone ` -* `cd my-addon` -* `npm install` +- `git clone ` +- `cd my-addon` +- `npm install` ## Linting -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` +- `npm run lint` +- `npm run lint:fix` ## Running tests -* `ember test` – Runs the test suite on the current Ember version -* `ember test --server` – Runs the test suite in "watch mode" -* `ember try:each` – Runs the test suite against multiple Ember versions +- `ember test` – Runs the test suite on the current Ember version +- `ember test --server` – Runs the test suite in "watch mode" +- `ember try:each` – Runs the test suite against multiple Ember versions ## Running the dummy application -* `ember serve` -* Visit the dummy application at [http://localhost:4200](http://localhost:4200). +- `ember serve` +- Visit the dummy application at [http://localhost:4200](http://localhost:4200). -For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). \ No newline at end of file +For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). diff --git a/LICENSE.md b/LICENSE.md index d645695..c61b663 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,182 +1,181 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" @@ -187,16 +186,16 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] +Copyright [yyyy] [name of copyright owner] - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index fa72223..e9ae5d9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [**Changelog**](#changelog) -Ember Pouch is a PouchDB/CouchDB adapter for Ember Data 2.0+. For older Ember Data versions use Ember Pouch version 3.2.2. +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. 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. @@ -35,6 +35,7 @@ npm install ember-pouch@3.2.2 --save-dev ``` This provides + - `import PouchDB from 'ember-pouch/pouchdb';` - `import {Model, Adapter, Serializer} from 'ember-pouch'` @@ -48,7 +49,7 @@ import Model, { attr } from '@ember-data/model'; export default class TodoModel extends Model { @attr('string') title; @attr('boolean') isCompleted; - @attr('string') rev; // <-- Add this to all your models + @attr('string') rev; // <-- Add this to all your models } ``` @@ -80,8 +81,8 @@ let remote = new PouchDB('http://localhost:5984/my_couch'); let db = new PouchDB('local_pouch'); db.sync(remote, { - live: true, // do a live, ongoing sync - retry: true // retry if the connection is lost + live: true, // do a live, ongoing sync + retry: true, // retry if the connection is lost }); export default class ApplicationAdapter extends Adapter { @@ -118,13 +119,13 @@ Replace `` with the name of your model and the file will automatical ### Adapter -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 application adapter. +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 application adapter. ``` ember g pouch-adapter application ``` -Now you can store your localDb and remoteDb names in your ember-cli's config. Just add the following keys to the `ENV` object: +Now you can store your localDb and remoteDb names in your ember-cli's config. Just add the following keys to the `ENV` object: ```javascript ENV.emberPouch.localDb = 'test'; @@ -159,12 +160,11 @@ import Controller from '@ember/controller'; import { action } from '@ember/object'; export default class PostController extends Controller { - -@action addComment(comment, author){ + @action addComment(comment, author) { //Create the comment - const comment = this.store.createRecord('comment',{ + const comment = this.store.createRecord('comment', { comment: comment, - author: author + author: author, }); //Add our comment to our existing post this.model.comments.pushObject(comment); @@ -185,8 +185,7 @@ import { action } from '@ember/object'; import { all } from 'rsvp'; export default class AdminController extends Controller { - -@action deletePost(post){ + @action deletePost(post) { //collect the promises for deletion let deletedComments = []; //get and destroy the posts comments @@ -238,8 +237,8 @@ import Route from '@ember/routing/route'; export default class SmasherRoute extends Route { model() { - return this.store.query('smasher', { - filter: { name: 'Mario' } + return this.store.query('smasher', { + filter: { name: 'Mario' }, }); } } @@ -271,15 +270,13 @@ import Route from '@ember/routing/route'; export default class SmasherRoute extends Route { model() { - return this.store.query('smasher', { + return this.store.query('smasher', { filter: { name: 'Mario', - debut: { '$gte': null } + debut: { $gte: null }, }, - sort: [ - { debut: 'desc' } - ] - }) + sort: [{ debut: 'desc' }], + }); } } ``` @@ -292,16 +289,14 @@ import Route from '@ember/routing/route'; export default class SmasherRoute extends Route { model() { - return this.store.query('smasher', { + return this.store.query('smasher', { filter: { name: 'Mario', - debut: { '$gte': null } + debut: { $gte: null }, }, - sort: [ - { debut: 'desc' } - ], - limit: 5 - }) + sort: [{ debut: 'desc' }], + limit: 5, + }); } } ``` @@ -314,21 +309,19 @@ import Route from '@ember/routing/route'; export default class SmasherRoute extends Route { model() { - return this.store.query('smasher', { + return this.store.query('smasher', { filter: { name: 'Mario', - debut: { '$gte': null } + debut: { $gte: null }, }, - sort: [ - { debut: 'desc' } - ], - skip: 5 - }) + sort: [{ debut: 'desc' }], + skip: 5, + }); } } ``` -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. +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. ### store.queryRecord(model, options) @@ -340,8 +333,8 @@ import Route from '@ember/routing/route'; export default class SmasherRoute extends Route { model() { - return this.store.queryRecord('smasher', { - filter: { name: 'Mario' } + return this.store.queryRecord('smasher', { + filter: { name: 'Mario' }, }); } } @@ -360,14 +353,16 @@ import { Model } from 'ember-pouch'; export default class PhotoAlbumModel extends Model { @attr('attachments', { - defaultValue: function() { + defaultValue: function () { return []; - } - }) photos + }, + }) + photos; } ``` 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. + ```handlebars
    {{#each myalbum.photos as |photo|}} @@ -380,18 +375,19 @@ Attach new files by adding an `Ember.Object` with a `.name`, `.content_type` and ```javascript // somewhere in your controller/component: -myAlbum.photos.addObject(Ember.Object.create({ - 'name': 'kitten.jpg', - 'content_type': 'image/jpg', - 'data': btoa('hello world') // base64-encoded `String`, or a DOM `Blob`, or a `File` -})); +myAlbum.photos.addObject( + Ember.Object.create({ + name: 'kitten.jpg', + content_type: 'image/jpg', + data: btoa('hello world'), // base64-encoded `String`, or a DOM `Blob`, or a `File` + }) +); ``` ## Sample app Tom Dale's blog example using Ember CLI and EmberPouch: [broerse/ember-cli-blog](https://github.com/broerse/ember-cli-blog) - ## Notes ### LocalStorage @@ -426,12 +422,12 @@ If you have a model or two that you know will always have a small number of reco With PouchDB, you also get access to a whole host of [PouchDB plugins](http://pouchdb.com/external.html). For example, to use the `pouchdb-authentication` plugin like this using `ember-auto-import`: + ```javascript import PouchDB from 'ember-pouch/pouchdb'; import auth from 'pouchdb-authentication'; PouchDB.plugin(auth); - ``` ### Relational Pouch @@ -458,7 +454,6 @@ You can also take a look at Martin Broerse his [ember-cli-blog](https://github.c ⚠️ 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. - ### Security 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)). @@ -469,7 +464,7 @@ To use, add a Content Security Policy whitelist entry to `/config/environment.js ```js ENV.contentSecurityPolicy = { - "connect-src": "'self' http://your_couch_host.com:5984" + 'connect-src': "'self' http://your_couch_host.com:5984", }; ``` @@ -531,20 +526,17 @@ function changeProjectDatabase(dbName, dbUser, dbPassword) { // CouchDB is serving at http://localhost:5455 let remote = new PouchDB('http://localhost:5455/' + dbName); // here we are using pouchdb-authentication for credential supports - remote.login( dbUser, dbPassword).then( - function (user) { - let db = new PouchDB(dbName) - db.sync(remote, {live:true, retry:true}) - // grab the adapter, it can be any ember-pouch adapter. - let adapter = this.store.adapterFor('project'); - // this is where we told the adapter to change the current database. - adapter.changeDb(db); - } - ) + remote.login(dbUser, dbPassword).then(function (user) { + let db = new PouchDB(dbName); + db.sync(remote, { live: true, retry: true }); + // grab the adapter, it can be any ember-pouch adapter. + let adapter = this.store.adapterFor('project'); + // this is where we told the adapter to change the current database. + adapter.changeDb(db); + }); } ``` - ## Eventually Consistent 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. @@ -552,25 +544,24 @@ Following the CouchDB consistency model, we have introduced `ENV.emberPouch.even `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. 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) - ## Installation -* `git clone` this repository -* `npm install` +- `git clone` this repository +- `npm install` ## Running -* `ember server` -* Visit your app at http://localhost:4200. +- `ember server` +- Visit your app at http://localhost:4200. ## Running Tests -* `ember test` -* `ember test --server` +- `ember test` +- `ember test --server` ## Building -* `ember build` +- `ember build` For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). @@ -581,106 +572,113 @@ This project was originally based on the [ember-data-hal-adapter](https://github 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)! ## Changelog -* **7.0.0** + +- **8.0.0-beta.1** + - Updated to support latest Ember 4.x (fixed isDeleted issues) + - Switch to PouchDB 7.3.0. Getting ready to use the indexeddb-adapter + - Embroider safe and compatible scenarios supported. Ember Pouch works fine with Ember 4.x projects. + - Update application adapter example to use class property #262 + - Fix Adapter Blueprint for Ember Octane #257 +- **7.0.0** - Use ember-auto-import and pouchdb-browser to ease the installation process - relational-pouch@4.0.0 - Use Octane Blueprints -* **6.0.0** +- **6.0.0** - Switch to PouchDB 7.0.0 -* **5.1.0** +- **5.1.0** - 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. -* **5.0.1** +- **5.0.1** - 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. - 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. -* **5.0.0** +- **5.0.0** - Add warning for old `dontsavehasmany` use [#216](https://github.com/pouchdb-community/ember-pouch/pull/216) - forcing the default serializer [#215](https://github.com/pouchdb-community/ember-pouch/pull/215) - test + flag + doc for eventually-consistent [#214](https://github.com/pouchdb-community/ember-pouch/pull/214) - config changes [#213](https://github.com/pouchdb-community/ember-pouch/pull/213) - Update pouchdb to version 6.4.2 [#211](https://github.com/pouchdb-community/ember-pouch/pull/211) -* **5.0.0-beta.6** +- **5.0.0-beta.6** - Add register-version.js to vendor/ember-pouch [#210](https://github.com/pouchdb-community/ember-pouch/pull/210) - Update documentation about Offline First [#209](https://github.com/pouchdb-community/ember-pouch/pull/209) -* **5.0.0-beta.5** +- **5.0.0-beta.5** - Add pouchdb.find.js from pouchdb [#208](https://github.com/pouchdb-community/ember-pouch/pull/208) - createIndex promises should be done before removing [#208](https://github.com/pouchdb-community/ember-pouch/pull/208) - Change sudo to required (see travis-ci/travis-ci#8836) [#208](https://github.com/pouchdb-community/ember-pouch/pull/208) - Ignore same revision changes [#189](https://github.com/pouchdb-community/ember-pouch/pull/189) -* **5.0.0-beta.4** +- **5.0.0-beta.4** - Resolve Ember.String.pluralize() deprecation [#206](https://github.com/pouchdb-community/ember-pouch/pull/206) - allow usage of skip parameter in pouchdb adapter queries [#198](https://github.com/pouchdb-community/ember-pouch/pull/198) -* **5.0.0-beta.3** +- **5.0.0-beta.3** - Fix Ember Data canary ember-try scenario [#202](https://github.com/pouchdb-community/ember-pouch/pull/202) - Restore ember-try configuration for Ember Data [#201](https://github.com/pouchdb-community/ember-pouch/pull/201) - Fix some jobs on Travis Trusty [#187](https://github.com/pouchdb-community/ember-pouch/pull/187) - clean up db changes listener [#195](https://github.com/pouchdb-community/ember-pouch/pull/195) - filter results of pouch adapter query by correct type [#194](https://github.com/pouchdb-community/ember-pouch/pull/194) - allow usage of limit parameter in pouchdb adapter queries [#193](https://github.com/pouchdb-community/ember-pouch/pull/193) -* **5.0.0-beta.2** +- **5.0.0-beta.2** - version fix [#196](https://github.com/pouchdb-community/ember-pouch/pull/196) -* **5.0.0-beta.1** +- **5.0.0-beta.1** - 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. -* **4.3.0** +- **4.3.0** - Bundle pouchdb-find [#191](https://github.com/pouchdb-community/ember-pouch/pull/191) -* **4.2.9** +- **4.2.9** - Lock relational-pouch version until pouchdb-find bugs are solved -* **4.2.8** +- **4.2.8** - Update Ember CLI and PouchDB [#186](https://github.com/pouchdb-community/ember-pouch/pull/186) -* **4.2.7** +- **4.2.7** - Fix `_shouldSerializeHasMany` deprecation [#185](https://github.com/pouchdb-community/ember-pouch/pull/185) -* **4.2.6** +- **4.2.6** - Fixes queryRecord deprecation [#152](https://github.com/pouchdb-community/ember-pouch/pull/152) - Change links to `pouchdb-community` - Use npm for ember-source [#183](https://github.com/pouchdb-community/ember-pouch/pull/183) -* **4.2.5** +- **4.2.5** - Correct Security documentation [#177](https://github.com/pouchdb-community/ember-pouch/pull/177) - Fix sort documentation and add additional notes [#176](https://github.com/pouchdb-community/ember-pouch/pull/176) - update ember-getowner-polyfill to remove deprecation warnings [#174](https://github.com/pouchdb-community/ember-pouch/pull/174) -* **4.2.4** +- **4.2.4** - Fix attachments typo in README [#170](https://github.com/pouchdb-community/ember-pouch/pull/170) -* **4.2.3** +- **4.2.3** - Update pouchdb to the latest version - Minor typofix [#166](https://github.com/pouchdb-community/ember-pouch/pull/166) -* **4.2.2** +- **4.2.2** - Update pouchdb to the latest version -* **4.2.1** +- **4.2.1** - Fix `Init` some more - Fix `Init` `_super.Init` error -* **4.2.0** +- **4.2.0** - Switch to npm versions -* **4.1.0** +- **4.1.0** - async is now true when not specified for relationships - hasMany relationship can have option dontsave -* **4.0.3** +- **4.0.3** - Fixes [#158](https://github.com/pouchdb-community/ember-pouch/pull/158) -* **4.0.2** +- **4.0.2** - Updated ember-cli fixes and some minor changes [#147](https://github.com/pouchdb-community/ember-pouch/pull/147) - Added Version badge and Ember Observer badge [#142](https://github.com/pouchdb-community/ember-pouch/pull/142) -* **4.0.0** +- **4.0.0** - Add support for Attachments [#135](https://github.com/pouchdb-community/ember-pouch/pull/135) - Implement glue code for query and queryRecord using pouchdb-find [#130](https://github.com/pouchdb-community/ember-pouch/pull/130) -* **3.2.2** +- **3.2.2** - Update Bower dependencies [#137](https://github.com/pouchdb-community/ember-pouch/pull/137) - Correct import of Ember Data model blueprint [#131](https://github.com/pouchdb-community/ember-pouch/pull/131) -* **3.2.1** +- **3.2.1** - Fix(Addon): Call super in init [#129](https://github.com/pouchdb-community/ember-pouch/pull/129) -* **3.2.0** +- **3.2.0** - 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) -* **3.1.1** +- **3.1.1** - Bugfix for hasMany relations by [@backspace](https://github.com/backspace) ([#111](https://github.com/pouchdb-community/ember-pouch/pull/111)). -* **3.1.0** +- **3.1.0** - 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! - 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! -* **3.0.1** +- **3.0.1** - 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)). -* **3.0.0** +- **3.0.0** - Update for compatibility with Ember & Ember-Data 2.0+. The adapter now supports Ember & Ember-Data 1.13.x and 2.x only. -* **2.0.3** +- **2.0.3** - 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)! - Indicate to ember-data 1.13+ that reloading individual ember-pouch records is never necessary (due to the change 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)). -* **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)) -* **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) -* **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. -* **1.2.5** - Last release with regular Bower/npm support via bundle javascript in the `dist/` directory. -* **1.0.0** - First release +- **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)) +- **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) +- **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. +- **1.2.5** - Last release with regular Bower/npm support via bundle javascript in the `dist/` directory. +- **1.0.0** - First release diff --git a/addon/adapters/pouch.js b/addon/adapters/pouch.js index 91fc4f9..1eaeebc 100644 --- a/addon/adapters/pouch.js +++ b/addon/adapters/pouch.js @@ -1,19 +1,18 @@ +import RESTAdapter from '@ember-data/adapter/rest'; import { assert } from '@ember/debug'; import { isEmpty } from '@ember/utils'; import { all, defer } from 'rsvp'; -import { get } from '@ember/object'; import { getOwner } from '@ember/application'; import { bind } from '@ember/runloop'; import { on } from '@ember/object/evented'; import { classify, camelize } from '@ember/string'; -import DS from 'ember-data'; import { pluralize } from 'ember-inflector'; //import BelongsToRelationship from 'ember-data/-private/system/relationships/state/belongs-to'; import { extractDeleteRecord, shouldSaveRelationship, - configFlagDisabled + configFlagDisabled, } from '../utils'; //BelongsToRelationship.reopen({ @@ -25,41 +24,46 @@ import { // } //}); -export default DS.RESTAdapter.extend({ +export default class PouchAdapter extends RESTAdapter.extend({ fixDeleteBug: true, coalesceFindRequests: false, // The change listener ensures that individual records are kept up to date // when the data in the database changes. This makes ember-data 2.0's record // reloading redundant. - shouldReloadRecord: function () { return false; }, - shouldBackgroundReloadRecord: function () { return false; }, - _onInit : on('init', function() { + shouldReloadRecord: function () { + return false; + }, + shouldBackgroundReloadRecord: function () { + return false; + }, + _onInit: on('init', function () { this._startChangesToStoreListener(); }), - _startChangesToStoreListener: function() { - var db = this.get('db'); - if (db && !this.changes) { // only run this once + _startChangesToStoreListener: function () { + var db = this.db; + if (db && !this.changes) { + // only run this once var onChangeListener = bind(this, 'onChange'); - this.set('onChangeListener', onChangeListener); + this.onChangeListener = onChangeListener; this.changes = db.changes({ since: 'now', live: true, - returnDocs: false + returnDocs: false, }); this.changes.on('change', onChangeListener); } }, - _stopChangesListener: function() { + _stopChangesListener: function () { if (this.changes) { - var onChangeListener = this.get('onChangeListener'); + var onChangeListener = this.onChangeListener; this.changes.removeListener('change', onChangeListener); this.changes.cancel(); this.changes = undefined; } }, - changeDb: function(db) { + changeDb: function (db) { this._stopChangesListener(); var store = this.store; @@ -70,17 +74,21 @@ export default DS.RESTAdapter.extend({ } this._schema = null; - this.set('db', db); + this.db = db; this._startChangesToStoreListener(); }, onChange: function (change) { // If relational_pouch isn't initialized yet, there can't be any records // in the store to update. - if (!this.get('db').rel) { return; } + if (!this.db.rel) { + return; + } - var obj = this.get('db').rel.parseDocID(change.id); + var obj = this.db.rel.parseDocID(change.id); // skip changes for non-relational_pouch docs. E.g., design docs. - if (!obj.type || !obj.id || obj.type === '') { return; } + if (!obj.type || !obj.id || obj.type === '') { + return; + } var store = this.store; @@ -88,7 +96,7 @@ export default DS.RESTAdapter.extend({ let promise = this.waitingForConsistency[change.id]; delete this.waitingForConsistency[change.id]; if (change.deleted) { - promise.reject("deleted"); + promise.reject('deleted'); } else { promise.resolve(this._findRecord(obj.type, obj.id)); } @@ -113,7 +121,11 @@ export default DS.RESTAdapter.extend({ } return; } - if (!recordInStore.get('isLoaded') || recordInStore.get('rev') === change.changes[0].rev || recordInStore.get('hasDirtyAttributes')) { + if ( + !recordInStore.get('isLoaded') || + recordInStore.get('rev') === change.changes[0].rev || + recordInStore.get('hasDirtyAttributes') + ) { // The record either hasn't loaded yet or has unpersisted local changes. // In either case, we don't want to refresh it in the store // (and for some substates, attempting to do so will result in an error). @@ -123,7 +135,13 @@ export default DS.RESTAdapter.extend({ if (change.deleted) { if (this.fixDeleteBug) { - recordInStore._internalModel.transitionTo('deleted.saved');//work around ember-data bug + if ( + recordInStore._internalModel._recordData && + recordInStore._internalModel._recordData.setIsDeleted + ) { + recordInStore._internalModel._recordData.setIsDeleted(true); + } + recordInStore._internalModel.transitionTo('deleted.saved'); //work around ember-data bug } else { store.unloadRecord(recordInStore); } @@ -132,7 +150,7 @@ export default DS.RESTAdapter.extend({ } }, - unloadedDocumentChanged: function(/* obj */) { + unloadedDocumentChanged: function (/* obj */) { /* * For performance purposes, we don't load records into the store that haven't previously been loaded. * If you want to change this, subclass this method, and push the data into the store. e.g. @@ -145,29 +163,33 @@ export default DS.RESTAdapter.extend({ */ }, - willDestroy: function() { + willDestroy: function () { this._stopChangesListener(); }, - + init() { this._indexPromises = []; this.waitingForConsistency = {}; this.createdRecords = {}; }, - + _indexPromises: null, _init: function (store, type, indexPromises) { var self = this, - recordTypeName = this.getRecordTypeName(type); - if (!this.get('db') || typeof this.get('db') !== 'object') { + recordTypeName = this.getRecordTypeName(type); + if (!this.db || typeof this.db !== 'object') { throw new Error('Please set the `db` property on the adapter.'); } - if (!get(type, 'attributes').has('rev')) { + if (!type.attributes.has('rev')) { var modelName = classify(recordTypeName); - throw new Error('Please add a `rev` attribute of type `string`' + - ' on the ' + modelName + ' model.'); + throw new Error( + 'Please add a `rev` attribute of type `string`' + + ' on the ' + + modelName + + ' model.' + ); } this._schema = this._schema || []; @@ -185,7 +207,7 @@ export default DS.RESTAdapter.extend({ var schemaDef = { singular: singular, - plural: plural + plural: plural, }; if (type.documentType) { @@ -198,35 +220,43 @@ export default DS.RESTAdapter.extend({ // check all the subtypes // We check the type of `rel.type`because with ember-data beta 19 // `rel.type` switched from DS.Model to string - - var rels = [];//extra array is needed since type.relationships/byName return a Map that is not iterable + + var rels = []; //extra array is needed since type.relationships/byName return a Map that is not iterable type.eachRelationship((_relName, rel) => rels.push(rel)); - + let rootCall = indexPromises == undefined; if (rootCall) { indexPromises = []; } - + for (let rel of rels) { if (rel.kind !== 'belongsTo' && rel.kind !== 'hasMany') { // TODO: support inverse as well continue; // skip } var relDef = {}, - relModel = (typeof rel.type === 'string' ? store.modelFor(rel.type) : rel.type); + relModel = + typeof rel.type === 'string' ? store.modelFor(rel.type) : rel.type; if (relModel) { let includeRel = true; if (!('options' in rel)) rel.options = {}; - - if (typeof(rel.options.async) === "undefined") { - rel.options.async = config.emberPouch && !isEmpty(config.emberPouch.async) ? config.emberPouch.async : true;//default true from https://github.com/emberjs/data/pull/3366 + + if (typeof rel.options.async === 'undefined') { + rel.options.async = + config.emberPouch && !isEmpty(config.emberPouch.async) + ? config.emberPouch.async + : true; //default true from https://github.com/emberjs/data/pull/3366 } let options = Object.create(rel.options); if (rel.kind === 'hasMany' && !shouldSaveRelationship(self, rel)) { let inverse = type.inverseFor(rel.key, store); if (inverse) { if (inverse.kind === 'belongsTo') { - indexPromises.push(self.get('db').createIndex({index: { fields: ['data.' + inverse.name, '_id'] }})); + indexPromises.push( + self.get('db').createIndex({ + index: { fields: ['data.' + inverse.name, '_id'] }, + }) + ); if (options.async) { includeRel = false; } else { @@ -239,24 +269,26 @@ export default DS.RESTAdapter.extend({ if (includeRel) { relDef[rel.kind] = { type: self.getRecordTypeName(relModel), - options: options + options: options, }; if (!schemaDef.relations) { schemaDef.relations = {}; } schemaDef.relations[rel.key] = relDef; } - + self._init(store, relModel, indexPromises); } } - this.get('db').setSchema(this._schema); - + this.db.setSchema(this._schema); + if (rootCall) { this._indexPromises = this._indexPromises.concat(indexPromises); return all(indexPromises).then(() => { - this._indexPromises = this._indexPromises.filter(x => !indexPromises.includes(x)); + this._indexPromises = this._indexPromises.filter( + (x) => !indexPromises.includes(x) + ); }); } }, @@ -270,12 +302,7 @@ export default DS.RESTAdapter.extend({ var serializerKey = camelize(modelName); var serializer = store.serializerFor(modelName); - serializer.serializeIntoHash( - data, - type, - record, - {includeId: true} - ); + serializer.serializeIntoHash(data, type, record, { includeId: true }); data = data[serializerKey]; @@ -291,29 +318,31 @@ export default DS.RESTAdapter.extend({ * Return key that conform to data adapter * ex: 'name' become 'data.name' */ - _dataKey: function(key) { - var dataKey ='data.' + key; - return ""+ dataKey + ""; + _dataKey: function (key) { + var dataKey = 'data.' + key; + return '' + dataKey + ''; }, /** * Returns the modified selector key to comform data key * Ex: selector: {name: 'Mario'} wil become selector: {'data.name': 'Mario'} */ - _buildSelector: function(selector) { + _buildSelector: function (selector) { var dataSelector = {}; var selectorKeys = []; for (var key in selector) { - if(selector.hasOwnProperty(key)){ + if (Object.prototype.hasOwnProperty.call(selector, key)) { selectorKeys.push(key); } } - selectorKeys.forEach(function(key) { - var dataKey = this._dataKey(key); - dataSelector[dataKey] = selector[key]; - }.bind(this)); + selectorKeys.forEach( + function (key) { + var dataKey = this._dataKey(key); + dataSelector[dataKey] = selector[key]; + }.bind(this) + ); return dataSelector; }, @@ -323,20 +352,22 @@ export default DS.RESTAdapter.extend({ * Ex: sort: ['series'] will become ['data.series'] * Ex: sort: [{series: 'desc'}] will became [{'data.series': 'desc'}] */ - _buildSort: function(sort) { - return sort.map(function (value) { - var sortKey = {}; - if (typeof value === 'object' && value !== null) { - for (var key in value) { - if(value.hasOwnProperty(key)){ - sortKey[this._dataKey(key)] = value[key]; + _buildSort: function (sort) { + return sort.map( + function (value) { + var sortKey = {}; + if (typeof value === 'object' && value !== null) { + for (var key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + sortKey[this._dataKey(key)] = value[key]; + } } + } else { + return this._dataKey(value); } - } else { - return this._dataKey(value); - } - return sortKey; - }.bind(this)); + return sortKey; + }.bind(this) + ); }, /** @@ -355,22 +386,26 @@ export default DS.RESTAdapter.extend({ return camelize(type.modelName); }, - findAll: async function(store, type /*, sinceToken */) { + findAll: async function (store, type /*, sinceToken */) { // TODO: use sinceToken await this._init(store, type); - return this.get('db').rel.find(this.getRecordTypeName(type)); + return this.db.rel.find(this.getRecordTypeName(type)); }, - findMany: async function(store, type, ids) { + findMany: async function (store, type, ids) { await this._init(store, type); - return this.get('db').rel.find(this.getRecordTypeName(type), ids); + return this.db.rel.find(this.getRecordTypeName(type), ids); }, - findHasMany: async function(store, record, link, rel) { + findHasMany: async function (store, record, link, rel) { await this._init(store, record.type); let inverse = record.type.inverseFor(rel.key, store); if (inverse && inverse.kind === 'belongsTo') { - return this.get('db').rel.findHasMany(camelize(rel.type), inverse.name, record.id); + return this.db.rel.findHasMany( + camelize(rel.type), + inverse.name, + record.id + ); } else { let result = {}; result[pluralize(rel.type)] = []; @@ -378,14 +413,14 @@ export default DS.RESTAdapter.extend({ } }, - query: async function(store, type, query) { + query: async function (store, type, query) { await this._init(store, type); var recordTypeName = this.getRecordTypeName(type); - var db = this.get('db'); + var db = this.db; var queryParams = { - selector: this._buildSelector(query.filter) + selector: this._buildSelector(query.filter), }; if (!isEmpty(query.sort)) { @@ -404,11 +439,11 @@ export default DS.RESTAdapter.extend({ return db.rel.parseRelDocs(recordTypeName, pouchRes.docs); }, - queryRecord: async function(store, type, query) { + queryRecord: async function (store, type, query) { let results = await this.query(store, type, query); let recordType = this.getRecordTypeName(type); let recordTypePlural = pluralize(recordType); - if(results[recordTypePlural].length > 0){ + if (results[recordTypePlural].length > 0) { results[recordType] = results[recordTypePlural][0]; } else { results[recordType] = null; @@ -423,8 +458,8 @@ export default DS.RESTAdapter.extend({ * We keep the method for backward compatibility and forward calls to * `findRecord`. This can be removed when the library drops support * for deprecated methods. - */ - find: function (store, type, id) { + */ + find: function (store, type, id) { return this.findRecord(store, type, id); }, @@ -435,7 +470,7 @@ export default DS.RESTAdapter.extend({ }, async _findRecord(recordTypeName, id) { - let payload = await this.get('db').rel.find(recordTypeName, id); + let payload = await this.db.rel.find(recordTypeName, id); // Ember Data chokes on empty payload, this function throws // an error when the requested data is not found if (typeof payload === 'object' && payload !== null) { @@ -449,23 +484,30 @@ export default DS.RESTAdapter.extend({ } if (configFlagDisabled(this, 'eventuallyConsistent')) - throw new Error("Document of type '" + recordTypeName + "' with id '" + id + "' not found."); - else - return this._eventuallyConsistent(recordTypeName, id); + throw new Error( + "Document of type '" + + recordTypeName + + "' with id '" + + id + + "' not found." + ); + else return this._eventuallyConsistent(recordTypeName, id); }, //TODO: cleanup promises on destroy or db change? waitingForConsistency: null, - _eventuallyConsistent: function(type, id) { - let pouchID = this.get('db').rel.makeDocID({type, id}); + _eventuallyConsistent: function (type, id) { + let pouchID = this.db.rel.makeDocID({ type, id }); let defered = defer(); this.waitingForConsistency[pouchID] = defered; - return this.get('db').rel.isDeleted(type, id).then(deleted => { + return this.db.rel.isDeleted(type, id).then((deleted) => { //TODO: should we test the status of the promise here? Could it be handled in onChange already? if (deleted) { delete this.waitingForConsistency[pouchID]; - throw new Error("Document of type '" + type + "' with id '" + id + "' is deleted."); + throw new Error( + "Document of type '" + type + "' with id '" + id + "' is deleted." + ); } else if (deleted === null) { return defered.promise; } else { @@ -483,17 +525,17 @@ export default DS.RESTAdapter.extend({ }, createdRecords: null, - createRecord: async function(store, type, record) { + createRecord: async function (store, type, record) { await this._init(store, type); var data = this._recordToData(store, type, record); - let rel = this.get('db').rel; - + let rel = this.db.rel; + let id = data.id; if (!id) { id = data.id = rel.uuid(); } this.createdRecords[id] = true; - + let typeName = this.getRecordTypeName(type); try { let saved = await rel.save(typeName, data); @@ -501,7 +543,7 @@ export default DS.RESTAdapter.extend({ let result = {}; result[pluralize(typeName)] = [data]; return result; - } catch(e) { + } catch (e) { delete this.createdRecords[id]; throw e; } @@ -511,8 +553,8 @@ export default DS.RESTAdapter.extend({ await this._init(store, type); var data = this._recordToData(store, type, record); let typeName = this.getRecordTypeName(type); - let saved = await this.get('db').rel.save(typeName, data); - Object.assign(data, saved);//TODO: could only set .rev + let saved = await this.db.rel.save(typeName, data); + Object.assign(data, saved); //TODO: could only set .rev let result = {}; result[pluralize(typeName)] = [data]; return result; @@ -521,7 +563,8 @@ export default DS.RESTAdapter.extend({ deleteRecord: async function (store, type, record) { await this._init(store, type); var data = this._recordToData(store, type, record); - return this.get('db').rel.del(this.getRecordTypeName(type), data) + return this.db.rel + .del(this.getRecordTypeName(type), data) .then(extractDeleteRecord); - } -}); + }, +}) {} diff --git a/addon/index.js b/addon/index.js index b628bb6..acf40d5 100644 --- a/addon/index.js +++ b/addon/index.js @@ -2,8 +2,4 @@ import Model from './model'; import Adapter from './adapters/pouch'; import Serializer from './serializers/pouch'; -export { - Model, - Adapter, - Serializer -}; \ No newline at end of file +export { Model, Adapter, Serializer }; diff --git a/addon/model.js b/addon/model.js index 0caf287..0af62f7 100644 --- a/addon/model.js +++ b/addon/model.js @@ -1,10 +1,5 @@ -import DS from 'ember-data'; - -const { - Model, - attr -} = DS; +import Model, { attr } from '@ember-data/model'; export default Model.extend({ - rev: attr('string') -}); \ No newline at end of file + rev: attr('string'), +}); diff --git a/addon/pouchdb.js b/addon/pouchdb.js index 67ae42b..b7b6a04 100644 --- a/addon/pouchdb.js +++ b/addon/pouchdb.js @@ -1,11 +1,7 @@ - import PouchDB from 'pouchdb-browser'; import PouchDBFind from 'pouchdb-find'; import PouchDBRelational from 'relational-pouch'; -PouchDB - .plugin(PouchDBFind) - .plugin(PouchDBRelational) - ; +PouchDB.plugin(PouchDBFind).plugin(PouchDBRelational); export default PouchDB; diff --git a/addon/serializers/pouch.js b/addon/serializers/pouch.js index 2242f82..5079302 100644 --- a/addon/serializers/pouch.js +++ b/addon/serializers/pouch.js @@ -1,21 +1,17 @@ -import { keys as EmberKeys, assign as EmberAssign } from '@ember/polyfills'; -import { get } from '@ember/object'; -import DS from 'ember-data'; +import JSONSerializer from '@ember-data/serializer/json'; +import RESTSerializer from '@ember-data/serializer/rest'; +import { keys as EmberKeys } from '@ember/polyfills'; -import { - shouldSaveRelationship -} from '../utils'; +import { shouldSaveRelationship } from '../utils'; const keys = Object.keys || EmberKeys; -const assign = Object.assign || EmberAssign; -var Serializer = DS.RESTSerializer.extend({ - - init: function() { +var Serializer = RESTSerializer.extend({ + init: function () { this._super(...arguments); }, - shouldSerializeHasMany: function(snapshot, key, relationship) { + shouldSerializeHasMany: function (snapshot, key, relationship) { let result = shouldSaveRelationship(this, relationship); return result; }, @@ -23,7 +19,9 @@ var Serializer = DS.RESTSerializer.extend({ // This fixes a failure in Ember Data 1.13 where an empty hasMany // was saving as undefined rather than []. serializeHasMany(snapshot, json, relationship) { - if (this._shouldSerializeHasMany(snapshot, relationship.key, relationship)) { + if ( + this._shouldSerializeHasMany(snapshot, relationship.key, relationship) + ) { this._super.apply(this, arguments); const key = relationship.key; @@ -52,9 +50,13 @@ var Serializer = DS.RESTSerializer.extend({ // of the document. // This will conflict with any 'attachments' attr in the model. Suggest that // #toRawDoc in relational-pouch should allow _attachments to be specified - json.attachments = assign({}, json.attachments || {}, json[payloadKey]); // jshint ignore:line + json.attachments = Object.assign( + {}, + json.attachments || {}, + json[payloadKey] + ); // jshint ignore:line json[payloadKey] = keys(json[payloadKey]).reduce((attr, fileName) => { - attr[fileName] = assign({}, json[payloadKey][fileName]); // jshint ignore:line + attr[fileName] = Object.assign({}, json[payloadKey][fileName]); // jshint ignore:line delete attr[fileName].data; delete attr[fileName].content_type; return attr; @@ -64,13 +66,13 @@ var Serializer = DS.RESTSerializer.extend({ extractAttributes(modelClass, resourceHash) { let attributes = this._super(modelClass, resourceHash); - let modelAttrs = get(modelClass, 'attributes'); - modelClass.eachTransformedAttribute(key => { + let modelAttrs = modelClass.attributes; + modelClass.eachTransformedAttribute((key) => { let attribute = modelAttrs.get(key); if (this._isAttachment(attribute)) { // put the corresponding _attachments entries from the response into the attribute let fileNames = keys(attributes[key]); - fileNames.forEach(fileName => { + fileNames.forEach((fileName) => { attributes[key][fileName] = resourceHash.attachments[fileName]; }); } @@ -82,23 +84,26 @@ var Serializer = DS.RESTSerializer.extend({ let relationships = this._super(...arguments); modelClass.eachRelationship((key, relationshipMeta) => { - if (relationshipMeta.kind === 'hasMany' && !shouldSaveRelationship(this, relationshipMeta) && !!relationshipMeta.options.async) { + if ( + relationshipMeta.kind === 'hasMany' && + !shouldSaveRelationship(this, relationshipMeta) && + !!relationshipMeta.options.async + ) { relationships[key] = { links: { related: key } }; } }); return relationships; }, - }); // DEPRECATION: The private method _shouldSerializeHasMany has been promoted to the public API // See https://www.emberjs.com/deprecations/ember-data/v2.x/#toc_jsonserializer-shouldserializehasmany -if( ! DS.JSONSerializer.prototype.shouldSerializeHasMany ) { +if (!JSONSerializer.prototype.shouldSerializeHasMany) { Serializer.reopen({ - _shouldSerializeHasMany( snapshot, key, relationship ){ - return this.shouldSerializeHasMany( snapshot, key, relationship ); - } + _shouldSerializeHasMany(snapshot, key, relationship) { + return this.shouldSerializeHasMany(snapshot, key, relationship); + }, }); } diff --git a/addon/transforms/attachment.js b/addon/transforms/attachment.js index eb029f5..2b41e7d 100644 --- a/addon/transforms/attachment.js +++ b/addon/transforms/attachment.js @@ -2,11 +2,13 @@ import { isNone } from '@ember/utils'; import AttachmentsTransform from './attachments'; export default AttachmentsTransform.extend({ - deserialize: function(serialized) { + deserialize: function (serialized) { return this._super(serialized).pop(); }, - serialize: function(deserialized) { - if (isNone(deserialized)) { return null; } + serialize: function (deserialized) { + if (isNone(deserialized)) { + return null; + } return this._super([deserialized]); - } + }, }); diff --git a/addon/transforms/attachments.js b/addon/transforms/attachments.js index 39d1253..549c396 100644 --- a/addon/transforms/attachments.js +++ b/addon/transforms/attachments.js @@ -1,14 +1,16 @@ +import Transform from '@ember-data/serializer/transform'; import { isArray } from '@ember/array'; import { keys as EmberKeys } from '@ember/polyfills'; -import EmberObject, { get } from '@ember/object'; +import EmberObject from '@ember/object'; import { isNone } from '@ember/utils'; -import DS from 'ember-data'; const keys = Object.keys || EmberKeys; -export default DS.Transform.extend({ - deserialize: function(serialized) { - if (isNone(serialized)) { return []; } +export default Transform.extend({ + deserialize: function (serialized) { + if (isNone(serialized)) { + return []; + } return keys(serialized).map(function (attachmentName) { let attachment = serialized[attachmentName]; @@ -23,24 +25,25 @@ export default DS.Transform.extend({ }); }, - serialize: function(deserialized) { - if (!isArray(deserialized)) { return null; } + serialize: function (deserialized) { + if (!isArray(deserialized)) { + return null; + } return deserialized.reduce(function (acc, attachment) { const serialized = { - content_type: get(attachment, 'content_type'), + content_type: attachment.content_type, }; - if (get(attachment, 'stub')) { + if (attachment.stub) { serialized.stub = true; - serialized.length = get(attachment, 'length'); - serialized.digest = get(attachment, 'digest'); - } - else { - serialized.data = get(attachment, 'data'); - serialized.length = get(attachment, 'length'); + serialized.length = attachment.length; + serialized.digest = attachment.digest; + } else { + serialized.data = attachment.data; + serialized.length = attachment.length; } - acc[get(attachment, 'name')] = serialized; + acc[attachment.name] = serialized; return acc; }, {}); - } + }, }); diff --git a/addon/utils.js b/addon/utils.js index 25e6409..d78188e 100644 --- a/addon/utils.js +++ b/addon/utils.js @@ -7,24 +7,25 @@ export function extractDeleteRecord() { //should this take a config? export function shouldSaveRelationship(container, relationship) { - if (typeof relationship.options.save !== "undefined") + if (typeof relationship.options.save !== 'undefined') return relationship.options.save; - + if (relationship.kind === 'belongsTo') return true; - + //TODO: save default locally? probably on container? - let saveDefault = configFlagEnabled(container, 'saveHasMany');//default is false if not specified - + let saveDefault = configFlagEnabled(container, 'saveHasMany'); //default is false if not specified + return saveDefault; } export function configFlagDisabled(container, key) { //default is on let config = getOwner(container).resolveRegistration('config:environment'); - let result = config['emberPouch'] && - (typeof config['emberPouch'][key] !== 'undefined') && + let result = + config['emberPouch'] && + typeof config['emberPouch'][key] !== 'undefined' && !config['emberPouch'][key]; - + return result; } @@ -32,6 +33,6 @@ export function configFlagEnabled(container, key) { //default is off let config = getOwner(container).resolveRegistration('config:environment'); let result = config['emberPouch'] && config['emberPouch'][key]; - + return result; } diff --git a/blueprints/pouch-adapter/index.js b/blueprints/pouch-adapter/index.js index 41a516d..77207fc 100644 --- a/blueprints/pouch-adapter/index.js +++ b/blueprints/pouch-adapter/index.js @@ -1,5 +1,5 @@ module.exports = { - description: '' + description: '', // locals: function(options) { // // Return custom template variables here. diff --git a/codemods.log b/codemods.log new file mode 100644 index 0000000..b7b5089 --- /dev/null +++ b/codemods.log @@ -0,0 +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 diff --git a/config/ember-try.js b/config/ember-try.js index e418f06..ea9377f 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -1,144 +1,46 @@ 'use strict'; const getChannelURL = require('ember-source-channel-url'); +const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); -module.exports = async function() { +module.exports = async function () { return { + useYarn: true, scenarios: [ { - name: 'ember-2.4-stack', + name: 'ember-lts-3.16', npm: { devDependencies: { - 'ember-data': '2.4.3', - 'ember-inflector': '^1.9.4', - 'ember-source': null, - 'ember-cli-shims': null - } - }, - bower: { - dependencies: { - 'ember': '2.4.6', - 'ember-cli-shims': '0.1.1' - } - } - }, - { - name: 'ember-lts-2.8', - bower: { - dependencies: { - 'ember': 'components/ember#lts-2-8', - 'ember-cli-shims': '0.1.1' + 'ember-source': '~3.16.0', + 'ember-data': '~3.16.0', }, - resolutions: { - 'ember': 'lts-2-8' - } }, - npm: { - devDependencies: { - 'ember-data': '2.8.1', - 'ember-inflector': '^1.9.4', - 'ember-source': null, - 'ember-cli-shims': null - } - } }, { - name: 'ember-lts-2.12', - env: { - EMBER_OPTIONAL_FEATURES: JSON.stringify({ 'jquery-integration': true }), - }, - npm: { - devDependencies: { - '@ember/jquery': '^0.5.1', - 'ember-data': '2.12.2', - 'ember-inflector': '^1.9.4', - 'ember-source': '2.12.2', - 'ember-cli-shims': '^1.1.0' - } - } - }, - { - name: 'ember-2.14-stack', - env: { - EMBER_OPTIONAL_FEATURES: JSON.stringify({ 'jquery-integration': true }), - }, + name: 'ember-lts-3.24', npm: { devDependencies: { - '@ember/jquery': '^0.5.1', - 'ember-data': '~2.14.0', - 'ember-source': '~2.14.0', - } + 'ember-source': '~3.24.3', + 'ember-data': '~3.24.0', + }, }, }, { - name: 'ember-lts-2.16', - env: { - EMBER_OPTIONAL_FEATURES: JSON.stringify({ 'jquery-integration': true }), - }, + name: 'ember-lts-3.28', npm: { devDependencies: { - '@ember/jquery': '^0.5.1', - 'ember-data': '~2.16.0', - 'ember-source': '~2.16.0', - } - } - }, - { - name: 'ember-lts-2.18', - env: { - EMBER_OPTIONAL_FEATURES: JSON.stringify({ 'jquery-integration': true }), + 'ember-source': '~3.28.0', + 'ember-data': '~3.28.7', + }, }, - npm: { - devDependencies: { - '@ember/jquery': '^0.5.1', - 'ember-data': '~2.18.0', - 'ember-source': '~2.18.0', - } - } - }, - { - name: 'ember-lts-3.4', - npm: { - devDependencies: { - 'ember-source': '~3.4.0', - 'ember-data': '~3.4.0', - } - } - }, - { - name: 'ember-lts-3.8', - npm: { - devDependencies: { - 'ember-source': '~3.8.0', - 'ember-data': '~3.8.0', - } - } - }, - { - name: 'ember-lts-3.12', - npm: { - devDependencies: { - 'ember-source': '~3.12.0', - 'ember-data': '~3.12.0', - } - } - }, - { - name: 'ember-lts-3.16', - npm: { - devDependencies: { - 'ember-source': '~3.16.0', - 'ember-data': '~3.16.0', - } - } }, { name: 'ember-release', npm: { devDependencies: { 'ember-data': 'latest', - 'ember-source': await getChannelURL('release') - } + 'ember-source': await getChannelURL('release'), + }, }, }, { @@ -146,18 +48,18 @@ module.exports = async function() { npm: { devDependencies: { 'ember-data': 'beta', - 'ember-source': await getChannelURL('beta') - } - } + 'ember-source': await getChannelURL('beta'), + }, + }, }, { name: 'ember-canary', npm: { devDependencies: { 'ember-data': 'canary', - 'ember-source': await getChannelURL('canary') - } - } + 'ember-source': await getChannelURL('canary'), + }, + }, }, // The default `.travis.yml` runs this scenario via `npm test`, // not via `ember try`. It's still included here so that running @@ -166,21 +68,8 @@ module.exports = async function() { { name: 'ember-default', npm: { - devDependencies: {} - } - }, - { - name: 'ember-default-with-jquery', - env: { - EMBER_OPTIONAL_FEATURES: JSON.stringify({ - 'jquery-integration': true - }) + devDependencies: {}, }, - npm: { - devDependencies: { - '@ember/jquery': '^0.5.1' - } - } }, { name: 'ember-classic', @@ -188,15 +77,21 @@ module.exports = async function() { EMBER_OPTIONAL_FEATURES: JSON.stringify({ 'application-template-wrapper': true, 'default-async-observers': false, - 'template-only-glimmer-components': false - }) + 'template-only-glimmer-components': false, + }), }, npm: { + devDependencies: { + 'ember-source': '~3.28.0', + 'ember-data': '~3.28.7', + }, ember: { - edition: 'classic' - } - } - } - ] + edition: 'classic', + }, + }, + }, + embroiderSafe(), + embroiderOptimized(), + ], }; }; diff --git a/config/environment.js b/config/environment.js index 012a412..9b5d25d 100644 --- a/config/environment.js +++ b/config/environment.js @@ -1,6 +1,6 @@ /* eslint-env node */ 'use strict'; -module.exports = function(/* environment, appConfig */) { - return { }; +module.exports = function (/* environment, appConfig */) { + return {}; }; diff --git a/ember-cli-build.js b/ember-cli-build.js index 156e23f..f4451a8 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -1,9 +1,15 @@ -/* eslint-env node */ 'use strict'; const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); +const { maybeEmbroider } = require('@embroider/test-setup'); -module.exports = function(defaults) { +/** + * `EMBROIDER_TEST_SETUP_OPTIONS` is set by the Embroider scenarios for `ember-try`: + * https://github.com/embroider-build/embroider/blob/v0.47.1/packages/test-setup/src/index.ts#L48-L90 + */ +const IS_EMBROIDER_ENABLED = Boolean(process.env.EMBROIDER_TEST_SETUP_OPTIONS); + +module.exports = function (defaults) { let app = new EmberAddon(defaults, { // Add options here }); @@ -15,5 +21,20 @@ module.exports = function(defaults) { behave. You most likely want to be modifying `./index.js` or app's build file */ - return app.toTree(); + return maybeEmbroider(app, { + skipBabel: [ + { + package: 'qunit', + }, + ], + /* eslint-disable prettier/prettier */ + packagerOptions: { + webpackConfig: IS_EMBROIDER_ENABLED === false ? {} : { + node: { + global: true, + }, + }, + }, + /* eslint-disable prettier/prettier */ + }); }; diff --git a/index.js b/index.js index f763e58..4cb841e 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,10 @@ /* eslint-env node */ 'use strict'; -var stew = require('broccoli-stew'); -var writeFile = require('broccoli-file-creator'); -var version = require('./package.json').version; - module.exports = { name: require('./package').name, - options: { - autoImport:{ + autoImport: { webpack: { node: { global: true, @@ -18,26 +13,8 @@ module.exports = { }, }, - init: function() { + init: function () { this._super.init && this._super.init.apply(this, arguments); - - var bowerDeps = this.project.bowerDependencies(); - - if (bowerDeps['pouchdb']) {this.ui.writeWarnLine('Please remove `pouchdb` from `bower.json`. As of ember-pouch 4.2.0, only the NPM package is needed.');} - if (bowerDeps['relational-pouch']) {this.ui.writeWarnLine('Please remove `relational-pouch` from `bower.json`. As of ember-pouch 4.2.0, only the NPM package is needed.');} - if (bowerDeps['pouchdb-find']) {this.ui.writeWarnLine('Please remove `pouchdb-find` from `bower.json`. As of ember-pouch 4.2.0, only the NPM package is needed.');} - }, - - treeForVendor: function() { - var content = "Ember.libraries.register('Ember Pouch', '" + version + "');"; - var registerVersionTree = writeFile( - 'ember-pouch/register-version.js', - content - ); - - return stew.find([ - registerVersionTree - ]); }, included(app) { @@ -48,12 +25,14 @@ module.exports = { app = app.app; } - app.import('vendor/ember-pouch/register-version.js'); - let env = this.project.config(app.env); if (env.emberpouch) { - if (env.emberpouch.hasOwnProperty('dontsavehasmany')) { - this.ui.writeWarnLine('The `dontsavehasmany` flag is no longer needed in `config/environment.js`'); + if ( + Object.prototype.hasOwnProperty.call(env.emberpouch, 'dontsavehasmany') + ) { + this.ui.writeWarnLine( + 'The `dontsavehasmany` flag is no longer needed in `config/environment.js`' + ); } } }, diff --git a/package.json b/package.json index 8c94c43..7c6f2bb 100644 --- a/package.json +++ b/package.json @@ -1,88 +1,100 @@ -{ - "name": "ember-pouch", - "version": "7.0.0", - "description": "PouchDB adapter for Ember Data", - "author": "Nolan Lawson", - "directories": { - "doc": "doc", - "test": "tests" - }, - "scripts": { - "build": "ember build --environment=production", - "license": "license-checker --summary --failOn GPL", - "lint:hbs": "ember-template-lint .", - "lint:js": "eslint .", - "start": "ember server", - "test": "ember test", - "test:all": "ember try:each" - }, - "keywords": [ - "ember-addon", +{ + "name": "ember-pouch", + "version": "8.0.0-beta.1", + "description": "PouchDB adapter for Ember Data", + "keywords": [ + "ember-addon", "Ember", - "Octane", - "Data", - "adapter", - "PouchDB", - "CouchDB" - ], - "repository": { - "type": "git", - "url": "https://github.com/pouchdb-community/ember-pouch.git" - }, - "engines": { - "node": "10.* || >= 12" - }, - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/pouchdb-community/ember-pouch/issues" - }, - "devDependencies": { - "@ember/optional-features": "^1.3.0", - "@glimmer/component": "^1.0.0", - "@glimmer/tracking": "^1.0.0", - "babel-eslint": "^10.0.3", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^1.5.3", - "ember-cli": "~3.16.0", - "ember-cli-app-version": "^3.2.0", - "ember-cli-dependency-checker": "^3.2.0", - "ember-cli-eslint": "^5.1.0", - "ember-cli-inject-live-reload": "^2.0.2", - "ember-cli-release": "^0.2.9", - "ember-cli-sri": "^2.1.1", - "ember-cli-template-lint": "^1.0.0-beta.3", - "ember-cli-uglify": "^3.0.0", - "ember-data": "~3.16.0", - "ember-disable-prototype-extensions": "^1.1.3", - "ember-export-application-global": "^2.0.1", - "ember-inflector": "^3.0.0", - "ember-load-initializers": "^2.1.1", - "ember-maybe-import-regenerator": "^0.1.6", - "ember-qunit": "^4.6.0", - "ember-resolver": "^7.0.0", - "ember-source": "~3.16.0", - "ember-source-channel-url": "^2.0.1", - "ember-try": "^1.4.0", - "eslint-plugin-ember": "^7.7.2", - "eslint-plugin-node": "^11.0.0", - "license-checker": "^25.0.1", - "loader.js": "^4.7.0", - "qunit-dom": "^1.0.0" - }, - "dependencies": { - "broccoli-file-creator": "^2.1.1", - "broccoli-stew": "^2.1.0", - "ember-auto-import": "^1.5.3", - "ember-cli-babel": "^7.17.2", - "ember-cli-htmlbars": "^4.2.2", - "pouchdb-browser": "^7.1.1", - "pouchdb-find": "^7.1.1", - "relational-pouch": "^4.0.0" - }, - "ember": { - "edition": "classic" - }, - "ember-addon": { - "configPath": "tests/dummy/config" - } -} + "Octane", + "Data", + "adapter", + "PouchDB", + "CouchDB" + ], + "bugs": { + "url": "https://github.com/pouchdb-community/ember-pouch/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/pouchdb-community/ember-pouch.git" + }, + "license": "Apache-2.0", + "author": "Nolan Lawson", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "license": "license-checker --summary --failOn GPL", + "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", + "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember server", + "test": "npm-run-all lint test:*", + "test:ember": "ember test", + "test:ember-compatibility": "ember try:each" + }, + "dependencies": { + "ember-auto-import": "~2.4.2", + "ember-cli-babel": "^7.26.11", + "pouchdb-browser": "^7.3.0", + "pouchdb-find": "^7.3.0", + "relational-pouch": "^4.0.0" + }, + "devDependencies": { + "@ember/optional-features": "^2.0.0", + "@ember/test-helpers": "^2.6.0", + "@embroider/test-setup": "^1.5.0", + "@glimmer/component": "^1.0.4", + "@glimmer/tracking": "^1.0.4", + "babel-eslint": "^10.1.0", + "broccoli-asset-rev": "^3.0.0", + "ember-auto-import": "~2.4.2", + "ember-cli": "~4.3.0", + "ember-cli-app-version": "~5.0.0", + "ember-cli-dependency-checker": "^3.2.0", + "ember-cli-htmlbars": "^6.0.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-release": "^0.2.9", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-data": "~4.4.0", + "ember-disable-prototype-extensions": "^1.1.3", + "ember-export-application-global": "^2.0.1", + "ember-inflector": "~4.0.2", + "ember-load-initializers": "^2.1.2", + "ember-maybe-import-regenerator": "^1.0.0", + "ember-qunit": "^5.1.5", + "ember-resolver": "^8.0.3", + "ember-source": "~4.3.0", + "ember-source-channel-url": "^3.0.0", + "ember-template-lint": "^4.3.0", + "ember-try": "^2.0.0", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-ember": "^10.5.9", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-qunit": "^7.2.0", + "license-checker": "^25.0.1", + "loader.js": "^4.7.0", + "npm-run-all": "^4.1.5", + "prettier": "^2.6.1", + "qunit": "^2.18.0", + "qunit-dom": "^2.0.0", + "webpack": "^5.70.0" + }, + "engines": { + "node": "12.* || 14.* || >= 16" + }, + "ember": { + "edition": "octane" + }, + "ember-addon": { + "configPath": "tests/dummy/config" + } +} diff --git a/testem.js b/testem.js index a23847f..ed2f371 100644 --- a/testem.js +++ b/testem.js @@ -1,13 +1,10 @@ -/* eslint-env node */ +'use strict'; + module.exports = { test_page: 'tests/index.html?hidepassed', disable_watching: true, - launch_in_ci: [ - 'Chrome' - ], - launch_in_dev: [ - 'Chrome' - ], + launch_in_ci: ['Chrome'], + launch_in_dev: ['Chrome'], browser_start_timeout: 120, browser_args: { Chrome: { @@ -19,8 +16,8 @@ module.exports = { '--disable-software-rasterizer', '--mute-audio', '--remote-debugging-port=0', - '--window-size=1440,900' - ].filter(Boolean) + '--window-size=1440,900', + ].filter(Boolean), }, - } + }, }; diff --git a/tests/dummy/app/adapter.js b/tests/dummy/app/adapter.js deleted file mode 100644 index cf15992..0000000 --- a/tests/dummy/app/adapter.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Adapter } from 'ember-pouch'; -import config from 'dummy/config/environment'; - -export default Adapter.extend({ - _init(store, type) { - type.eachRelationship((name, rel) => { - rel.options.async = config.emberPouch.async; - if (rel.kind === 'hasMany') { - rel.options.save = config.emberPouch.saveHasMany; - } - }); - return this._super(...arguments); - }, -}); diff --git a/tests/dummy/app/adapters/application.js b/tests/dummy/app/adapters/application.js index cf69e46..67082b0 100644 --- a/tests/dummy/app/adapters/application.js +++ b/tests/dummy/app/adapters/application.js @@ -1,7 +1,7 @@ import { defer } from 'rsvp'; import { assert } from '@ember/debug'; import { isEmpty } from '@ember/utils'; -import Adapter from 'dummy/adapter'; +import { Adapter } from 'ember-pouch'; import PouchDB from 'ember-pouch/pouchdb'; import config from 'dummy/config/environment'; @@ -13,31 +13,45 @@ function createDb() { let db = new PouchDB(localDb); if (config.emberPouch.remote) { - let remoteDb = new PouchDB(config.emberPouch.remoteDb); + let remoteDb = new PouchDB(config.emberPouch.remoteDb); - db.sync(remoteDb, { - live: true, - retry: true - }); + db.sync(remoteDb, { + live: true, + retry: true, + }); } return db; } -export default Adapter.extend({ - init() { - this._super(...arguments); - this.set('db', createDb()); - }, - - onChangeListenerTest: null, +export default class ApplicationAdapter extends Adapter { + constructor(owner, args) { + super(owner, args); + this.db = createDb(); + } + + _init(store, type) { + type.eachRelationship((name, rel) => { + rel.options.async = config.emberPouch.async; + if (rel.kind === 'hasMany') { + rel.options.save = config.emberPouch.saveHasMany; + } + }); + if (super._init) { + return super._init(...arguments); + } + } + + onChangeListenerTest = null; onChange() { - this._super(...arguments); + if (super.onChange) { + super.onChange(...arguments); + } if (this.onChangeListenerTest) { this.onChangeListenerTest(...arguments); } - }, - + } + waitForChangeWithID(id) { let defered = defer(); this.onChangeListenerTest = (c) => { @@ -45,7 +59,7 @@ export default Adapter.extend({ this.onChangeListenerTest = null; defered.resolve(c); } - } + }; return defered.promise; - }, -}); + } +} diff --git a/tests/dummy/app/adapters/taco-salad.js b/tests/dummy/app/adapters/taco-salad.js index 3e3c067..5d1fd19 100644 --- a/tests/dummy/app/adapters/taco-salad.js +++ b/tests/dummy/app/adapters/taco-salad.js @@ -1,7 +1,7 @@ import { run } from '@ember/runloop'; import { assert } from '@ember/debug'; import { isEmpty } from '@ember/utils'; -import Adapter from 'dummy/adapter'; +import { Adapter } from 'ember-pouch'; import PouchDB from 'ember-pouch/pouchdb'; import config from 'dummy/config/environment'; @@ -13,29 +13,42 @@ function createDb() { let db = new PouchDB(localDb); if (config.emberPouch.remote) { - let remoteDb = new PouchDB(config.emberPouch.remoteDb); + let remoteDb = new PouchDB(config.emberPouch.remoteDb); - db.sync(remoteDb, { - live: true, - retry: true - }); + db.sync(remoteDb, { + live: true, + retry: true, + }); } return db; } -export default Adapter.extend({ - init() { - this._super(...arguments); - this.set('db', createDb()); - }, +export default class TacoSaladAdapter extends Adapter { + constructor(owner, args) { + super(owner, args); + this.db = createDb(); + } + + _init(store, type) { + type.eachRelationship((name, rel) => { + rel.options.async = config.emberPouch.async; + if (rel.kind === 'hasMany') { + rel.options.save = config.emberPouch.saveHasMany; + } + }); + if (super._init) { + return super._init(...arguments); + } + } + unloadedDocumentChanged(obj) { - let store = this.get('store'); + let store = this.store; let recordTypeName = this.getRecordTypeName(store.modelFor(obj.type)); - this.get('db').rel.find(recordTypeName, obj.id).then(function(doc){ - run(function() { + this.db.rel.find(recordTypeName, obj.id).then(function (doc) { + run(function () { store.pushPayload(recordTypeName, doc); }); }); } -}); +} diff --git a/tests/dummy/app/app.js b/tests/dummy/app/app.js index 6a4f53e..5d3411f 100644 --- a/tests/dummy/app/app.js +++ b/tests/dummy/app/app.js @@ -1,12 +1,12 @@ import Application from '@ember/application'; import Resolver from 'ember-resolver'; import loadInitializers from 'ember-load-initializers'; -import config from './config/environment'; +import config from 'dummy/config/environment'; const App = Application.extend({ modulePrefix: config.modulePrefix, podModulePrefix: config.podModulePrefix, - Resolver + Resolver, }); loadInitializers(App, config.modulePrefix); diff --git a/tests/dummy/app/index.html b/tests/dummy/app/index.html index 61400b2..247e7e2 100644 --- a/tests/dummy/app/index.html +++ b/tests/dummy/app/index.html @@ -1,16 +1,16 @@ - - + + Dummy - - + + {{content-for "head"}} - - + + {{content-for "head-footer"}} diff --git a/tests/dummy/app/models/food-item.js b/tests/dummy/app/models/food-item.js index 4aac718..88df31a 100644 --- a/tests/dummy/app/models/food-item.js +++ b/tests/dummy/app/models/food-item.js @@ -1,9 +1,9 @@ -import DS from 'ember-data'; +import { attr, belongsTo } from '@ember-data/model'; import { Model } from 'ember-pouch'; // N.b.: awkward model name is to test getRecordTypeName export default Model.extend({ - name: DS.attr('string'), - soup: DS.belongsTo('taco-soup') + name: attr('string'), + soup: belongsTo('taco-soup'), }); diff --git a/tests/dummy/app/models/smasher.js b/tests/dummy/app/models/smasher.js index 3948998..22e5b4f 100644 --- a/tests/dummy/app/models/smasher.js +++ b/tests/dummy/app/models/smasher.js @@ -1,9 +1,8 @@ -import DS from 'ember-data'; +import { attr } from '@ember-data/model'; import { Model } from 'ember-pouch'; export default Model.extend({ - name: DS.attr('string'), - series: DS.attr('string'), - debut: DS.attr(), + name: attr('string'), + series: attr('string'), + debut: attr(), }); - diff --git a/tests/dummy/app/models/taco-recipe.js b/tests/dummy/app/models/taco-recipe.js index f8a41b7..8c05c30 100644 --- a/tests/dummy/app/models/taco-recipe.js +++ b/tests/dummy/app/models/taco-recipe.js @@ -1,7 +1,7 @@ -import DS from 'ember-data'; +import { attr } from '@ember-data/model'; import { Model } from 'ember-pouch'; export default Model.extend({ - coverImage: DS.attr('attachment'), - photos: DS.attr('attachments') + coverImage: attr('attachment'), + photos: attr('attachments'), }); diff --git a/tests/dummy/app/models/taco-salad.js b/tests/dummy/app/models/taco-salad.js index 1849a89..48346c4 100644 --- a/tests/dummy/app/models/taco-salad.js +++ b/tests/dummy/app/models/taco-salad.js @@ -1,7 +1,7 @@ -import DS from 'ember-data'; +import { attr, hasMany } from '@ember-data/model'; import { Model } from 'ember-pouch'; export default Model.extend({ - flavor: DS.attr('string'), - ingredients: DS.hasMany('food-item') + flavor: attr('string'), + ingredients: hasMany('food-item'), }); diff --git a/tests/dummy/app/models/taco-soup.js b/tests/dummy/app/models/taco-soup.js index 1849a89..48346c4 100644 --- a/tests/dummy/app/models/taco-soup.js +++ b/tests/dummy/app/models/taco-soup.js @@ -1,7 +1,7 @@ -import DS from 'ember-data'; +import { attr, hasMany } from '@ember-data/model'; import { Model } from 'ember-pouch'; export default Model.extend({ - flavor: DS.attr('string'), - ingredients: DS.hasMany('food-item') + flavor: attr('string'), + ingredients: hasMany('food-item'), }); diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 224ca42..64e543a 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -1,10 +1,9 @@ import EmberRouter from '@ember/routing/router'; -import config from './config/environment'; +import config from 'dummy/config/environment'; export default class Router extends EmberRouter { location = config.locationType; rootURL = config.rootURL; } -Router.map(function() { -}); +Router.map(function () {}); diff --git a/tests/dummy/app/serializers/taco-recipe.js b/tests/dummy/app/serializers/taco-recipe.js index c74ddc3..d64d4c7 100644 --- a/tests/dummy/app/serializers/taco-recipe.js +++ b/tests/dummy/app/serializers/taco-recipe.js @@ -3,6 +3,6 @@ import ApplicationSerializer from './application'; export default ApplicationSerializer.extend({ attrs: { coverImage: 'cover_image', - photos: { key: 'photo_gallery' } - } + photos: { key: 'photo_gallery' }, + }, }); diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs index e2147ca..ba7d1f8 100644 --- a/tests/dummy/app/templates/application.hbs +++ b/tests/dummy/app/templates/application.hbs @@ -1 +1,2 @@ +

    Welcome to Ember

    {{outlet}} \ No newline at end of file diff --git a/tests/dummy/config/ember-cli-update.json b/tests/dummy/config/ember-cli-update.json new file mode 100644 index 0000000..c4e5616 --- /dev/null +++ b/tests/dummy/config/ember-cli-update.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "ember-cli", + "version": "3.28.5", + "blueprints": [ + { + "name": "addon", + "outputRepo": "https://github.com/ember-cli/ember-addon-output", + "codemodsSource": "ember-addon-codemods-manifest@1", + "isBaseBlueprint": true, + "options": ["--no-welcome"] + } + ] + } + ] +} diff --git a/tests/dummy/config/environment.js b/tests/dummy/config/environment.js index 10aea88..4390152 100644 --- a/tests/dummy/config/environment.js +++ b/tests/dummy/config/environment.js @@ -1,13 +1,13 @@ /* eslint-env node */ 'use strict'; -module.exports = function(environment) { +module.exports = function (environment) { let ENV = { modulePrefix: 'dummy', environment, emberPouch: { localDb: 'ember-pouch-test' }, rootURL: '/', - locationType: 'auto', + locationType: 'history', EmberENV: { FEATURES: { // Here you can enable experimental features on an ember canary build @@ -16,14 +16,14 @@ module.exports = function(environment) { }, EXTEND_PROTOTYPES: { // Prevent Ember Data from overriding Date.parse. - Date: false - } + Date: false, + }, }, APP: { // Here you can pass flags/options to your application instance // when it is created - } + }, }; if (environment === 'development') { @@ -36,7 +36,7 @@ module.exports = function(environment) { if (environment === 'test') { // Testem prefers this... - ENV.locationType = 'none'; + ENV.locationType = 'history'; // keep test console output quieter ENV.APP.LOG_ACTIVE_GENERATION = false; diff --git a/tests/dummy/config/targets.js b/tests/dummy/config/targets.js index 8ffae36..3cd797a 100644 --- a/tests/dummy/config/targets.js +++ b/tests/dummy/config/targets.js @@ -3,16 +3,24 @@ const browsers = [ 'last 1 Chrome versions', 'last 1 Firefox versions', - 'last 1 Safari versions' + 'last 1 Safari versions', ]; -const isCI = !!process.env.CI; -const isProduction = process.env.EMBER_ENV === 'production'; - -if (isCI || isProduction) { - browsers.push('ie 11'); -} +// Ember's browser support policy is changing, and IE11 support will end in +// v4.0 onwards. +// +// See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy +// +// If you need IE11 support on a version of Ember that still offers support +// for it, uncomment the code block below. +// +// const isCI = Boolean(process.env.CI); +// const isProduction = process.env.EMBER_ENV === 'production'; +// +// if (isCI || isProduction) { +// browsers.push('ie 11'); +// } module.exports = { - browsers + browsers, }; diff --git a/tests/helpers/module-for-pouch-acceptance.js b/tests/helpers/module-for-pouch-acceptance.js index 7594e39..c7906c0 100644 --- a/tests/helpers/module-for-pouch-acceptance.js +++ b/tests/helpers/module-for-pouch-acceptance.js @@ -1,7 +1,7 @@ import { Promise, all, resolve } from 'rsvp'; -export default function(hooks) { - hooks.beforeEach(function() { +export default function (hooks) { + hooks.beforeEach(function () { return Promise.resolve().then(() => { this.store = function store() { return this.owner.lookup('service:store'); @@ -19,18 +19,19 @@ export default function(hooks) { }; }); }); - - hooks.afterEach(function() { + + hooks.afterEach(function () { let db = this.db(); return all(this.adapter()._indexPromises) - .then(() => { - return db.getIndexes().then(data => { - return all(data.indexes.map( - index => { - return index.ddoc ? (db.deleteIndex(index)) : (resolve()); - } - )); - }); - }).then(() => db.destroy()); + .then(() => { + return db.getIndexes().then((data) => { + return all( + data.indexes.map((index) => { + return index.ddoc ? db.deleteIndex(index) : resolve(); + }) + ); + }); + }) + .then(() => db.destroy()); }); } diff --git a/tests/index.html b/tests/index.html index 5209b85..091c906 100644 --- a/tests/index.html +++ b/tests/index.html @@ -1,33 +1,36 @@ - - + + Dummy Tests - - + + - {{content-for "head"}} - {{content-for "test-head"}} + {{content-for "head"}} {{content-for "test-head"}} - - - + + + - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} + {{content-for "head-footer"}} {{content-for "test-head-footer"}} - {{content-for "body"}} - {{content-for "test-body"}} + {{content-for "body"}} {{content-for "test-body"}} - +
    +
    +
    +
    +
    +
    + + - {{content-for "body-footer"}} - {{content-for "test-body-footer"}} + {{content-for "body-footer"}} {{content-for "test-body-footer"}} diff --git a/tests/integration/adapters/pouch-basics-test.js b/tests/integration/adapters/pouch-basics-test.js index 040f381..5a421dd 100644 --- a/tests/integration/adapters/pouch-basics-test.js +++ b/tests/integration/adapters/pouch-basics-test.js @@ -3,7 +3,7 @@ import { Promise, all } from 'rsvp'; import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import DS from 'ember-data'; +// import DS from 'ember-data'; import moduleForIntegration from '../../helpers/module-for-pouch-acceptance'; import config from 'dummy/config/environment'; @@ -22,554 +22,838 @@ function promiseToRunLater(timeout) { // } //} - function savingHasMany() { - return config.emberPouch.saveHasMany; + return config.emberPouch.saveHasMany; } function getDocsForRelations() { - let result = []; + let result = []; - let c = { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }; - if (savingHasMany()) { c.data.ingredients = ['X', 'Y']; } - result.push(c); + let c = { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }; + if (savingHasMany()) { + c.data.ingredients = ['X', 'Y']; + } + result.push(c); - let d = { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }; - if (savingHasMany()) { d.data.ingredients = ['Z']; } - result.push(d); + let d = { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }; + if (savingHasMany()) { + d.data.ingredients = ['Z']; + } + result.push(d); - result.push({ _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' }}); - result.push({ _id: 'foodItem_2_Y', data: { name: 'pork loin', soup: 'C' }}); - result.push({ _id: 'foodItem_2_Z', data: { name: 'black beans', soup: 'D' }}); + result.push({ _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' } }); + result.push({ _id: 'foodItem_2_Y', data: { name: 'pork loin', soup: 'C' } }); + result.push({ + _id: 'foodItem_2_Z', + data: { name: 'black beans', soup: 'D' }, + }); - return result; + return result; } /* * Tests basic CRUD behavior for an app using the ember-pouch adapter. */ +module('Integration | Adapter | Basic CRUD Ops', {}, function (hooks) { + setupTest(hooks); + moduleForIntegration(hooks); + + let allTests = function () { + test('can find all', function (assert) { + assert.expect(3); + + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs([ + { _id: 'tacoSoup_2_A', data: { flavor: 'al pastor' } }, + { _id: 'tacoSoup_2_B', data: { flavor: 'black bean' } }, + { _id: 'burritoShake_2_X', data: { consistency: 'smooth' } }, + ]); + }) + .then(() => { + return this.store().findAll('taco-soup'); + }) + .then((found) => { + assert.strictEqual( + found.get('length'), + 2, + 'should have found the two taco soup items only' + ); + assert.deepEqual( + found.mapBy('id'), + ['A', 'B'], + 'should have extracted the IDs correctly' + ); + assert.deepEqual( + found.mapBy('flavor'), + ['al pastor', 'black bean'], + 'should have extracted the attributes also' + ); + }) + .finally(done); + }); -module('Integration | Adapter | Basic CRUD Ops', {}, function(hooks) { -setupTest(hooks); -moduleForIntegration(hooks); - -let allTests = function() { - -test('can find all', function (assert) { - assert.expect(3); - - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs([ - { _id: 'tacoSoup_2_A', data: { flavor: 'al pastor' } }, - { _id: 'tacoSoup_2_B', data: { flavor: 'black bean' } }, - { _id: 'burritoShake_2_X', data: { consistency: 'smooth' } } - ]); - }).then(() => { - return this.store().findAll('taco-soup'); - }).then((found) => { - assert.equal(found.get('length'), 2, 'should have found the two taco soup items only'); - assert.deepEqual(found.mapBy('id'), ['A', 'B'], - 'should have extracted the IDs correctly'); - assert.deepEqual(found.mapBy('flavor'), ['al pastor', 'black bean'], - 'should have extracted the attributes also'); - }).finally(done); -}); - -test('can find one', function (assert) { - assert.expect(2); - - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs([ - { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, - { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, - ]); - }).then(() => { - return this.store().find('taco-soup', 'D'); - }).then((found) => { - assert.equal(found.get('id'), 'D', - 'should have found the requested item'); - assert.deepEqual(found.get('flavor'), 'black bean', - 'should have extracted the attributes also'); - }).finally(done); -}); + test('can find one', function (assert) { + assert.expect(2); -test('can query with sort', function (assert) { - assert.expect(3); - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().createIndex({ index: { - fields: ['data.name'] } - }).then(() => { - return this.db().bulkDocs([ - { _id: 'smasher_2_mario', data: { name: 'Mario', series: 'Mario', debut: 1981 }}, - { _id: 'smasher_2_puff', data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }}, - { _id: 'smasher_2_link', data: { name: 'Link', series: 'Zelda', debut: 1986 }}, - { _id: 'smasher_2_dk', data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }}, - { _id: 'smasher_2_pika', data: { name: 'Pikachu', series: 'Pokemon', _id: 'pikachu', debut: 1996 }} - ]); - }); - }).then(() => { - return this.store().query('smasher', { - filter: {name: {$gt: ''}}, - sort: ['name'] + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, + ]); + }) + .then(() => { + return this.store().find('taco-soup', 'D'); + }) + .then((found) => { + assert.strictEqual( + found.get('id'), + 'D', + 'should have found the requested item' + ); + assert.deepEqual( + found.get('flavor'), + 'black bean', + 'should have extracted the attributes also' + ); + }) + .finally(done); }); - }).then((found) => { - assert.equal(found.get('length'), 5, 'should returns all the smashers '); - assert.deepEqual(found.mapBy('id'), ['dk','puff','link','mario','pika'], - 'should have extracted the IDs correctly'); - assert.deepEqual(found.mapBy('name'), ['Donkey Kong', 'Jigglypuff', 'Link', 'Mario','Pikachu'], - 'should have extracted the attributes also'); - }).finally(done); -}); -test('can query multi-field queries', function (assert) { - assert.expect(3); - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().createIndex({ index: { - fields: ['data.series', 'data.debut'] } - }).then(() => { - return this.db().bulkDocs([ - { _id: 'smasher_2_mario', data: { name: 'Mario', series: 'Mario', debut: 1981 }}, - { _id: 'smasher_2_puff', data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }}, - { _id: 'smasher_2_link', data: { name: 'Link', series: 'Zelda', debut: 1986 }}, - { _id: 'smasher_2_dk', data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }}, - { _id: 'smasher_2_pika', data: { name: 'Pikachu', series: 'Pokemon', _id: 'pikachu', debut: 1996 }} - ]); - }); - }).then(() => { - return this.store().query('smasher', { - filter: {series: 'Mario' }, - sort: [ - {series: 'desc'}, - {debut: 'desc'}] + test('can query with sort', function (assert) { + assert.expect(3); + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db() + .createIndex({ + index: { + fields: ['data.name'], + }, + }) + .then(() => { + return this.db().bulkDocs([ + { + _id: 'smasher_2_mario', + data: { name: 'Mario', series: 'Mario', debut: 1981 }, + }, + { + _id: 'smasher_2_puff', + data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }, + }, + { + _id: 'smasher_2_link', + data: { name: 'Link', series: 'Zelda', debut: 1986 }, + }, + { + _id: 'smasher_2_dk', + data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }, + }, + { + _id: 'smasher_2_pika', + data: { + name: 'Pikachu', + series: 'Pokemon', + _id: 'pikachu', + debut: 1996, + }, + }, + ]); + }); + }) + .then(() => { + return this.store().query('smasher', { + filter: { name: { $gt: '' } }, + sort: ['name'], + }); + }) + .then((found) => { + assert.strictEqual( + found.get('length'), + 5, + 'should returns all the smashers ' + ); + assert.deepEqual( + found.mapBy('id'), + ['dk', 'puff', 'link', 'mario', 'pika'], + 'should have extracted the IDs correctly' + ); + assert.deepEqual( + found.mapBy('name'), + ['Donkey Kong', 'Jigglypuff', 'Link', 'Mario', 'Pikachu'], + 'should have extracted the attributes also' + ); + }) + .finally(done); }); - }).then((found) => { - assert.equal(found.get('length'), 2, 'should have found the two smashers'); - assert.deepEqual(found.mapBy('id'), ['mario', 'dk'], - 'should have extracted the IDs correctly'); - assert.deepEqual(found.mapBy('name'), ['Mario', 'Donkey Kong'], - 'should have extracted the attributes also'); - }).finally(done); -}); -test('queryRecord returns null when no record is found', function (assert) { - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().createIndex({ index: { - fields: ['data.flavor'] } - }).then(() => { - return this.db().bulkDocs([ - { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor', ingredients: ['X', 'Y'] } }, - { _id: 'tacoSoup_2_D', data: { flavor: 'black bean', ingredients: ['Z'] } }, - { _id: 'foodItem_2_X', data: { name: 'pineapple' }}, - { _id: 'foodItem_2_Y', data: { name: 'pork loin' }}, - { _id: 'foodItem_2_Z', data: { name: 'black beans' }} - ]); + test('can query multi-field queries', function (assert) { + assert.expect(3); + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db() + .createIndex({ + index: { + fields: ['data.series', 'data.debut'], + }, + }) + .then(() => { + return this.db().bulkDocs([ + { + _id: 'smasher_2_mario', + data: { name: 'Mario', series: 'Mario', debut: 1981 }, + }, + { + _id: 'smasher_2_puff', + data: { name: 'Jigglypuff', series: 'Pokemon', debut: 1996 }, + }, + { + _id: 'smasher_2_link', + data: { name: 'Link', series: 'Zelda', debut: 1986 }, + }, + { + _id: 'smasher_2_dk', + data: { name: 'Donkey Kong', series: 'Mario', debut: 1981 }, + }, + { + _id: 'smasher_2_pika', + data: { + name: 'Pikachu', + series: 'Pokemon', + _id: 'pikachu', + debut: 1996, + }, + }, + ]); + }); + }) + .then(() => { + return this.store().query('smasher', { + filter: { series: 'Mario' }, + sort: [{ series: 'desc' }, { debut: 'desc' }], + }); + }) + .then((found) => { + assert.strictEqual( + found.get('length'), + 2, + 'should have found the two smashers' + ); + assert.deepEqual( + found.mapBy('id'), + ['mario', 'dk'], + 'should have extracted the IDs correctly' + ); + assert.deepEqual( + found.mapBy('name'), + ['Mario', 'Donkey Kong'], + 'should have extracted the attributes also' + ); + }) + .finally(done); }); - }).then(() => { - return this.store().queryRecord('taco-soup', { - filter: {flavor: 'all pastor' } - }); - }).then((found) => { - assert.equal(found, null, 'should be null'); - done(); - }).catch((error) => { - assert.ok(false, 'error in test:' + error); - done(); - }); -}); -test('can query one record', function (assert) { - assert.expect(1); + test('queryRecord returns null when no record is found', function (assert) { + assert.expect(1); + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db() + .createIndex({ + index: { + fields: ['data.flavor'], + }, + }) + .then(() => { + return this.db().bulkDocs([ + { + _id: 'tacoSoup_2_C', + data: { flavor: 'al pastor', ingredients: ['X', 'Y'] }, + }, + { + _id: 'tacoSoup_2_D', + data: { flavor: 'black bean', ingredients: ['Z'] }, + }, + { _id: 'foodItem_2_X', data: { name: 'pineapple' } }, + { _id: 'foodItem_2_Y', data: { name: 'pork loin' } }, + { _id: 'foodItem_2_Z', data: { name: 'black beans' } }, + ]); + }); + }) + .then(() => { + return this.store().queryRecord('taco-soup', { + filter: { flavor: 'all pastor' }, + }); + }) + .then((found) => { + assert.strictEqual(found, null, 'should be null'); + done(); + }) + .catch((error) => { + assert.ok(false, 'error in test:' + error); + done(); + }); + }); - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().createIndex({ index: { - fields: ['data.flavor'] } - }).then(() => { - return this.db().bulkDocs(getDocsForRelations()); + test('can query one record', function (assert) { + assert.expect(1); + + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db() + .createIndex({ + index: { + fields: ['data.flavor'], + }, + }) + .then(() => { + return this.db().bulkDocs(getDocsForRelations()); + }); + }) + .then(() => { + return this.store().queryRecord('taco-soup', { + filter: { flavor: 'al pastor' }, + }); + }) + .then((found) => { + assert.strictEqual( + found.get('flavor'), + 'al pastor', + 'should have found the requested item' + ); + }) + .finally(done); }); - }).then(() => { - return this.store().queryRecord('taco-soup', { - filter: {flavor: 'al pastor' } + + test('can query one associated records', function (assert) { + assert.expect(3); + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db() + .createIndex({ + index: { + fields: ['data.flavor'], + }, + }) + .then(() => { + return this.db().bulkDocs(getDocsForRelations()); + }); + }) + .then(() => { + return this.store().queryRecord('taco-soup', { + filter: { flavor: 'al pastor' }, + }); + }) + .then((found) => { + assert.strictEqual( + found.get('flavor'), + 'al pastor', + 'should have found the requested item' + ); + return found.get('ingredients'); + }) + .then((foundIngredients) => { + assert.deepEqual( + foundIngredients.mapBy('id'), + ['X', 'Y'], + 'should have found both associated items' + ); + assert.deepEqual( + foundIngredients.mapBy('name'), + ['pineapple', 'pork loin'], + 'should have fully loaded the associated items' + ); + }) + .finally(done); }); - }).then((found) => { - assert.equal(found.get('flavor'), 'al pastor', - 'should have found the requested item'); - }).finally(done); -}); -test('can query one associated records', function (assert) { - assert.expect(3); - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().createIndex({ index: { - fields: ['data.flavor'] } - }).then(() => { - return this.db().bulkDocs(getDocsForRelations()); + test('can find associated records', function (assert) { + assert.expect(3); + + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs(getDocsForRelations()); + }) + .then(() => { + return this.store().find('taco-soup', 'C'); + }) + .then((found) => { + assert.strictEqual( + found.get('id'), + 'C', + 'should have found the requested item' + ); + return found.get('ingredients'); + }) + .then((foundIngredients) => { + assert.deepEqual( + foundIngredients.mapBy('id'), + ['X', 'Y'], + 'should have found both associated items' + ); + assert.deepEqual( + foundIngredients.mapBy('name'), + ['pineapple', 'pork loin'], + 'should have fully loaded the associated items' + ); + }) + .finally(done); }); - }).then(() => { - return this.store().queryRecord('taco-soup', { - filter: {flavor: 'al pastor' }}); - }).then((found) => { - assert.equal(found.get('flavor'), 'al pastor', - 'should have found the requested item'); - return found.get('ingredients'); - }).then((foundIngredients) => { - assert.deepEqual(foundIngredients.mapBy('id'), ['X', 'Y'], - 'should have found both associated items'); - assert.deepEqual(foundIngredients.mapBy('name'), ['pineapple', 'pork loin'], - 'should have fully loaded the associated items'); - }).finally(done); -}); -test('can find associated records', function (assert) { - assert.expect(3); - - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs(getDocsForRelations()); - }).then(() => { - return this.store().find('taco-soup', 'C'); - }).then((found) => { - assert.equal(found.get('id'), 'C', - 'should have found the requested item'); - return found.get('ingredients'); - }).then((foundIngredients) => { - assert.deepEqual(foundIngredients.mapBy('id'), ['X', 'Y'], - 'should have found both associated items'); - assert.deepEqual(foundIngredients.mapBy('name'), ['pineapple', 'pork loin'], - 'should have fully loaded the associated items'); - }).finally(done); -}); + test('create a new record', function (assert) { + assert.expect(2); -test('create a new record', function (assert) { - assert.expect(2); + var done = assert.async(); + Promise.resolve() + .then(() => { + var newSoup = this.store().createRecord('taco-soup', { + id: 'E', + flavor: 'balsamic', + }); + return newSoup.save(); + }) + .then(() => { + return this.db().get('tacoSoup_2_E'); + }) + .then((newDoc) => { + assert.strictEqual( + newDoc.data.flavor, + 'balsamic', + 'should have saved the attribute' + ); + + var recordInStore = this.store().peekRecord('tacoSoup', 'E'); + assert.strictEqual( + newDoc._rev, + recordInStore.get('rev'), + 'should have associated the ember-data record with the rev for the new record' + ); + }) + .finally(done); + }); - var done = assert.async(); - Promise.resolve().then(() => { - var newSoup = this.store().createRecord('taco-soup', { id: 'E', flavor: 'balsamic' }); - return newSoup.save(); - }).then(() => { - return this.db().get('tacoSoup_2_E'); - }).then((newDoc) => { - assert.equal(newDoc.data.flavor, 'balsamic', 'should have saved the attribute'); + test('creating an associated record stores a reference to it in the parent', function (assert) { + assert.expect(1); + + var done = assert.async(); + Promise.resolve() + .then(() => { + var s = { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }; + if (savingHasMany()) { + s.data.ingredients = []; + } + return this.db().bulkDocs([s]); + }) + .then(() => { + return this.store().findRecord('taco-soup', 'C'); + }) + .then((tacoSoup) => { + var newIngredient = this.store().createRecord('food-item', { + name: 'pineapple', + soup: tacoSoup, + }); + + //tacoSoup.save() actually not needed in !savingHasmany mode, but should still work + return newIngredient + .save() + .then(() => (savingHasMany() ? tacoSoup.save() : tacoSoup)); + }) + .then(() => { + run(() => this.store().unloadAll()); + return this.store().findRecord('taco-soup', 'C'); + }) + .then((tacoSoup) => { + return tacoSoup.get('ingredients'); + }) + .then((foundIngredients) => { + assert.deepEqual( + foundIngredients.mapBy('name'), + ['pineapple'], + 'should have fully loaded the associated items' + ); + }) + .finally(done); + }); - var recordInStore = this.store().peekRecord('tacoSoup', 'E'); - assert.equal(newDoc._rev, recordInStore.get('rev'), - 'should have associated the ember-data record with the rev for the new record'); + // This test fails due to a bug in ember data + // (https://github.com/emberjs/data/issues/3736) + // starting with ED v2.0.0-beta.1. It works again with ED v2.1.0. + // if (!DS.VERSION.match(/^2\.0/)) { + test('update an existing record', function (assert) { + assert.expect(2); - }).finally(done); -}); + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, + ]); + }) + .then(() => { + return this.store().find('taco-soup', 'C'); + }) + .then((found) => { + found.set('flavor', 'pork'); + return found.save(); + }) + .then(() => { + return this.db().get('tacoSoup_2_C'); + }) + .then((updatedDoc) => { + assert.strictEqual( + updatedDoc.data.flavor, + 'pork', + 'should have updated the attribute' + ); + + var recordInStore = this.store().peekRecord('tacoSoup', 'C'); + assert.strictEqual( + updatedDoc._rev, + recordInStore.get('rev'), + 'should have associated the ember-data record with the updated rev' + ); + }) + .finally(done); + }); + // } + + test('delete an existing record', function (assert) { + assert.expect(1); + + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, + { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, + ]); + }) + .then(() => { + return this.store().find('taco-soup', 'C'); + }) + .then((found) => { + return found.destroyRecord(); + }) + .then(() => { + return this.db().get('tacoSoup_2_C'); + }) + .then( + (doc) => { + assert.notOk(doc, 'document should no longer exist'); + }, + (result) => { + assert.strictEqual( + result.status, + 404, + 'document should no longer exist' + ); + } + ) + .finally(done); + }); + }; + + let asyncTests = function () { + test('eventually consistency - success', function (assert) { + assert.expect(1); + assert.timeout(5000); + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs([ + { _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' } }, + //{_id: 'tacoSoup_2_C', data: { flavor: 'test' } } + ]); + }) + .then(() => this.store().findRecord('food-item', 'X')) + .then((foodItem) => { + let result = [ + foodItem + .get('soup') + .then((soup) => assert.strictEqual(soup.id, 'C')), + + promiseToRunLater(0).then(() => { + return this.db().bulkDocs([ + { _id: 'tacoSoup_2_C', data: { flavor: 'test' } }, + ]); + }), + ]; + + return all(result); + }) + .finally(done); + }); -test('creating an associated record stores a reference to it in the parent', function (assert) { - assert.expect(1); - - var done = assert.async(); - Promise.resolve().then(() => { - var s = { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor'} }; - if (savingHasMany()) { - s.data.ingredients = []; - } - return this.db().bulkDocs([ - s - ]); - }).then(() => { - return this.store().findRecord('taco-soup', 'C'); - }).then(tacoSoup => { - var newIngredient = this.store().createRecord('food-item', { - name: 'pineapple', - soup: tacoSoup + test('eventually consistency - deleted', function (assert) { + assert.expect(1); + assert.timeout(5000); + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs([ + { _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' } }, + //{_id: 'tacoSoup_2_C', data: { flavor: 'test' } } + ]); + }) + .then(() => this.store().findRecord('food-item', 'X')) + .then((foodItem) => { + let result = [ + foodItem + .get('soup') + .then((soup) => assert.strictEqual(soup, null, 'isDeleted')) + .catch(() => assert.ok(true, 'isDeleted')), + + promiseToRunLater(100).then(() => + this.db().bulkDocs([{ _id: 'tacoSoup_2_C', _deleted: true }]) + ), + ]; + + return all(result); + }) + .finally(done); }); - //tacoSoup.save() actually not needed in !savingHasmany mode, but should still work - return newIngredient.save().then(() => savingHasMany() ? tacoSoup.save() : tacoSoup); - }).then(() => { - run(() => this.store().unloadAll()); - return this.store().findRecord('taco-soup', 'C'); - }).then(tacoSoup => { - return tacoSoup.get('ingredients'); - }).then(foundIngredients => { - assert.deepEqual(foundIngredients.mapBy('name'), ['pineapple'], - 'should have fully loaded the associated items'); - }).finally(done); -}); + test('_init should work', function (assert) { + let db = this.db(); -// This test fails due to a bug in ember data -// (https://github.com/emberjs/data/issues/3736) -// starting with ED v2.0.0-beta.1. It works again with ED v2.1.0. -if (!DS.VERSION.match(/^2\.0/)) { - test('update an existing record', function (assert) { - assert.expect(2); - - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs([ - { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, - { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, - ]); - }).then(() => { - return this.store().find('taco-soup', 'C'); - }).then((found) => { - found.set('flavor', 'pork'); - return found.save(); - }).then(() => { - return this.db().get('tacoSoup_2_C'); - }).then((updatedDoc) => { - assert.equal(updatedDoc.data.flavor, 'pork', 'should have updated the attribute'); - - var recordInStore = this.store().peekRecord('tacoSoup', 'C'); - assert.equal(updatedDoc._rev, recordInStore.get('rev'), - 'should have associated the ember-data record with the updated rev'); - - }).finally(done); - }); -} + assert.strictEqual(db.rel, undefined, 'should start without schema'); -test('delete an existing record', function (assert) { - assert.expect(1); - - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs([ - { _id: 'tacoSoup_2_C', data: { flavor: 'al pastor' } }, - { _id: 'tacoSoup_2_D', data: { flavor: 'black bean' } }, - ]); - }).then(() => { - return this.store().find('taco-soup', 'C'); - }).then((found) => { - return found.destroyRecord(); - }).then(() => { - return this.db().get('tacoSoup_2_C'); - }).then((doc) => { - assert.ok(!doc, 'document should no longer exist'); - }, (result) => { - assert.equal(result.status, 404, 'document should no longer exist'); - }).finally(done); -}); + let promises = []; -}; - -let asyncTests = function() { - -test('eventually consistency - success', function (assert) { - assert.timeout(5000); - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs([ - { _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' }}, - //{_id: 'tacoSoup_2_C', data: { flavor: 'test' } } - ]); - }) - .then(() => this.store().findRecord('food-item', 'X')) - .then(foodItem => { - let result = [ - foodItem.get('soup') - .then(soup => assert.equal(soup.id, 'C')), - - promiseToRunLater(0) - .then(() => { - return this.db().bulkDocs([ - {_id: 'tacoSoup_2_C', data: { flavor: 'test' } } - ]);}), - ]; - - return all(result); - }) - .finally(done); -}); + let adapter = this.adapter(); + promises.push( + adapter._init(this.store(), this.store().modelFor('taco-soup')) + ); -test('eventually consistency - deleted', function (assert) { - assert.timeout(5000); - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs([ - { _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'C' }}, - //{_id: 'tacoSoup_2_C', data: { flavor: 'test' } } - ]); - }) - .then(() => this.store().findRecord('food-item', 'X')) - .then(foodItem => { - let result = [ - foodItem.get('soup') - .then((soup) => assert.ok(soup === null, 'isDeleted')) - .catch(() => assert.ok(true, 'isDeleted')), - - promiseToRunLater(100) - .then(() => this.db().bulkDocs([ - {_id: 'tacoSoup_2_C', _deleted: true } - ])), - ]; - - return all(result); - }) - .finally(done); -}); + //this tests _init synchronously by design, as re-entry and infitinite loop detection works this way + assert.notEqual(db.rel, undefined, '_init should set schema'); + assert.strictEqual( + this.adapter()._schema.length, + 2, + 'should have set all relationships on the schema' + ); -test('_init should work', function (assert) { - let db = this.db(); - - assert.ok(db.rel == undefined, "should start without schema"); - - let promises = []; - - let adapter = this.adapter(); - promises.push(adapter._init(this.store(), this.store().modelFor('taco-soup'))); - -//this tests _init synchronously by design, as re-entry and infitinite loop detection works this way - assert.ok(db.rel != undefined, "_init should set schema"); - assert.equal(this.adapter()._schema.length, 2, "should have set all relationships on the schema"); - - promises.push(adapter._init(this.store(), this.store().modelFor('taco-soup'))); - - return all(promises); -}); + promises.push( + adapter._init(this.store(), this.store().modelFor('taco-soup')) + ); -//TODO: only do this for async or dontsavehasmany? -test('delete cascade null', function (assert) { - assert.timeout(5000); - assert.expect(2); - - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs(getDocsForRelations()); - }) -// .then(() => this.store().findRecord('food-item', 'Z'))//prime ember-data store with Z -// .then(found => found.get('soup'))//prime belongsTo - .then(() => this.store().findRecord('taco-soup', 'D')) - .then((found) => { - return found.destroyRecord(); - }).then(() => { - run(() => this.store().unloadAll()); // normally this would be done by onChange listener - return this.store().findRecord('food-item', 'Z');//Z should be updated now - }) - .then((found) => { - return Promise.resolve(found.get('soup')).catch(() => null).then((soup) => { - assert.ok(!found.belongsTo || found.belongsTo('soup').value() === null, - 'should set value of belongsTo to null'); - return soup; + return all(promises); }); - }).then((soup) => { - assert.ok(soup === null, - 'deleted soup should have cascaded to a null value for the belongsTo'); - }).finally(done); -}); -test('remote delete removes belongsTo relationship', function (assert) { - assert.timeout(5000); - assert.expect(2); - - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs(getDocsForRelations()); - }) - .then(() => this.store().findRecord('food-item', 'Z'))//prime ember-data store with Z - .then(found => found.get('soup'))//prime belongsTo - .then((found) => { - let id = "tacoSoup_2_" + found.id; - let promise = this.adapter().waitForChangeWithID(id); - - this.db().remove(id, found.get('rev')); - - return promise; - }).then(() => { - return this.store().findRecord('food-item', 'Z');//Z should be updated now - }) - .then((found) => { - return Promise.resolve(found.get('soup')).catch(() => null).then((soup) => { - assert.ok(!found.belongsTo || found.belongsTo('soup').value() === null, - 'should set value of belongsTo to null'); - return soup; - }); - }).then((soup) => { - assert.ok(soup === null, - 'deleted soup should have cascaded to a null value for the belongsTo'); - }).finally(done); -}); + //TODO: only do this for async or dontsavehasmany? + test('delete cascade null', function (assert) { + assert.timeout(5000); + assert.expect(2); -test('remote delete removes hasMany relationship', function (assert) { - assert.timeout(5000); - assert.expect(3); - - let liveIngredients = null; - - var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs(getDocsForRelations()); - }) - .then(() => this.store().findRecord('taco-soup', 'C'))//prime ember-data store with C - .then(found => found.get('ingredients'))//prime hasMany - .then((ingredients) => { - liveIngredients = ingredients;//save for later - - assert.equal(ingredients.length, 2, "should be 2 food items initially"); - - let itemToDelete = ingredients.toArray()[0]; - let id = "foodItem_2_" + itemToDelete.id; - let promise = this.adapter().waitForChangeWithID(id); - - this.db().remove(id, itemToDelete.get('rev')); - - return promise; - }).then(() => { - return this.store().findRecord('taco-soup', 'C');//get updated soup.ingredients - }) - .then(found => found.get('ingredients')) - .then((ingredients) => { - assert.equal( ingredients.length, 1, "1 food item should be removed from the relationship"); - assert.equal(liveIngredients.length, 1, "1 food item should be removed from the live relationship"); - }).finally(done); -}); + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs(getDocsForRelations()); + }) + // .then(() => this.store().findRecord('food-item', 'Z'))//prime ember-data store with Z + // .then(found => found.get('soup'))//prime belongsTo + .then(() => this.store().findRecord('taco-soup', 'D')) + .then((found) => { + return found.destroyRecord(); + }) + .then(() => { + run(() => this.store().unloadAll()); // normally this would be done by onChange listener + return this.store().findRecord('food-item', 'Z'); //Z should be updated now + }) + .then((found) => { + return Promise.resolve(found.get('soup')) + .catch(() => null) + .then((soup) => { + assert.ok( + !found.belongsTo || found.belongsTo('soup').value() === null, + 'should set value of belongsTo to null' + ); + return soup; + }); + }) + .then((soup) => { + assert.ok( + soup === null, + 'deleted soup should have cascaded to a null value for the belongsTo' + ); + }) + .finally(done); + }); -module('not eventually consistent', { beforeEach: function() { - config.emberPouch.eventuallyConsistent = false; - }, - afterEach: function() { - config.emberPouch.eventuallyConsistent = true; - } - }, function() { - test('not found', function (assert) { + test('remote delete removes belongsTo relationship', function (assert) { + assert.timeout(5000); assert.expect(2); - assert.ok(config.emberPouch.eventuallyConsistent == false, 'eventuallyConsistent is false'); - let done = assert.async(); - - Promise.resolve().then(() => this.store().findRecord('food-item', 'non-existent') - .then(() => assert.ok(false)) - .catch(() => { - assert.ok(true, 'item is not found'); - done(); - })); - }); -}); -}; - - let syncAsync = function() { - module('async', { - beforeEach: function() { - config.emberPouch.async = true; - } - }, () => { allTests(); asyncTests(); }); - module('sync', { - beforeEach: function() { - config.emberPouch.async = false; - } - }, allTests); - }; - - module('dont save hasMany', { - beforeEach: function() { - config.emberPouch.saveHasMany = false; - } - }, syncAsync); - - module('save hasMany', { - beforeEach: function() { - config.emberPouch.saveHasMany = true; - } - }, syncAsync); + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs(getDocsForRelations()); + }) + .then(() => this.store().findRecord('food-item', 'Z')) //prime ember-data store with Z + .then((found) => found.get('soup')) //prime belongsTo + .then((found) => { + let id = 'tacoSoup_2_' + found.id; + let promise = this.adapter().waitForChangeWithID(id); + + this.db().remove(id, found.get('rev')); + + return promise; + }) + .then(() => { + return this.store().findRecord('food-item', 'Z'); //Z should be updated now + }) + .then((found) => { + return Promise.resolve(found.get('soup')) + .catch(() => null) + .then((soup) => { + assert.ok( + !found.belongsTo || found.belongsTo('soup').value() === null, + 'should set value of belongsTo to null' + ); + return soup; + }); + }) + .then((soup) => { + assert.ok( + soup === null, + 'deleted soup should have cascaded to a null value for the belongsTo' + ); + }) + .finally(done); + }); + + test('remote delete removes hasMany relationship', function (assert) { + assert.timeout(5000); + assert.expect(3); + + let liveIngredients = null; + + var done = assert.async(); + Promise.resolve() + .then(() => { + return this.db().bulkDocs(getDocsForRelations()); + }) + .then(() => this.store().findRecord('taco-soup', 'C')) //prime ember-data store with C + .then((found) => found.get('ingredients')) //prime hasMany + .then((ingredients) => { + liveIngredients = ingredients; //save for later + + assert.strictEqual( + ingredients.length, + 2, + 'should be 2 food items initially' + ); + + let itemToDelete = ingredients.toArray()[0]; + let id = 'foodItem_2_' + itemToDelete.id; + let promise = this.adapter().waitForChangeWithID(id); + + this.db().remove(id, itemToDelete.get('rev')); + + return promise; + }) + .then(() => { + return this.store().findRecord('taco-soup', 'C'); //get updated soup.ingredients + }) + .then((found) => found.get('ingredients')) + .then((ingredients) => { + assert.strictEqual( + ingredients.length, + 1, + '1 food item should be removed from the relationship' + ); + assert.strictEqual( + liveIngredients.length, + 1, + '1 food item should be removed from the live relationship' + ); + }) + .finally(done); + }); + + module( + 'not eventually consistent', + { + beforeEach: function () { + config.emberPouch.eventuallyConsistent = false; + }, + afterEach: function () { + config.emberPouch.eventuallyConsistent = true; + }, + }, + function () { + test('not found', function (assert) { + assert.expect(2); + assert.false( + config.emberPouch.eventuallyConsistent, + 'eventuallyConsistent is false' + ); + let done = assert.async(); + + Promise.resolve().then(() => + this.store() + .findRecord('food-item', 'non-existent') + .then(() => assert.ok(false)) + .catch(() => { + assert.ok(true, 'item is not found'); + done(); + }) + ); + }); + } + ); + }; + + let syncAsync = function () { + module( + 'async', + { + beforeEach: function () { + config.emberPouch.async = true; + }, + }, + () => { + allTests(); + asyncTests(); + } + ); + module( + 'sync', + { + beforeEach: function () { + config.emberPouch.async = false; + }, + }, + allTests + ); + }; + + module( + 'dont save hasMany', + { + beforeEach: function () { + config.emberPouch.saveHasMany = false; + }, + }, + syncAsync + ); + + module( + 'save hasMany', + { + beforeEach: function () { + config.emberPouch.saveHasMany = true; + }, + }, + syncAsync + ); }); diff --git a/tests/integration/adapters/pouch-default-change-watcher-test.js b/tests/integration/adapters/pouch-default-change-watcher-test.js index 2e8826e..d80ab65 100644 --- a/tests/integration/adapters/pouch-default-change-watcher-test.js +++ b/tests/integration/adapters/pouch-default-change-watcher-test.js @@ -17,191 +17,275 @@ function promiseToRunLater(callback, timeout) { }); } -module('Integration | Adapter | Default Change Watcher', function(hooks) { +module('Integration | Adapter | Default Change Watcher', function (hooks) { setupTest(hooks); moduleForIntegration(hooks); - - hooks.beforeEach(function(assert) { + + hooks.beforeEach(function (assert) { var done = assert.async(); - Promise.resolve().then(() => { - return this.db().bulkDocs([ - { _id: 'tacoSoup_2_A', data: { flavor: 'al pastor', ingredients: ['X', 'Y'] } }, - { _id: 'tacoSoup_2_B', data: { flavor: 'black bean', ingredients: ['Z'] } }, - { _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'A' } }, - { _id: 'foodItem_2_Y', data: { name: 'pork loin', soup: 'A' } }, - { _id: 'foodItem_2_Z', data: { name: 'black beans', soup: 'B' } } - ]); - }).finally(done); + Promise.resolve() + .then(() => { + return this.db().bulkDocs([ + { + _id: 'tacoSoup_2_A', + data: { flavor: 'al pastor', ingredients: ['X', 'Y'] }, + }, + { + _id: 'tacoSoup_2_B', + data: { flavor: 'black bean', ingredients: ['Z'] }, + }, + { _id: 'foodItem_2_X', data: { name: 'pineapple', soup: 'A' } }, + { _id: 'foodItem_2_Y', data: { name: 'pork loin', soup: 'A' } }, + { _id: 'foodItem_2_Z', data: { name: 'black beans', soup: 'B' } }, + ]); + }) + .finally(done); }); -test('a loaded instance automatically reflects directly-made database changes', function (assert) { - assert.expect(2); - var done = assert.async(); - - resolve().then(() => { - return this.store().find('taco-soup', 'B'); - }).then((soupB) => { - assert.equal('black bean', soupB.get('flavor'), - 'the loaded instance should reflect the initial test data'); - - return this.db().get('tacoSoup_2_B'); - }).then((soupBRecord) => { - soupBRecord.data.flavor = 'carnitas'; - return this.db().put(soupBRecord); - }).then(() => { - return promiseToRunLater(() => { - var alreadyLoadedSoupB = this.store().peekRecord('taco-soup', 'B'); - assert.equal(alreadyLoadedSoupB.get('flavor'), 'carnitas', - 'the loaded instance should automatically reflect the change in the database'); - }, 100); - }).finally(done); -}); + test('a loaded instance automatically reflects directly-made database changes', function (assert) { + assert.expect(2); + var done = assert.async(); -test('a record that is not loaded stays not loaded when it is changed', function (assert) { - assert.expect(2); - var done = assert.async(); - - resolve().then(() => { - assert.equal(null, this.store().peekRecord('taco-soup', 'A'), - 'test setup: record should not be loaded already'); - - return this.db().get('tacoSoup_2_A'); - }).then((soupARecord) => { - soupARecord.data.flavor = 'barbacoa'; - return this.db().put(soupARecord); - }).then(() => { - return promiseToRunLater(() => { - assert.equal(null, this.store().peekRecord('taco-soup', 'A'), - 'the corresponding instance should still not be loaded'); - }, 15); - }).finally(done); -}); + resolve() + .then(() => { + return this.store().find('taco-soup', 'B'); + }) + .then((soupB) => { + assert.strictEqual( + soupB.get('flavor'), + 'black bean', + 'the loaded instance should reflect the initial test data' + ); -test('a new record is not automatically loaded', function (assert) { - assert.expect(2); - var done = assert.async(); + return this.db().get('tacoSoup_2_B'); + }) + .then((soupBRecord) => { + soupBRecord.data.flavor = 'carnitas'; + return this.db().put(soupBRecord); + }) + .then(() => { + return promiseToRunLater(() => { + var alreadyLoadedSoupB = this.store().peekRecord('taco-soup', 'B'); + assert.strictEqual( + alreadyLoadedSoupB.get('flavor'), + 'carnitas', + 'the loaded instance should automatically reflect the change in the database' + ); + }, 100); + }) + .finally(done); + }); - resolve().then(() => { - assert.equal(null, this.store().peekRecord('taco-soup', 'C'), - 'test setup: record should not be loaded already'); + test('a record that is not loaded stays not loaded when it is changed', function (assert) { + assert.expect(2); + var done = assert.async(); - return this.db().put({ - _id: 'tacoSoup_2_C', data: { flavor: 'sofritas' } - }); - }).then(() => { - return promiseToRunLater(() => { - assert.equal(null, this.store().peekRecord('taco-soup', 'C'), - 'the corresponding instance should still not be loaded'); - }, 15); - }).finally(done); -}); + resolve() + .then(() => { + assert.strictEqual( + this.store().peekRecord('taco-soup', 'A'), + null, + 'test setup: record should not be loaded already' + ); -test('a deleted record is automatically marked deleted', function (assert) { - assert.expect(2); - var done = assert.async(); - - let initialRecord = null; - - resolve().then(() => { - return this.store().find('taco-soup', 'B'); - }).then((soupB) => { - initialRecord = soupB; - assert.equal('black bean', soupB.get('flavor'), - 'the loaded instance should reflect the initial test data'); - - return this.db().get('tacoSoup_2_B'); - }).then((soupBRecord) => { - return this.db().remove(soupBRecord); - }).then(() => { - return promiseToRunLater(() => { - assert.ok(initialRecord.get('isDeleted'), - 'the corresponding instance should now be deleted'); - }, 100); - }).finally(done); -}); + return this.db().get('tacoSoup_2_A'); + }) + .then((soupARecord) => { + soupARecord.data.flavor = 'barbacoa'; + return this.db().put(soupARecord); + }) + .then(() => { + return promiseToRunLater(() => { + assert.strictEqual( + this.store().peekRecord('taco-soup', 'A'), + null, + 'the corresponding instance should still not be loaded' + ); + }, 15); + }) + .finally(done); + }); -test('a change to a record with a non-relational-pouch ID does not cause an error', function (assert) { - assert.expect(0); - var done = assert.async(); + test('a new record is not automatically loaded', function (assert) { + assert.expect(2); + var done = assert.async(); - resolve().then(() => { - // do some op to cause relational-pouch to be initialized - return this.store().find('taco-soup', 'B'); - }).then(() => { - return this.db().put({ - _id: '_design/ingredient-use' - }); - }).finally(done); -}); + resolve() + .then(() => { + assert.strictEqual( + this.store().peekRecord('taco-soup', 'C'), + null, + 'test setup: record should not be loaded already' + ); -test('a change to a record of an unknown type does not cause an error', function (assert) { - assert.expect(0); - var done = assert.async(); + return this.db().put({ + _id: 'tacoSoup_2_C', + data: { flavor: 'sofritas' }, + }); + }) + .then(() => { + return promiseToRunLater(() => { + assert.strictEqual( + this.store().peekRecord('taco-soup', 'C'), + null, + 'the corresponding instance should still not be loaded' + ); + }, 15); + }) + .finally(done); + }); - resolve().then(() => { - // do some op to cause relational-pouch to be initialized - return this.store().find('taco-soup', 'B'); - }).then(() => { - return this.db().put({ - _id: 'burritoShake_2_X', data: { consistency: 'chunky' } - }); - }).finally(done); -}); -}); + test('a deleted record is automatically marked deleted', function (assert) { + assert.expect(2); + var done = assert.async(); -module('Integration | Adapter | With unloadedDocumentChanged implementation to load new docs into store', function(hooks) { - setupTest(hooks); - moduleForIntegration(hooks); - - hooks.beforeEach(function(assert) { + let initialRecord = null; + + resolve() + .then(() => { + return this.store().find('taco-soup', 'B'); + }) + .then((soupB) => { + initialRecord = soupB; + assert.strictEqual( + soupB.get('flavor'), + 'black bean', + 'the loaded instance should reflect the initial test data' + ); + return this.db().get('tacoSoup_2_B'); + }) + .then((soupBRecord) => { + return this.db().remove(soupBRecord); + }) + .then(() => { + return promiseToRunLater(() => { + assert.ok( + initialRecord.get('isDeleted'), + 'the corresponding instance should now be deleted ' + ); + }, 100); + }) + .finally(done); + }); + + test('a change to a record with a non-relational-pouch ID does not cause an error', function (assert) { + assert.expect(0); var done = assert.async(); - this.adapter = function adapter() { - return this.store().adapterFor('taco-salad'); - }; - this.db = function db() { - return this.adapter().get('db'); - }; - - Promise.resolve().then(() => { - return this.db().bulkDocs([ - { _id: 'tacoSalad_2_A', data: { flavor: 'al pastor', ingredients: ['X', 'Y'] } }, - { _id: 'tacoSalad_2_B', data: { flavor: 'black bean', ingredients: ['Z'] } }, - { _id: 'foodItem_2_X', data: { name: 'pineapple' } }, - { _id: 'foodItem_2_Y', data: { name: 'pork loin' } }, - { _id: 'foodItem_2_Z', data: { name: 'black beans' } } - ]); - }).finally(done); + + resolve() + .then(() => { + // do some op to cause relational-pouch to be initialized + return this.store().find('taco-soup', 'B'); + }) + .then(() => { + return this.db().put({ + _id: '_design/ingredient-use', + }); + }) + .finally(done); }); - -test('a new record is automatically loaded', function (assert) { - assert.expect(4); - var done = assert.async(); - - resolve().then(() => { - return this.store().find('taco-salad', 'B'); - }).then((soupB) => { - assert.equal('black bean', soupB.get('flavor'), - 'the loaded instance should reflect the initial test data'); - }).then(() => { - assert.equal(null, this.store().peekRecord('taco-salad', 'C'), - 'test setup: record should not be loaded already'); - - return this.db().put({ - _id: 'tacoSalad_2_C', data: { flavor: 'sofritas' } - }); - }).then(() => { - return promiseToRunLater(() => { - var alreadyLoadedSaladC = this.store().peekRecord('taco-salad', 'C'); - assert.ok(alreadyLoadedSaladC, - 'the corresponding instance should now be loaded'); - if (alreadyLoadedSaladC) { - assert.equal(alreadyLoadedSaladC.get('flavor'), 'sofritas', - 'the corresponding instance should now be loaded with the right data'); - } - }, 15); - }).finally(done); -}); + test('a change to a record of an unknown type does not cause an error', function (assert) { + assert.expect(0); + var done = assert.async(); + + resolve() + .then(() => { + // do some op to cause relational-pouch to be initialized + return this.store().find('taco-soup', 'B'); + }) + .then(() => { + return this.db().put({ + _id: 'burritoShake_2_X', + data: { consistency: 'chunky' }, + }); + }) + .finally(done); + }); }); +module( + 'Integration | Adapter | With unloadedDocumentChanged implementation to load new docs into store', + function (hooks) { + setupTest(hooks); + moduleForIntegration(hooks); + + hooks.beforeEach(function (assert) { + var done = assert.async(); + this.adapter = function adapter() { + return this.store().adapterFor('taco-salad'); + }; + this.db = function db() { + return this.adapter().get('db'); + }; + + Promise.resolve() + .then(() => { + return this.db().bulkDocs([ + { + _id: 'tacoSalad_2_A', + data: { flavor: 'al pastor', ingredients: ['X', 'Y'] }, + }, + { + _id: 'tacoSalad_2_B', + data: { flavor: 'black bean', ingredients: ['Z'] }, + }, + { _id: 'foodItem_2_X', data: { name: 'pineapple' } }, + { _id: 'foodItem_2_Y', data: { name: 'pork loin' } }, + { _id: 'foodItem_2_Z', data: { name: 'black beans' } }, + ]); + }) + .finally(done); + }); + + test('a new record is automatically loaded', function (assert) { + assert.expect(4); + var done = assert.async(); + + resolve() + .then(() => { + return this.store().find('taco-salad', 'B'); + }) + .then((soupB) => { + assert.strictEqual( + soupB.get('flavor'), + 'black bean', + 'the loaded instance should reflect the initial test data' + ); + }) + .then(() => { + assert.strictEqual( + this.store().peekRecord('taco-salad', 'C'), + null, + 'test setup: record should not be loaded already' + ); + + return this.db().put({ + _id: 'tacoSalad_2_C', + data: { flavor: 'sofritas' }, + }); + }) + .then(() => { + return promiseToRunLater(() => { + var alreadyLoadedSaladC = this.store().peekRecord( + 'taco-salad', + 'C' + ); + assert.ok( + alreadyLoadedSaladC, + 'the corresponding instance should now be loaded' + ); + //if (alreadyLoadedSaladC) { + assert.strictEqual( + alreadyLoadedSaladC.get('flavor'), + 'sofritas', + 'the corresponding instance should now be loaded with the right data' + ); + //} + }, 15); + }) + .finally(done); + }); + } +); diff --git a/tests/integration/serializers/pouch-test.js b/tests/integration/serializers/pouch-test.js index 952b482..f8fc8b8 100644 --- a/tests/integration/serializers/pouch-test.js +++ b/tests/integration/serializers/pouch-test.js @@ -8,87 +8,104 @@ import moduleForIntegration from '../../helpers/module-for-pouch-acceptance'; * Tests attachments behavior for an app using the ember-pouch serializer. */ -module('Integration | Serializer | Attachments', function(hooks) { - setupTest(hooks); +module('Integration | Serializer | Attachments', function (hooks) { + setupTest(hooks); moduleForIntegration(hooks); -let id = 'E'; -let coverImage = { - name: 'cover.jpg', - content_type: 'image/jpeg', - data: window.btoa('cover.jpg'), - length: 9 -}; -let photo1 = { - name: 'photo-1.jpg', - content_type: 'image/jpeg', - data: window.btoa('photo-1.jpg') -}; -let photo2 = { - name: 'photo-2.jpg', - content_type: 'image/jpeg', - data: window.btoa('photo-2.jpg') -}; + let id = 'E'; + let coverImage = { + name: 'cover.jpg', + content_type: 'image/jpeg', + data: window.btoa('cover.jpg'), + length: 9, + }; + let photo1 = { + name: 'photo-1.jpg', + content_type: 'image/jpeg', + data: window.btoa('photo-1.jpg'), + }; + let photo2 = { + name: 'photo-2.jpg', + content_type: 'image/jpeg', + data: window.btoa('photo-2.jpg'), + }; -test('puts attachments into the `attachments` property when saving', function (assert) { - assert.expect(11); + test('puts attachments into the `attachments` property when saving', function (assert) { + assert.expect(11); - var done = assert.async(); - Promise.resolve().then(() => { - var newRecipe = this.store().createRecord('taco-recipe', { - id, - coverImage: coverImage, - photos: [photo1, photo2] - }); - return newRecipe.save(); - }).then(() => { - return this.db().get('tacoRecipe_2_E'); - }).then((newDoc) => { - function checkAttachment(attachments, fileName, value, message) { - delete attachments[fileName].revpos; - assert.deepEqual(attachments[fileName], value, message); - } - checkAttachment(newDoc._attachments, 'cover.jpg', { - digest: 'md5-SxxZx3KOKxy2X2yyCq9c+Q==', - content_type: 'image/jpeg', - stub: true, - length: 9 - }, 'attachments are placed into the _attachments property of the doc'); - assert.deepEqual(Object.keys(newDoc._attachments).sort(), - [coverImage.name, photo1.name, photo2.name].sort(), - 'all attachments are included in the _attachments property of the doc' - ); - assert.equal('cover_image' in newDoc.data, true, - 'respects the mapping provided by the serializer `attrs`' - ); - assert.deepEqual(newDoc.data.cover_image, { - 'cover.jpg': { - length: 9 - } - }, 'the attribute contains the file name'); - assert.equal(newDoc.data.cover_image['cover.jpg'].length, 9, - 'the attribute contains the length to avoid empty length when File objects are ' + - 'saved and have not been reloaded' - ); - assert.deepEqual(newDoc.data.photo_gallery, { - 'photo-1.jpg': {}, - 'photo-2.jpg': {} - }); + var done = assert.async(); + Promise.resolve() + .then(() => { + var newRecipe = this.store().createRecord('taco-recipe', { + id, + coverImage: coverImage, + photos: [photo1, photo2], + }); + return newRecipe.save(); + }) + .then(() => { + return this.db().get('tacoRecipe_2_E'); + }) + .then((newDoc) => { + function checkAttachment(attachments, fileName, value, message) { + delete attachments[fileName].revpos; + assert.deepEqual(attachments[fileName], value, message); + } + checkAttachment( + newDoc._attachments, + 'cover.jpg', + { + digest: 'md5-SxxZx3KOKxy2X2yyCq9c+Q==', + content_type: 'image/jpeg', + stub: true, + length: 9, + }, + 'attachments are placed into the _attachments property of the doc' + ); + assert.deepEqual( + Object.keys(newDoc._attachments).sort(), + [coverImage.name, photo1.name, photo2.name].sort(), + 'all attachments are included in the _attachments property of the doc' + ); + assert.true( + 'cover_image' in newDoc.data, + 'respects the mapping provided by the serializer `attrs`' + ); + assert.deepEqual( + newDoc.data.cover_image, + { + 'cover.jpg': { + length: 9, + }, + }, + 'the attribute contains the file name' + ); + assert.strictEqual( + newDoc.data.cover_image['cover.jpg'].length, + 9, + 'the attribute contains the length to avoid empty length when File objects are ' + + 'saved and have not been reloaded' + ); + assert.deepEqual(newDoc.data.photo_gallery, { + 'photo-1.jpg': {}, + 'photo-2.jpg': {}, + }); - var recordInStore = this.store().peekRecord('tacoRecipe', 'E'); - let coverAttr = recordInStore.get('coverImage'); - assert.equal(coverAttr.get('name'), coverImage.name); - assert.equal(coverAttr.get('data'), coverImage.data); + var recordInStore = this.store().peekRecord('tacoRecipe', 'E'); + let coverAttr = recordInStore.get('coverImage'); + assert.strictEqual(coverAttr.get('name'), coverImage.name); + assert.strictEqual(coverAttr.get('data'), coverImage.data); - let photosAttr = recordInStore.get('photos'); - assert.equal(photosAttr.length, 2, '2 photos'); - assert.equal(photosAttr[0].get('name'), photo1.name); - assert.equal(photosAttr[0].get('data'), photo1.data); + let photosAttr = recordInStore.get('photos'); + assert.strictEqual(photosAttr.length, 2, '2 photos'); + assert.strictEqual(photosAttr[0].get('name'), photo1.name); + assert.strictEqual(photosAttr[0].get('data'), photo1.data); - done(); - }).catch((error) => { - assert.ok(false, 'error in test:' + error); - done(); + done(); + }) + .catch((error) => { + assert.ok(false, 'error in test:' + error); + done(); + }); }); }); -}); diff --git a/tests/test-helper.js b/tests/test-helper.js index 93797ec..4efd6e5 100644 --- a/tests/test-helper.js +++ b/tests/test-helper.js @@ -1,8 +1,12 @@ -import Application from '../app'; -import config from '../config/environment'; +import Application from 'dummy/app'; +import config from 'dummy/config/environment'; +import * as QUnit from 'qunit'; import { setApplication } from '@ember/test-helpers'; +import { setup } from 'qunit-dom'; import { start } from 'ember-qunit'; setApplication(Application.create(config.APP)); -start(); \ No newline at end of file +setup(QUnit.assert); + +start(); diff --git a/tests/unit/transforms/attachments-test.js b/tests/unit/transforms/attachments-test.js index 8885323..a85abfd 100644 --- a/tests/unit/transforms/attachments-test.js +++ b/tests/unit/transforms/attachments-test.js @@ -8,14 +8,14 @@ let testSerializedData = { 'hello.txt': { content_type: 'text/plain', data: 'aGVsbG8gd29ybGQ=', - digest: "md5-7mkg+nM0HN26sZkLN8KVSA==" + digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', // CouchDB doesn't add 'length' }, 'stub.txt': { stub: true, content_type: 'text/plain', - digest: "md5-7mkg+nM0HN26sZkLN8KVSA==", - length: 11 + digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', + length: 11, }, }; @@ -24,55 +24,88 @@ let testDeserializedData = [ name: 'hello.txt', content_type: 'text/plain', data: 'aGVsbG8gd29ybGQ=', - digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==' + digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', }), EmberObject.create({ name: 'stub.txt', content_type: 'text/plain', stub: true, digest: 'md5-7mkg+nM0HN26sZkLN8KVSA==', - length: 11 - }) + length: 11, + }), ]; -module('Unit | Transform | attachments', function(hooks) { +module('Unit | Transform | attachments', function (hooks) { setupTest(hooks); - test('it serializes an attachment', function(assert) { + test('it serializes an attachment', function (assert) { let transform = this.owner.lookup('transform:attachments'); - assert.equal(transform.serialize(null), null); - assert.equal(transform.serialize(undefined), null); + assert.strictEqual(transform.serialize(null), null); + assert.strictEqual(transform.serialize(undefined), null); assert.deepEqual(transform.serialize([]), {}); let serializedData = transform.serialize(testDeserializedData); let hello = testDeserializedData[0].get('name'); - assert.equal(hello, 'hello.txt'); - assert.equal(serializedData[hello].content_type, testSerializedData[hello].content_type); - assert.equal(serializedData[hello].data, testSerializedData[hello].data); + assert.strictEqual(hello, 'hello.txt'); + assert.strictEqual( + serializedData[hello].content_type, + testSerializedData[hello].content_type + ); + assert.strictEqual( + serializedData[hello].data, + testSerializedData[hello].data + ); let stub = testDeserializedData[1].get('name'); - assert.equal(stub, 'stub.txt'); - assert.equal(serializedData[stub].content_type, testSerializedData[stub].content_type); - assert.equal(serializedData[stub].stub, true); + assert.strictEqual(stub, 'stub.txt'); + assert.strictEqual( + serializedData[stub].content_type, + testSerializedData[stub].content_type + ); + assert.true(serializedData[stub].stub); }); - test('it deserializes an attachment', function(assert) { + test('it deserializes an attachment', function (assert) { let transform = this.owner.lookup('transform:attachments'); assert.deepEqual(transform.deserialize(null), []); assert.deepEqual(transform.deserialize(undefined), []); let deserializedData = transform.deserialize(testSerializedData); - assert.equal(deserializedData[0].get('name'), testDeserializedData[0].get('name')); - assert.equal(deserializedData[0].get('content_type'), testDeserializedData[0].get('content_type')); - assert.equal(deserializedData[0].get('data'), testDeserializedData[0].get('data')); - assert.equal(deserializedData[0].get('digest'), testDeserializedData[0].get('digest')); + assert.strictEqual( + deserializedData[0].get('name'), + testDeserializedData[0].get('name') + ); + assert.strictEqual( + deserializedData[0].get('content_type'), + testDeserializedData[0].get('content_type') + ); + assert.strictEqual( + deserializedData[0].get('data'), + testDeserializedData[0].get('data') + ); + assert.strictEqual( + deserializedData[0].get('digest'), + testDeserializedData[0].get('digest') + ); - assert.equal(deserializedData[1].get('name'), testDeserializedData[1].get('name')); - assert.equal(deserializedData[1].get('content_type'), testDeserializedData[1].get('content_type')); - assert.equal(deserializedData[1].get('stub'), true); - assert.equal(deserializedData[1].get('digest'), testDeserializedData[1].get('digest')); - assert.equal(deserializedData[1].get('length'), testDeserializedData[1].get('length')); + assert.strictEqual( + deserializedData[1].get('name'), + testDeserializedData[1].get('name') + ); + assert.strictEqual( + deserializedData[1].get('content_type'), + testDeserializedData[1].get('content_type') + ); + assert.true(deserializedData[1].get('stub')); + assert.strictEqual( + deserializedData[1].get('digest'), + testDeserializedData[1].get('digest') + ); + assert.strictEqual( + deserializedData[1].get('length'), + testDeserializedData[1].get('length') + ); }); });