Repository: feathersjs-ecosystem/feathers-vuex Branch: master Commit: 8643bd7b0cd0 Files: 101 Total size: 756.0 KB Directory structure: gitextract_asa44tit/ ├── .babelrc ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── contributing.md │ ├── issue_template.md │ └── pull_request_template.md ├── .gitignore ├── .istanbul.yml ├── .npmignore ├── .travis.yml ├── .vscode/ │ └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs/ │ ├── .vuepress/ │ │ └── config.js │ ├── 2.0-major-release.md │ ├── 3.0-major-release.md │ ├── api-overview.md │ ├── auth-plugin.md │ ├── common-patterns.md │ ├── composition-api.md │ ├── data-components.md │ ├── example-applications.md │ ├── feathers-vuex-forms.md │ ├── feathervuex-in-vuejs3-setup.md │ ├── getting-started.md │ ├── index.md │ ├── mixins.md │ ├── model-classes.md │ ├── nuxt.md │ ├── service-plugin.md │ └── vue-plugin.md ├── mocha.opts ├── notes.old.md ├── package.json ├── src/ │ ├── FeathersVuexCount.ts │ ├── FeathersVuexFind.ts │ ├── FeathersVuexFormWrapper.ts │ ├── FeathersVuexGet.ts │ ├── FeathersVuexInputWrapper.ts │ ├── FeathersVuexPagination.ts │ ├── auth-module/ │ │ ├── auth-module.actions.ts │ │ ├── auth-module.getters.ts │ │ ├── auth-module.mutations.ts │ │ ├── auth-module.state.ts │ │ ├── make-auth-plugin.ts │ │ └── types.ts │ ├── index.ts │ ├── make-find-mixin.ts │ ├── make-get-mixin.ts │ ├── service-module/ │ │ ├── global-clients.ts │ │ ├── global-models.ts │ │ ├── make-base-model.ts │ │ ├── make-service-module.ts │ │ ├── make-service-plugin.ts │ │ ├── service-module.actions.ts │ │ ├── service-module.events.ts │ │ ├── service-module.getters.ts │ │ ├── service-module.mutations.ts │ │ ├── service-module.state.ts │ │ └── types.ts │ ├── useFind.ts │ ├── useGet.ts │ ├── utils.ts │ └── vue-plugin/ │ └── vue-plugin.ts ├── stories/ │ ├── .npmignore │ ├── FeathersVuexFormWrapper.stories.js │ └── FeathersVuexInputWrapper.stories.js ├── test/ │ ├── auth-module/ │ │ ├── actions.test.js │ │ └── auth-module.test.ts │ ├── auth.test.js │ ├── fixtures/ │ │ ├── fake-data.js │ │ ├── feathers-client.js │ │ ├── server.js │ │ ├── store.js │ │ └── todos.js │ ├── index.test.ts │ ├── make-find-mixin.test.ts │ ├── service-module/ │ │ ├── make-service-plugin.test.ts │ │ ├── misconfigured-client.test.ts │ │ ├── model-base.test.ts │ │ ├── model-instance-defaults.test.ts │ │ ├── model-methods.test.ts │ │ ├── model-relationships.test.ts │ │ ├── model-serialize.test.ts │ │ ├── model-temp-ids.test.ts │ │ ├── model-tests.test.ts │ │ ├── service-module.actions.test.ts │ │ ├── service-module.getters.test.ts │ │ ├── service-module.mutations.test.ts │ │ ├── service-module.reinitialization.test.ts │ │ ├── service-module.test.ts │ │ └── types.ts │ ├── test-utils.ts │ ├── use/ │ │ ├── InstrumentComponent.js │ │ ├── find.test.ts │ │ └── get.test.ts │ ├── utils.test.ts │ └── vue-plugin.test.ts ├── tsconfig.json └── tsconfig.test.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "plugins": [ "add-module-exports" ], "presets": [ [ "env", { "modules": false } ], "es2015", "stage-2" ] } ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: marshallswain ================================================ FILE: .github/contributing.md ================================================ # Contributing to Feathers Thank you for contributing to Feathers! :heart: :tada: This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. To make this easier to manage we currently use [Zenhub](https://www.zenhub.com/) for issue triage and visibility. They have a free browser plugin you can install so that you can see what is in flight at any time, but of course you also always see current issues in Github. ## Report a bug Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. ## Report a Security Concern We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). ## Pull Requests We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** ### Code style Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. ### ES6 compilation Feathers uses [Babel](https://babeljs.io/) to leverage the latest developments of the JavaScript language. All code and samples are currently written in ES2015. To transpile the code in this repository run > npm run compile __Note:__ `npm test` will run the compilation automatically before the tests. ### Tests [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. ### Documentation Feathers documentation is contained in Markdown files in the [feathers-docs](https://github.com/feathersjs/feathers-docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release. ## External Modules If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [showcase](https://docs.feathersjs.com/why/showcase.html). You also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start. If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: ## Contributor Code of Conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) ================================================ FILE: .github/issue_template.md ================================================ ### Steps to reproduce (First please check that this issue is not already solved as [described here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) - [ ] Tell us what broke. The more detailed the better. - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. ### Expected behavior Tell us what should happen ### Actual behavior Tell us what happens instead ### System configuration Tell us about the applicable parts of your setup. **Module versions** (especially the part that's not working): **NodeJS version**: **Operating System**: **Browser Version**: **React Native Version**: **Module Loader**: ================================================ FILE: .github/pull_request_template.md ================================================ ### Summary (If you have not already please refer to the contributing guideline as [described here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) - [ ] Tell us about the problem your pull request is solving. - [ ] Are there any open issues that are related to this? - [ ] Is this PR dependent on PRs in other repos? If so, please mention them to keep the conversations linked together. ### Other Information If there's anything else that's important and relevant to your pull request, mention that information here. This could include benchmarks, or other information. Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). Thanks for contributing to Feathers! :heart: ================================================ FILE: .gitignore ================================================ .DS_Store # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # Commenting this out is preferred by some people, see # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- node_modules # Users Environment Variables .lock-wscript # The compiled/babelified modules lib/ # Editor directories and files .idea .vscode/settings.json *.suo *.ntvs* *.njsproj *.sln /dist ================================================ FILE: .istanbul.yml ================================================ verbose: false instrumentation: root: ./src/ excludes: - lib/ include-all-sources: true reporting: print: summary reports: - html - text - lcov watermarks: statements: [50, 80] lines: [50, 80] functions: [50, 80] branches: [50, 80] ================================================ FILE: .npmignore ================================================ .editorconfig .jshintrc .travis.yml .istanbul.yml .babelrc .idea/ test/ !lib/ ================================================ FILE: .travis.yml ================================================ language: node_js node_js: node cache: yarn addons: code_climate: repo_token: 'your repo token' firefox: "51.0" services: - xvfb notifications: email: false ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible Node.js debug attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "TS - Mocha Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "args": [ "--require", "ts-node/register", "-u", "bdd", "--timeout", "999999", "--colors", "--recursive", "${workspaceFolder}/test/**/*.ts" ], "env": { "TS_NODE_PROJECT": "tsconfig.test.json" }, "internalConsoleOptions": "openOnSessionStart" }, { "type": "node", "request": "launch", "name": "Launch Program", "program": "${workspaceRoot}/lib/", "cwd": "${workspaceRoot}" }, { "type": "node", "request": "attach", "name": "Attach to Process", "port": 5858 } ] } ================================================ FILE: CHANGELOG.md ================================================ # Change Log ## [v1.0.0-pre.3](https://github.com/feathersjs/feathers-vuex/tree/v1.0.0-pre.3) (2017-08-26) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v1.0.0-pre.2...v1.0.0-pre.3) **Closed issues:** - \[Proposal\] Allow existing store modules to provide additional properties like the customActions, etc. [\#36](https://github.com/feathersjs/feathers-vuex/issues/36) ## [v1.0.0-pre.2](https://github.com/feathersjs/feathers-vuex/tree/v1.0.0-pre.2) (2017-08-24) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v1.0.0-pre.1...v1.0.0-pre.2) ## [v1.0.0-pre.1](https://github.com/feathersjs/feathers-vuex/tree/v1.0.0-pre.1) (2017-08-22) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.8.0...v1.0.0-pre.1) **Closed issues:** - Update mutation not changing store or backend [\#38](https://github.com/feathersjs/feathers-vuex/issues/38) - namespaces cannot be arrays for nested modules \(supported by vuex\) [\#34](https://github.com/feathersjs/feathers-vuex/issues/34) - using deep-assign package [\#15](https://github.com/feathersjs/feathers-vuex/issues/15) ## [v0.8.0](https://github.com/feathersjs/feathers-vuex/tree/v0.8.0) (2017-08-18) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.7.2...v0.8.0) **Closed issues:** - How to setup vuex and vue-router to redirect when a store value is not set? [\#37](https://github.com/feathersjs/feathers-vuex/issues/37) - OAuth with Google and feathers-vuex [\#33](https://github.com/feathersjs/feathers-vuex/issues/33) **Merged pull requests:** - Support array namespaces for module nesting [\#35](https://github.com/feathersjs/feathers-vuex/pull/35) ([jskrzypek](https://github.com/jskrzypek)) ## [v0.7.2](https://github.com/feathersjs/feathers-vuex/tree/v0.7.2) (2017-08-04) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.7.1...v0.7.2) **Closed issues:** - Mapped Action is not a function [\#31](https://github.com/feathersjs/feathers-vuex/issues/31) - Real time updates stops working sometimes depending on the query [\#28](https://github.com/feathersjs/feathers-vuex/issues/28) **Merged pull requests:** - Return Promise.reject for action path of services when catching an er… [\#32](https://github.com/feathersjs/feathers-vuex/pull/32) ([phenrigomes](https://github.com/phenrigomes)) ## [v0.7.1](https://github.com/feathersjs/feathers-vuex/tree/v0.7.1) (2017-07-08) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.7.0...v0.7.1) **Closed issues:** - errorOnAuthenticate is not reset [\#29](https://github.com/feathersjs/feathers-vuex/issues/29) - Hoping to support server paging parameters [\#23](https://github.com/feathersjs/feathers-vuex/issues/23) **Merged pull requests:** - Fix 29 [\#30](https://github.com/feathersjs/feathers-vuex/pull/30) ([mgesmundo](https://github.com/mgesmundo)) ## [v0.7.0](https://github.com/feathersjs/feathers-vuex/tree/v0.7.0) (2017-06-27) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.6.0...v0.7.0) **Merged pull requests:** - Remove “feathers” module completely [\#27](https://github.com/feathersjs/feathers-vuex/pull/27) ([marshallswain](https://github.com/marshallswain)) ## [v0.6.0](https://github.com/feathersjs/feathers-vuex/tree/v0.6.0) (2017-06-27) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.5.0...v0.6.0) **Merged pull requests:** - Allow customizing a service’s default store [\#26](https://github.com/feathersjs/feathers-vuex/pull/26) ([marshallswain](https://github.com/marshallswain)) ## [v0.5.0](https://github.com/feathersjs/feathers-vuex/tree/v0.5.0) (2017-06-27) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.4.2...v0.5.0) **Closed issues:** - Are feathers-reactive and RxJS dependencies necessary? [\#22](https://github.com/feathersjs/feathers-vuex/issues/22) - Weird / destructive behaviour using mapActions service find between components [\#21](https://github.com/feathersjs/feathers-vuex/issues/21) - removeItems action added after create when upgrading version from version 0.4.0 to 0.4.1 [\#19](https://github.com/feathersjs/feathers-vuex/issues/19) **Merged pull requests:** - Remove all non-serializable data from the state [\#25](https://github.com/feathersjs/feathers-vuex/pull/25) ([marshallswain](https://github.com/marshallswain)) - Opt in to `autoRemove` [\#24](https://github.com/feathersjs/feathers-vuex/pull/24) ([marshallswain](https://github.com/marshallswain)) ## [v0.4.2](https://github.com/feathersjs/feathers-vuex/tree/v0.4.2) (2017-06-07) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.4.1...v0.4.2) **Closed issues:** - What is the benefit to convert data in feathers to vuex instead of accessing feathers services directly? [\#18](https://github.com/feathersjs/feathers-vuex/issues/18) - items not being removed from the list - fix proposal [\#12](https://github.com/feathersjs/feathers-vuex/issues/12) **Merged pull requests:** - QuickFix: Use idField for removal [\#20](https://github.com/feathersjs/feathers-vuex/pull/20) ([cmeissl](https://github.com/cmeissl)) ## [v0.4.1](https://github.com/feathersjs/feathers-vuex/tree/v0.4.1) (2017-05-26) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.4.0...v0.4.1) **Closed issues:** - Is possible to upload a file using feathers-vuex? [\#11](https://github.com/feathersjs/feathers-vuex/issues/11) - Integration with Nuxt [\#8](https://github.com/feathersjs/feathers-vuex/issues/8) **Merged pull requests:** - Bugfix - add params to patch action service call [\#14](https://github.com/feathersjs/feathers-vuex/pull/14) ([ndamjan](https://github.com/ndamjan)) - fix item removal in addOrUpdateList \(\#12\) [\#13](https://github.com/feathersjs/feathers-vuex/pull/13) ([ndamjan](https://github.com/ndamjan)) ## [v0.4.0](https://github.com/feathersjs/feathers-vuex/tree/v0.4.0) (2017-05-01) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.3.1...v0.4.0) ## [v0.3.1](https://github.com/feathersjs/feathers-vuex/tree/v0.3.1) (2017-05-01) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.3.0...v0.3.1) ## [v0.3.0](https://github.com/feathersjs/feathers-vuex/tree/v0.3.0) (2017-05-01) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.2.2...v0.3.0) **Merged pull requests:** - Node: Keep functions out of Vuex state [\#9](https://github.com/feathersjs/feathers-vuex/pull/9) ([marshallswain](https://github.com/marshallswain)) ## [v0.2.2](https://github.com/feathersjs/feathers-vuex/tree/v0.2.2) (2017-04-28) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.2.1...v0.2.2) ## [v0.2.1](https://github.com/feathersjs/feathers-vuex/tree/v0.2.1) (2017-04-18) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.2.0...v0.2.1) **Closed issues:** - `clearList` mutation behaves unexpectedly if `current` isn't defined [\#5](https://github.com/feathersjs/feathers-vuex/issues/5) **Merged pull requests:** - Fix clearList unexpected behavior. Closes \#5 [\#6](https://github.com/feathersjs/feathers-vuex/pull/6) ([silvestreh](https://github.com/silvestreh)) ## [v0.2.0](https://github.com/feathersjs/feathers-vuex/tree/v0.2.0) (2017-04-18) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.1.1...v0.2.0) **Merged pull requests:** - Actions [\#4](https://github.com/feathersjs/feathers-vuex/pull/4) ([marshallswain](https://github.com/marshallswain)) ## [v0.1.1](https://github.com/feathersjs/feathers-vuex/tree/v0.1.1) (2017-04-18) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.1.0...v0.1.1) ## [v0.1.0](https://github.com/feathersjs/feathers-vuex/tree/v0.1.0) (2017-04-18) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.0.5...v0.1.0) **Merged pull requests:** - Service tests clear list [\#3](https://github.com/feathersjs/feathers-vuex/pull/3) ([marshallswain](https://github.com/marshallswain)) - add before-install script [\#2](https://github.com/feathersjs/feathers-vuex/pull/2) ([marshallswain](https://github.com/marshallswain)) ## [v0.0.5](https://github.com/feathersjs/feathers-vuex/tree/v0.0.5) (2017-04-15) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.0.4...v0.0.5) **Merged pull requests:** - Update readme.md [\#1](https://github.com/feathersjs/feathers-vuex/pull/1) ([marshallswain](https://github.com/marshallswain)) ## [v0.0.4](https://github.com/feathersjs/feathers-vuex/tree/v0.0.4) (2017-04-12) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.0.3...v0.0.4) ## [v0.0.3](https://github.com/feathersjs/feathers-vuex/tree/v0.0.3) (2017-03-15) [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.0.2...v0.0.3) ## [v0.0.2](https://github.com/feathersjs/feathers-vuex/tree/v0.0.2) (2017-03-15) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Feathers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Feathers-Vuex [![Build Status](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex.png?branch=master)](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex) [![Dependency Status](https://img.shields.io/david/feathersjs-ecosystem/feathers-vuex.svg?style=flat-square)](https://david-dm.org/feathersjs-ecosystem/feathers-vuex) [![Download Status](https://img.shields.io/npm/dm/feathers-vuex.svg?style=flat-square)](https://www.npmjs.com/package/feathers-vuex) [![Greenkeeper badge](https://badges.greenkeeper.io/feathersjs-ecosystem/feathers-vuex.svg)](https://greenkeeper.io/) `Feathers-Vuex` is a first class integration of FeathersJS and Vuex. It implements many Redux best practices under the hood, eliminates _a lot_ of boilerplate code with flexible data modeling, and still allows you to easily customize the Vuex store. ![feathers-vuex service logo](./service-logo.png) ## Demo & Documentation [Demo](https://codesandbox.io/s/xk52mqm7o) See [https://vuex.feathersjs.com](https://vuex.feathersjs.com) for full documentation. ## Installation ```bash npm install feathers-vuex --save ``` ```bash yarn add feathers-vuex ``` IMPORTANT: Feathers-Vuex is (and requires to be) published in ES6 format for full compatibility with JS classes. If your project uses Babel, it must be configured properly. See the [Project Configuration](https://vuex.feathersjs.com/getting-started.html#project-configuration) section for more information. ## Contributing This repo is pre-configured to work with the Visual Studio Code debugger. After running `yarn install`, use the "Mocha Tests" debug script for a smooth debugging experience. ## License Copyright (c) Forever and Ever, or at least the current year. Licensed under the [MIT license](https://github.com/feathersjs-ecosystem/feathers-vuex/blob/master/LICENSE). ================================================ FILE: docs/.vuepress/config.js ================================================ module.exports = { title: 'FeathersVuex', description: 'Integration of FeathersJS, Vue, and Nuxt for the artisan developer', head: [['link', { rel: 'icon', href: '/favicon.ico' }]], theme: 'default-prefers-color-scheme', themeConfig: { repo: 'feathersjs-ecosystem/feathers-vuex', docsDir: 'docs', editLinks: true, sidebar: [ '/api-overview.md', '/3.0-major-release.md', '/getting-started.md', '/example-applications.md', '/vue-plugin.md', '/service-plugin.md', '/auth-plugin.md', '/model-classes.md', '/common-patterns.md', '/composition-api.md', '/mixins.md', '/data-components.md', '/feathers-vuex-forms.md', '/nuxt.md', '/2.0-major-release.md', ], serviceWorker: { updatePopup: true } } } ================================================ FILE: docs/2.0-major-release.md ================================================ --- title: 2.0 Breaking Changes sidebarDepth: 3 --- # 2.0 Breaking Changes The biggest change in Feathers-Vuex 2.0 is that it has been refactored with TypeScript! (It's mostly ES6, still) Your project does NOT require to be written in TypeScript. The `dist` is compiled to ES6. ## Fixing the build for older Vue-CLI Apps As of October 2019, up-to-date Vue-CLI apps are able to properly build and use `feathers-vuex` after adding `transpileDependencies`. For apps based on older modules, I've found the simplest solution to be to copy the `feathers-vuex` module into `src/libs` inside your project. Create a `copy-deps.sh` file in the root of your project and setup the following: ```bash rm -rf src/libs mkdir src/libs # feathers-vuex cp -r node_modules/feathers-vuex/dist src/libs/feathers-vuex ``` Then in `package.json`, create a script that uses the `copy-deps.sh` file: ```json { "copy": ". ./copy-deps.sh", "serve": "npm run copy && vue-cli-service serve", "build": "npm run copy && vue-cli-service build", "postinstall": "npm run copy" } ``` It's not the prettiest solution, but it works well and is the simplest for older apps. ## Here's what's new in `feathers-vuex` Check out the tests for the best documentation. They've been reorganized. This is still a Work in Progress. ## Changes to Initialization 1. To assist in connecting with multiple FeathersJS API servers, a new `serverAlias` option is now required. This requires a simple addition to the initial options. 2. The exports have changed. - (a) A new `BaseModel` is available. This is the base `FeathersVuexModel` which contains the model methods. Feel free to extend it and make it fit your awesome services! - (b) The `service` method has been renamed to `makeServicePlugin`. - (c) The `auth` method is now called `makeAuthPlugin` - (d) The `models` object is now exported, so you can access them from anywhere. They are keyed by `serverAlias`. - (e) A new `clients` object is available. The intention is to allow working with multiple FeathersJS API servers. 3. You no longer pass a `servicePath` to create a service-plugin. Instead, pass the actual Feathers service. 4. Since you can customize the Model, you also pass the extended Model into the `makeServicePlugin` method. Below is an all-in-one example of a the basic configuration steps. See the next section for how to setup a project. ```js // ./src/store/store.js import feathers from './feathers-client' import Vuex from 'vuex' import feathersVuex from 'feathers-vuex' const { BaseModel, // (2a) makeServicePlugin, // (2b) makeAuthPlugin, // (2c) models, // (2d) clients // (2e) } = feathersVuex(feathers, { idField: '_id', serverAlias: 'myApi' // (1) }) class Todo extends BaseModel { // required constructor (data, options) { super(data, options) } // required static modelName = 'Todo' // optional, but useful static instanceDefaults(data) { return { name: '', isComplete: false, userId: null, user: null // populated on the server } } // optional, but useful static setupInstance(data) { if (data.user) { data.user = new models.myApi.User(data.user) } return data } // customize the model as you see fit! } const todosPlugin = makeServicePlugin({ Model: Todo, // (3) service: feathers.service('todos') // (4) }) const store = new Vuex.Store({ plugins: [ todosPlugin ] }) ``` ## Feathers-Vuex Vue plugin changes The Vue plugin is registered in exactly the same way. The difference comes when you try to find the Model classes in the `$FeathersVuex` object. Instead of finding models directly on the `$FeathersVuex` object, they are namespaced by the `serverAlias` you provided. This allows cleaner support for multiple APIs. Supposing you had this code in a component, previously... ```js created () { // The old way const { Todo } = this.$FeathersVuex } ``` Modify it to include the new `serverAlias`. Suppose you set a `serverAlias` of `myApi`, you'd put this in the new version: ```js created () { // The new way includes the `serverAlias` of '.myApi' const { Todo } = this.$FeathersVuex.myApi } ``` ## Better default `idField` support Since records are keyed by id, `feathers-vuex` needs to know what the `idField` is for each service. In the last version, the default was `id`, and you had to specify something different. This version supports `id` and `_id` with zero configuration. You only need to set `idField` when you're using something other than `id` or `_id`. There's still a warning message when records don't have a property matching the `idField`. Just like in the last version, it only appears when you turn on `debug: true` in the options. ## Support for Temporary Records Feathers-Vuex 2.0 supports tracking temporary items and automatically assigns a temporary id to new records. It also adds the records to `state.tempsById`. This is customizable using the `tempIdField` option. Because of the new ability to handle temporary records, a message is only logged when assigning a temporary id to a record. The `checkId` utility function has been removed, since this was its main purpose. ## Getters Work with Temporary Records The `find` getter has been updated to include records from `state.tempsById` when you pass `temps: true` in the params. The `get` getter has also been updated to work with temp ids. Pass the tempId the way you normally would pass the id: `get(tempId)` ## The "currentItem" workflow is no longer supported The `setCurrent` mutation and `currentId` state encouraged use of a very limiting API. It's much more common for apps to require more than one current record. The `createCopy`, `resetCopy` (formerly called `rejectCopy`), `commitCopy`, and `clearCopy` mutations (since v1.x) provide a more flexible solution for implementing the same functionality. As a result of this, following have been removed from the modules: - state: `currentID`, `copy` - getters: `current`, `getCopy` - mutations: `setCurrent`, `clearCurrent`, `clearList`, `commitCopy`, `clearCopy`, `resetCopy` ## The `diffOnPatch` option has been removed (See the next section for its replacement.) I have not been able to find a diffing algorithm that works equally well acroos all schemas. It's especially difficult for nested schemas. Because of this, `diffOnPatch` is no longer a global option. It is being replaced by the `diffOnPatch` static Model method. See the next section. ## Model Classes: BYOD (Bring Your Own Diffing) > Note: As of `feathers-vuex@3.9.0`, you can also pass `params.data` to the patch object to implement partial patching on objects. You might choose to use `params.data` instead of `diffOnPatch`. First, why do any diffing? On the API server, an `update` request replaces an entire object, but a `patch` request only overwrites the attributes that are provided in the data. For services with simple schemas, it doesn't really matter. But if your schema grows really large, it can be supportive to only send the updates instead of the entire object. A new `diffOnPatch` method is available to override in your extended models. `diffOnPatch` gets called just before sending the data to the API server. It gets called with the data and must return the diffed data. By default, it is set to `diffOnPatch: data => data`. Below is an example of how you might implement `diffOnPatch`. You would only ever use this with a cloned instance, otherwise there's nothing to diff. ```js import { diff } from 'deep-object-diff' const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { serverAlias: 'myApi' }) class Todo extends BaseModel { public constructor (data, options?) { super(data, options) } public static modelName = 'Todo' public static diffOnPatch (data) { const originalObject = Todo.store.state.keyedById[data._id] return diff(originalObject, data) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Todo, service: feathers.service(servicePath) }) ] }) ``` ## The `modelName` option has moved While the original intent was to completely remove the `modelName` option, it's still required after transpiling to ES5. This is because during transpilation, the class name gets stripped and can't be put back into place. Since ES5 is the default target for most build environments, the `modelName` is still required to be specified, but it has been moved. Instead of being an option, it's required as a static property of each class. Note: Once ES6 is the default target for most build systems, modelName will become optional. For future upgradability, it's recommended that you give your `modelName` the exact same name as your model class. ```js const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { serverAlias: 'myApi' }) class Todo extends BaseModel { public constructor (data, options?) { super(data, options) } public static modelName = 'Todo' // modelName is required on all Model classes. public static exampleProp: string = 'Hello, World! (notice the comma, folks!)' } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Todo, service: feathers.service(servicePath) }) ] }) ``` ## Options are no longer kept on the Model The Model class no longer has an `options` property. You can access the same information through the `Model.store.state[Model.namespace]`. ## The 'apiPrefix' option has been removed Feathers-Vuex now includes full support for communicating with multiple FeathersJS APIs. The `apiPrefix` option was a poorly-implemented, hacky, first attempt at this same feature. Since it didn't work as intended, it has been removed. See this example test for working with multiple APIs: ```js import { assert } from 'chai' import Vue from 'vue' import Vuex from 'vuex' import { feathersRestClient as feathers, makeFeathersRestClient } from '../../test/fixtures/feathers-client' import feathersVuex from './index' it('works with multiple, independent Feathers servers', function() { // Connect to myApi, create a Todo Model & Plugin const feathersMyApi = makeFeathersRestClient('https://api.my-api.com') const myApi = feathersVuex(feathersMyApi, { idField: '_id', serverAlias: 'myApi' }) class Todo extends myApi.BaseModel { public test: boolean = true } const todosPlugin = myApi.makeServicePlugin({ Model: Todo, service: feathersMyApi.service('todos') }) // Create a Task Model & Plugin on theirApi const feathersTheirApi = makeFeathersRestClient('https://api.their-api.com') const theirApi = feathersVuex(feathersTheirApi, { serverAlias: 'theirApi' }) class Task extends theirApi.BaseModel { public test: boolean = true } const tasksPlugin = theirApi.makeServicePlugin({ Model: Task, service: feathersTheirApi.service('tasks') }) // Register the plugins new Vuex.Store({ plugins: [todosPlugin, tasksPlugin] }) const { models } = myApi assert(models.myApi.Todo === Todo) assert(!models.theirApi.Todo, `Todo stayed out of the 'theirApi' namespace`) assert(models.theirApi.Task === Task) assert(!models.myApi.Task, `Task stayed out of the 'myApi' namespace`) assert.equal( models.myApi.byServicePath[Todo.servicePath], Todo, 'also registered in models.byServicePath' ) assert.equal( models.theirApi.byServicePath[Task.servicePath], Task, 'also registered in models.byServicePath' ) ``` ## Services are no longer set up, internally You no longer just pass a servicePath. Instead, create the service, then pass the returned service object. ## Simplified Pending Mutations Previously, there was a mutation for every single variety of method and set/unset pending. (`setFindPending`, `unsetFindPending`, etc.). There were a total of twelve methods for this simple operation. They are now combined into two methods: `setPending(method)` and `unsetPending(method)`. Here's the difference. ```js // The old way commit('setFindPending') commit('unsetFindPending') // The new way commit('setPending', 'find') commit('unsetPending', 'find') ``` ## Simplified Error Mutations The "error" mutations have been simplified similar to the "pending" mutations: ```js // The old way commit('setFindError', error) commit('clearFindError') // The new way commit('setError', { method: 'find', error }) commit('clearError', 'find') ``` ## `instanceDefaults` must be a function In the previous version, you could specify instanceDefaults as an object. It was buggy and limiting. In this new version, `instanceDefaults` must always be a function. See the next section for an example. ## Getter and Setter props go on the Model classes One of the great features about using Model classes is data-level computed properties. You get to specify computed properties directly on your data structures instead of inside components, which keeps a better separation of concerns. In `feathers-vuex@2.x`, since we have direct access to the Model classes, it's the perfect place to define the computed properties: ```js import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client' class User extends BaseModel { constructor(data, options) { super(data, options) } static modelName = 'User' // required // Computed properties don't go on in the instanceDefaults, anymore. static instanceDefaults() { return { firstName: '', lastName: '', email: '', password: '', isAdmin: false, } } // Here's a computed getter get fullName() { return `${this.firstName} ${this.lastName}` } // Probably not something you'd do in real life, but it's an example of a setter. set fullName(fullName) { const [ firstName, lastName ] = fullName.split(' ') Object.assign(this, { firstName, lastName }) } } const servicePath = 'users' const servicePlugin = makeServicePlugin({ Model: User, service: feathersClient.service(servicePath), servicePath }) ``` ## Relationships have been separated from `instanceDefaults` Feathers-Vuex 2.0 has a new API for establishing relationships between data. Before we cover how it works, let's review the old API. Feathers-Vuex 1.x allowed using the `instanceDefaults` API to both setup default values for Vue reactivity AND establishing relationships between services. It supported passing a string name that matched a model name to setup a relationship, as shown in this next example. This was a simple, but very limited API: ```js // The old way instanceDefaults: { _id: '', description: '', isCompleted: false, user: 'User' } ``` Any instance data with a matching key would overwrite the same property in the instanceDefaults, which resulted in an inconsistent API. In Feathers-Vuex 2.0, the `instanceDefaults` work the same for setting defaults with only one exception: They no longer setup the relationships. The new `setupInstance` function provides an API that is much more powerful. As mentioned earlier, it MUST be provided as a function: ```js // See the `model-instance-defaults.test.ts` file for example usage. // This is a brief example. instanceDefaults(data, { models, store}) { return { _id: '', description: '', isCompleted: false // No user props, here. } } ``` Notice in the above example that we did not return `user`. Relationships are now handled in the `setupInstance` method. Where `instanceDefaults` props get overwritten with instance data, the props returned from `setupInstance` overwrite the instance data. If it were using `Object.assign`, internally (it's not, but IF it were), it would look like the below example, where `data` is the original instance data passed to the constructor. ```js Object.assign({}, instanceDefaults(data), data, setupInstance(data)) ``` ## Define Relationships and Modify Data with `setupInstance` The new `setupInstance` method allows a lot of flexibility in creating new instances. It has the exact same API as the `instanceDefaults` method. The only difference is the order in which they are applied to the instance data. Although it looks similar to `instanceDefaults`, it can't be used for default values. This is because it overwrites instance data. Having separate methods allows a clean separation between setting up defaults and establishing relationships with other constructors. ```js // See the `model-relationships.test.ts` file for example usage. // This is a brief example. function setupInstance(data, { models, store }) { const { User, Tag } = models.myServerAlias // Based on the serverAlias you provide, initially // A single User instance if (data.user) { data.user = new User(data.user) } // An array of Tag instances if (data.tags) { data.tags = data.tags.map(t => new Tag(t)) } // A JavaScript Date Object if (data.createdAt) { data.createdAt = new Date(data.createdAt) } return data } ``` Or below is an example that does the exact same thing with one line per attribute: ```js function setupInstance(data, { models, store }) { const { User } = models.myServerAlias Object.assign(data, { ...(data.user && { user: new User(data.user) }), // A single User instance ...(data.tags && { tags: data.tags.map(t => new Tag(t)) }), // An array of Tag instances ...(data.createdAt && { createdAt: new Date(data.createdAt) }) // A JavaScript Date Object }) return data } ``` Where `instanceDefaults` props get replaced by instance data, the props returned from `setupInstance` overwrite the instance data. If it were using `Object.assign`, internally (it's not, but IF it were), it would look like the below example, where `data` is the original instance data passed to the constructor. ```js Object.assign({}, instanceDefaults(data), data, setupInstance(data)) ``` ## Preventing duplicate merge when extending BaseModel with a custom constructor The BaseModel constructor calls `mergeWithAccessors(this, newData)`. This utility function correctly copies data between both regular objects and Vue.observable instances. If you create a class where you need to do your own merging, you probably don't want `mergeWithAccessors` to run twice. In this case, you can use the `merge: false` BaseModel ___instance option___ to prevent the internal merge. You can then access the `mergeWithAccessors` method by calling `MyModel.merge(this, newData)`. Here's an example: ```ts const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'myApiServer' }) class Todo extends BaseModel { public constructor(data, options?) { options.merge = false // Prevent the internal merge from occurring. super(data, options) // ... your custom constructor logic happens here. // Call the static merge method to do your own merging. Todo.merge(this, data) } } ``` It's important to note that setting `merge: false` in the options will disable the `setupinstance` function. You need to manually call it, like this: ```ts class Todo extends BaseModel { public constructor(data, options?) { options = options || {} options.merge = false // Prevent the internal merge from occurring. super(data, options) // ... your custom construcor logic happens here. // Call setupInstance manually const { models, store } = Todo // JavaScript fundamentals: if you're using `this` in `setupInstance`, use .call(this, ...) const instanceData = Todo.setupInstance.call(this, data, { models, store }) // If you're not using `this, just call it like normal const instanceData = Todo.setupInstance(data, { models, store }) // Call the static merge method to do your own merging. Todo.merge(this, instanceData) } } ``` ## Customizing the BaseModel Because we have access to the BaseModel, we can extend it to do whatever custom stuff we need in our application. The `feathers-client.js` file is a great, centralized location for accomplishing this: ```js // src/feathers-client.js import feathers from '@feathersjs/feathers' import socketio from '@feathersjs/socketio-client' import authClient from '@feathersjs/authentication-client' import io from 'socket.io-client' import feathersVuex from 'feathers-vuex' // or '@/libs/feathers-vuex' if you're copying feathers-vuex as mentioned earlier. // Setup the Feathers client const host = process.env.VUE_APP_API_URL // or set a string here, directly const socket = io(host, { transports: ['websocket'] }) const feathersClient = feathers() .configure(socketio(socket)) .configure(authClient({ storage: window.localStorage })) export default feathersClient // Setup feathers-vuex const { makeServicePlugin, makeAuthPlugin, BaseModel, models, clients, FeathersVuex } = feathersVuex(feathersClient, { serverAlias: 'api', // or whatever that makes sense for your project idField: '_id' // `id` and `_id` are both supported, so this is only necessary if you're using something else. }) // Note that if you want to // extend the BaseClass for the rest of the app, this is a great place to do it. // After you've extended the BaseClass with your CustomClass, export it, here. class CustomBaseModel extends BaseModel { // Optionally add custom functionality for all services, here. } // Export all of the utilities for the rest of the app. export { makeAuthPlugin, makeServicePlugin, BaseModel, models, clients, FeathersVuex, CustomBaseModel // Don't forget to export it for use in all other services. } ``` ## Auth plugin changes With FeathersJS version 4, the user is returned in the authentication response, by default. This means that it's no longer required to provide a `userService` option to populate the user. 👍 If you would like to enable backwards compatibility with the previous version of Feathers, pass the below code in the makeAuthPlugin. ```js makeAuthPlugin({ userService: 'users', actions: { responseHandler({ commit, state, dispatch }, response) { if (response.accessToken) { commit('setAccessToken', response.accessToken) // Decode the token and set the payload, but return the response return feathersClient.passport .verifyJWT(response.accessToken) .then(payload => { commit('setPayload', payload) let user = response[state.responseEntityField] // If a user was returned in the authenticate response, use that user. if (user) { if (state.serverAlias && state.userService) { const Model = Object.keys(models[state.serverAlias]) .map(modelName => models[state.serverAlias][modelName]) .find(model => model.servicePath === state.userService) if (Model) { user = new Model(user) } } commit('setUser', user) // Populate the user if the userService was provided } else if ( state.userService && payload.hasOwnProperty(state.entityIdField) ) { return dispatch( 'populateUser', payload[state.entityIdField] ).then(() => { commit('unsetAuthenticatePending') return response }) } else { commit('unsetAuthenticatePending') } return response }) // If there was not an accessToken in the response, allow the response to pass through to handle two-factor-auth } else { return response } } } ``` The above code will override the `responseHandler` auth action to work with the Passport-based version of Feathers Authentication. ## Gotchas ### Don't Perform Queries (Side Effects) in Getters Don't try to perform a query from within a getter like the example, below. It will result in an infinite loop: ```js get user () { if (this.userId) { const user = Models.User.getFromStore(this.userId) // Fetch the User record if we don't already have it if (!user) { Models.User.get(this.userId) } return user } else { return null } } ``` ### Using custom query parameters There are two places where the query operators have to be allowed. - In the Feathers Client (for the actions): refer to the FeathersJS docs for `whitelist`ing operators. - Inside feathers-vuex (for the getters): Check out the `paramsForServer` and `whitelist` options for `feathers-vuex`. Both accept an array of strings representing prop names, but now I can't remember why I determined that I needed both. :) For the Feathers Client, follow the FeathersJS docs for your database adapter. ================================================ FILE: docs/3.0-major-release.md ================================================ --- title: What's New in 3.0 sidebarDepth: 3 --- # What's new in Feathers-Vuex 3.0 ## Vue Composition API Support Version 3.0 of Feathers-Vuex is the Vue Composition API release! There were quite a few disappointed (and misinformed:) developers in 2019 when the Vue.js team announced what is now called the Vue Composition API. From my perspective: - It is the most powerful feature added to Vue since its first release. - It improves the ability to create dynamic functionality in components. - It greatly enhances organization of code in components. - It encourages code re-use. Check out the [vue-use-web](https://tarektouati.github.io/vue-use-web/) collection for some great examples. And now it has become the best way to perform queries with Feathers-Vuex. To find out how to take advantage of the new functionality in your apps, read the [Feather-Vuex Composition API docs](./composition-api.md). ## New `extend` option for `makeServicePlugin` The `makeServicePlugin` now supports an `extend` method that allows customizing the store and gives access to the actual Vuex `store` object, as shown in this example: ```js import { makeServicePlugin } from ‘feathers-vuex’ import { feathersClient } from ‘./feathers-client.js’ class Todo { /* truncated */ } export default makeServicePlugin({ Model: Todo, service: feathersClient.service(‘todos’), extend({ store, module }) { // Listen to other parts of the store store.watch(/* truncated */) return { state: {}, getters: {}, mutations: {}, actions: {} } } }) ``` ## Partial data on patch As of version 3.9.0, you can provide an object as `params.data`, and Feathers-Vuex will use `params.data` as the patch data. This change was made to the service-module, itself, so it will work for `patch` across all of feathers-vuex. Here's an example of patching with partial data: ```js import { models } from 'feathers-vuex' const { Todo } = models.api const todo = new Todo({ description: 'Do Something', isComplete: false }) todo.patch({ data: { isComplete: true } }) // also works for patching with instance.save todo.save({ data: { isComplete: true } }) ``` ## FeathersVuexPagination Component To assist with Server Side Pagination support, Feathers-Vuex now includes the `` component. It's a renderless component that removes the boilerplate behind handling pagination in the UI. Read about it in the [Composition API Docs](/composition-api.html#feathersvuexpagination). ## Custom Handling for Feathers Events Version 3.1 of Feathers-Vuex enables ability to add custom handling for each of the FeathersJS realtime events. You can read more about it in the [Service Plugin: Events](./service-plugin.md#service-events) docs. ## Breaking Changes Feathers-Vuex follows semantic versioning. There are two breaking changes in this release: ### Auth Plugin `user` Not Reactive Due to changes in how reactivity is applied to service state (it's now using Vue.set under the hood), the `user` state of the `auth` module is no longer reactive. To fix this issue, two getters have been added to the `auth` state. They are available when a `userService` is provided to the `makeAuthPlugin` options. - `user`: returns the reactive, logged-in user from the `userService` specified in the options. - `isAuthenticated`: a easy to remember boolean attribute for if the user is logged in. If you depend on a reactive, logged-in user in your apps, here is how to fix the reactivity: - Replace any reference to `store.state.auth.user` with `store.getters['auth/user']`. Because the `user` state is no longer reactive, it is logical for it to be removed in the next version. It will likely be replaced by a `userId` attribute in Feathers-Vuex 4.0. ### Server-Side Pagination Support is Off by Default The `makeFindMixin` (and the new `useFind` utility) now have server-side pagination support turned off, by default. Real-time arrays of results are now the default setting. This really improves the development experience, especially for new users. To migrate your app to version 3.0, you need to update any `params` where you are using server-side pagination. It will work as it has been in version 2.0 once you explicitly set `paginate: true` in the params, like this: ```js import { makeFindMixin } from 'feathers-vuex' export default { name: 'MyComponent', mixins: [ makeFindMixin({ service: 'users', watch: true })], computed: { usersParams() { return { query: {}, paginate: true // explicitly enable pagination, now. } } } } ``` This behavior exactly matches the new `useFind` utility. ## Deprecations ### The `keepCopiesInStore` Option The `keepCopiesInStore` option is now deprecated. This was a part of the "clone and commit" API which basically disabled the reason for creating the "clone and commit" API in the first place. If you're not familiar with the Feathers-Vuex "clone and commit" API, you can learn more about the [built-in data modeling](./model-classes.md) API and the section about [Working with Forms](./feathers-vuex-forms.md#the-clone-and-commit-pattern). The `keepCopiesInStore` feature is set to be removed in Feathers-Vuex 4.0. ### Auth Plugin State: `user` As described, earlier on this page, since the Auth Plugin's `user` state is no longer reactive and has been replaced by a `user` getter that IS reactive, the `user` state will be removed in the Feathers-Vuex 4.0. ### Renderless Data Components: `query`, `fetchQuery` and `temps` To keep consistency with mixins and the composition API it's preferred to use `params` and `fetchParams` instead of the old `query` and `fetchQuery` for renderless data components. Also the `:temps="true"` is deprecated in favour of `:params="{ query: {}, temps: true }"`. This way additional params can be passed to the server if you need some more magic like `$populateParams`. ================================================ FILE: docs/api-overview.md ================================================ --- title: API Overview sidebarDepth: 3 --- [![Build Status](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex.png?branch=master)](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex) [![Dependency Status](https://img.shields.io/david/feathersjs-ecosystem/feathers-vuex.svg?style=flat-square)](https://david-dm.org/feathersjs-ecosystem/feathers-vuex) [![Download Status](https://img.shields.io/npm/dm/feathers-vuex.svg?style=flat-square)](https://www.npmjs.com/package/feathers-vuex) ![feathers-vuex service logo](https://github.com/feathersjs-ecosystem/feathers-vuex/raw/master/service-logo.png) > Integrate the Feathers Client into Vuex `feathers-vuex` is a first class integration of the Feathers Client and Vuex. It implements many Redux best practices under the hood, eliminates *a lot* of boilerplate code, and still allows you to easily customize the Vuex store. These docs are for version 2.x. For feathers-vuex@1.x, please go to [https://feathers-vuex-v1.netlify.com](https://feathers-vuex-v1.netlify.com). ## Features - Fully powered by Vuex & Feathers - Realtime By Default - Actions With Reactive Data - Local Queries - Live Queries - Feathers Query Syntax - Vuex Strict Mode Support - [Client-Side Pagination Support](./service-plugin.md#pagination-and-the-find-getter) - Fall-Through Caching - [`$FeathersVuex` Plugin for Vue](./vue-plugin.md) - [Per-Service Data Modeling](./common-patterns.md#Basic-Data-Modeling-with-instanceDefaults) - [Clone & Commit](./feathers-vuex-forms.md#the-clone-and-commit-pattern) - Simplified Auth - [Per-Record Defaults](./model-classes.md#instancedefaults) - [Data Level Computed Properties](./2.0-major-release.md#getter-and-setter-props-go-on-the-model-classes) - [Improved Relation Support](./2.0-major-release.md#define-relationships-and-modify-data-with-setupinstance) - [Powerful Mixins](./mixins.md) - [Renderless Data Components](./data-components.md) - [Renderless Form Component](./feathers-vuex-forms.md#feathersvuexformwrapper) for Simplified Vuex Forms - [Temporary (Local-only) Record Support](./2.0-major-release.md#support-for-temporary-records) * - New `useFind` and `useGet` Vue Composition API super powers! - [Server-Powered Pagination Support](./service-plugin.md#pagination-and-the-find-action) * - [VuePress Dark Mode Support](https://tolking.github.io/vuepress-theme-default-prefers-color-scheme/) for the Docs `** Improved in v3.0.0` ## License Licensed under the [MIT license](LICENSE). Feathers-Vuex is developed and maintained by [Marshall Thompson](https://www.github.com/marshallswain). ================================================ FILE: docs/auth-plugin.md ================================================ --- title: Auth Plugin --- The Auth module assists setting up user login and logout. ## Setup See the [Auth Setup](/getting-started.html#auth-plugin) section for an example of how to setup the Auth Plugin. ## Breaking Changes in 2.0 The following breaking changes were made between 1.x and 2.0: - The `auth` method is now called `makeAuthPlugin`. ## Configuration You can provide a `userService` in the auth plugin's options to automatically populate the user upon successful login. ## State It includes the following state by default: ```js { accessToken: undefined, // The JWT payload: undefined, // The JWT payload userService: null, // Specify the userService to automatically populate the user upon login. entityIdField: 'userId', // The property in the payload storing the user id responseEntityField: 'user', // The property in the payload storing the user user: null, // Deprecated: This is no longer reactive, so use the `user` getter. See below. isAuthenticatePending: false, isLogoutPending: false, errorOnAuthenticate: undefined, errorOnLogout: undefined } ``` ## Getters Two getters are available when a `userService` is provided to the `makeAuthPlugin` options. - `user`: returns the reactive, logged-in user from the `userService` specified in the options. Returns `null` if not logged in. - `isAuthenticated`: a easy to remember boolean attribute for if the user is logged in. ## Actions The following actions are included in the `auth` module. Login is accomplished through the `authenticate` action. For logout, use the `logout` action. It's important to note that the records that were loaded for a user are NOT automatically cleared upon logout. Because the business logic requirements for that feature would vary from app to app, it can't be baked into Feathers-Vuex. It must be manually implemented. The recommended solution is to simply refresh the browser, which clears the data from memory. - `authenticate`: use instead of `feathersClient.authenticate()` - `logout`: use instead of `feathersClient.logout()` If you provided a `userService` and have correctly configured your `entityIdField` and `responseEntityField` (the defaults work with Feathers V4 out of the box), the `user` state will be updated with the logged-in user. The record will also be reactive, which means when the user record updates (in the users service) the auth user will automatically update, as well. > Note: The Vuex auth store will not update if you use the feathers client version of the above methods. ## Example Here's a short example that implements the `authenticate` and `logout` actions. ```js export default { // ... methods: { login() { this.$store.dispatch('auth/authenticate', { email: '...', password: '...' }) } // ... logout() { this.$store.dispatch('auth/logout') } } // ... } ``` Note that if you customize the auth plugin's `namespace` then the `auth/` prefix in the above example would change to the provided namespace. ================================================ FILE: docs/common-patterns.md ================================================ --- title: Common Patterns --- ## Set the `idField` If you have a "WTF this isn't working" moment while setting up a new service, make sure you've set the `idField` property on your service. In `feathers-vuex@1.x`, the `id` is the default `idField`. You have to manually set `_id`. Starting in `feathers-vuex@2.x`, both the `id` and `_id` fields are supported without any configuration, so you only set the `idField` when your service uses something else. ## Enable debugging You can set `debug: true` in the options to enable some logging to assist with debugging. ## Use the `` and `` components Using the new `` and `` components provides concise access to the best features of `feathers-vuex`, including live queries, reactive lists, custom pagination tracking per component, and fall-through cacheing of local data in the Vuex store. Check out the [Renderless Data Components](./data-components.html) docs for more details. ## Use the `makeFindMixin` and `makeGetMixin` utilities The mixin utilities provide the same functionality as the components, but with more power and flexibility. Check out the [Mixin docs](./mixins.html) for more details. ## Working with TypeScript As of version 2.0, Feathers-Vuex has been rewritten in TypeScript. See [this issue](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/114) for suggestions for with TypeScript helpers. In version 3.0, support for the [Vue Composition API](https://github.com/vuejs/composition-api) was added in the form of the `useFind` and `useGet` utilities. These new utilities provide the best experience for working with TypeScript. This is due to two things: 1. Better general TypeScript support in the Vue Composition API. 1. Consistent object-key naming and types in the new utilities. Read more about how to use them in the [Feathers-Vuex Composition API Docs](./composition-api.md) ## Clearing data upon user logout The best solution is to simply refresh to clear memory. The alternative to refreshing would be to perform manual cleanup of the service stores. Refreshing is much simpler, so it's the officially supported solution. Feel free to read [this issue](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/10) for more suggestions. ## Accessing the store from hooks Because the service's Model [is available](./service-plugin.html#The-FeathersClient-Service) at `service.FeathersVuexModel`, you can access the store inside hooks. This is especially handy if you have some custom attributes in a paginated server response. As an example, this `speeding-tickets` service has a `summary` attribute that comes back in the response. We can ```js import { makeServicePlugin, BaseModel } from '../feathers-client' class SpeedingTicket extends BaseModel { constructor(data, options) { super(data, options) } // Required for $FeathersVuex plugin to work after production transpile. static modelName = 'SpeedingTicket' // Define default properties here static instanceDefaults() { return { vin: '', plateState: '' } } } const servicePath = 'speeding-tickets' const servicePlugin = makeServicePlugin({ Model: SpeedingTicket, service: feathersClient.service(servicePath), servicePath, mutations: { handleSummaryData (state, summaryData) { state.mostRecentSummary = summaryData } } }) feathersClient.service(servicePath) .hooks({ after: { find: [ context => { const { service, result } = context if (result.summary) { service.FeathersVuexModel.store.commit('handleSummaryData', result.summary) } } ] } }) ``` ## Handling custom server responses Sometimes your server response may contain more attributes than just database records and pagination data. You could handle this directly in a component, if it's only needed in that one component, But, if you need it in multiple components, there are better options. Depending on what you need to do, you may be able to solve this by [accessing the store from hooks](#Accessing-the-store-from-hooks). But that solution won't handle a scenario where you need the response data to be already populated in the store. If you need the response data to already be in the store, you can use the [`afterFind` action](./service-plugin.html#afterFind-response). Here's what this looks like: ```js import { makeServicePlugin, BaseModel } from '../feathers-client' class SpeedingTicket extends BaseModel { constructor(data, options) { super(data, options) } // Required for $FeathersVuex plugin to work after production transpile. static modelName = 'SpeedingTicket' // Define default properties here static instanceDefaults() { return { vin: '', plateState: '' } } } const servicePath = 'speeding-tickets' const servicePlugin = makeServicePlugin({ Model: SpeedingTicket, service: feathersClient.service(servicePath), servicePath, actions: { afterFind ({ commit, dispatch, getters, state }, response) { if (response.summary) { commit('handleSummaryData', response.summary) } } }, mutations: { handleSummaryData (state, summaryData) { state.mostRecentSummary = summaryData } } }) ``` ## Reactive Lists with Live Queries Using Live Queries will greatly simplify app development. The `find` getter enables this feature. Here is how you might setup a component to take advantage of them. The next example shows how to setup two live-query lists using two getters. ```js import { mapState, mapGetters, mapActions } from 'vuex' export default { name: 'some-component', computed: { ...mapState('appointments', { areAppointmentsLoading: 'isFindPending' }), ...mapGetters('appointments', { findAppointmentsInStore: 'find' } ), // Query for future appointments queryUpcoming () { return { date: { $gt: new Date() }} }, // Query for past appointments queryPast () { return { date: { $lt: new Date() }} }, // The list of upcoming appointments. upcomingAppointments () { return this.findAppointmentsInStore({ query: this.queryUpcoming }).data }, // The list of past appointments pastAppointments () { return this.findAppointmentsInStore({ query: this.queryPast }).data } }, methods: { ...mapActions('appointments', { findAppointments: 'find' }) }, created () { // Find all appointments. We'll use the getters to separate them. this.findAppointments({ query: {} }) } } ``` in the above example of component code, the `upcomingAppointments` and `pastAppointments` will automatically update. If a new item is sent from the server, it will get added to one of the lists, automatically. `feathers-vuex` listens to socket events automatically, so you don't have to manually wire any of this up! ## Organizing the services in your project You can use the file system to organize each service into its own module. This is especially useful in organizing larger-sized projects. Here's an example `store.js`. It uses Webpack's require.context feature save repetitive imports. See it [here](/getting-started.html#auth-plugin) With the `store.js` file in place, we can start adding services to the `services` folder. - [Learn how to setup the feathers-client.js file](/getting-started.html#feathers-client-feathers-vuex) - [Learn how to setup a Vuex plugin for a Feathers service](/getting-started.html#service-plugins) - [Learn how to setup the auth plugin](/getting-started.html#auth-plugin) ## Actions return reactive store records Previously, when you directly used the response from an action, the individual records were not reactive. This meant that these plain objects wouldn't update when you updated the matching record in the store. ```js methods: { ...mapActions('todos', { findTodos: 'find' }) }, created () { this.findTodos({ query: {} }) .then(response => { const todos = response.data || response // Suppose firstTodo has an id of 'todo-1' const firstTodo = todos[0] // Now, when you update the data in the store... this.$store.state.todos.keyedById['todo-1'].description = 'Updated description' // ... the instance in the `find` response also updates. Yay! console.log(firstTodo.description) // --> 'Updated description' }) } ``` This is a super convenient feature, and it works with all actions (except remove, of course) But be aware that **only the individual records returned are reactive**. The lists, themselves, are not reactive. So if another record comes in from the server that matches the query, the list will not update. For reactive lists, you must use the `find` getter, as shown in the following example. ```js computed: { ...mapGetters('todos', { findTodosInStore: 'find' }) todos () { return this.findTodosInStore({ query: {} }).data } }, methods: { ...mapActions('todos', { findTodos: 'find' }) }, created () { this.findTodos({ query: {} }) .then(response => { // In the find action, the 'todos' array is not a reactive list, but the individual records are. const todos = response.data || response }) } ``` In the above example, the computed `todos` will be a reactive list. This means that when new records are added to the store, the list of todos will automatically update in the UI to include the new data. In summary, you can plan on individual records in the action response data to be reactive, but if you need the actual arrays to be reactive to live queries, use the 'find' getter. ## Basic Data Modeling with `instanceDefaults` See the [instanceDefaults API](./model-classes.html#instancedefaults) ## Handling Non-Reactive Data If you are encountering a scenario where certain properties in your records are not reactive, it's probably because they 1. Are not defined in the `instanceDefaults`. 2. Are getting added to the record after it gets added to the Vuex store. There are two ways to solve this: 1. Add the property to the `instanceDefaults` (see the previous section, above) This tends to be the simplest solution. 2. Make sure the property is added in the responses from the API server. ## Model-Specific Computed Properties You may find yourself in a position where model-specific computed properties would be very useful. [github issue](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/163). In Feathers-Vuex 1.7, these could be specified in the `instanceDefaults`. As of 2.0, they are specified directly on each Model class: ```js class Post extends BaseModel { // Required for $FeathersVuex plugin to work after production transpile. static modelName = 'Post' // Define default properties here static instanceDefaults() { return { description: '', isComplete: false, comments: [], } } // Specify computed properties as regular class properties get numberOfCommenters () { // Put your logic here. }, set someOtherProp () { // Setters also work } } ``` ## Relationships for Populated Data If you're looking for a great solution for populating data to work with Feathers-Vuex, check out [feathers-graph-populate](https://feathers-graph-populate.netlify.app/). A common task with almost any API is properly handling relationships between endpoints. Imagine an API where you have `/todos` and `/users` services. Each todo record can belong to a single user, so a todo has a `userId`. ```js // GET todos/1 { id: 1, description: 'Learn about the health benefits of a low-carb diet.', isComplete: false, userId: 5 } ``` And a user response looks like this: ```js // GET users/5 { id: 5, name: 'Marshall', username: 'marshallswain' email: 'marshall@ilovehealthy.com' } ``` Suppose a requirement is put on the `/todos` service to populate the `user` in the response. (As a super handy side note, this task is pretty easy when using [Matt Chaffe's](https://github.com/mattchewone) magical, efficient [feathers-shallow-populate hook](https://www.npmjs.com/package/feathers-shallow-populate)) So now the todo response looks like this: ```js { id: 1, description: 'Learn about the health benefits of a low-carb diet.', isComplete: false, userId: 5, user: { id: 5, name: 'Marshall', username: 'marshallswain' email: 'marshall@ilovehealthy.com' } } ``` Can you see the problem that will occur with this response? When this record is put into the `/todos` store, it will contain a copy of the user record. But we already have the user record in the `/users` store. And what happens when the user data changes? Now it's out of sync. To keep it in sync, you might have to manually listen for `users updated` & `users patched` events. Then you might have to write a custom mutation to update the user record attached to every applicable `todo` record. This gets messy, fast! There's an easier way to solve this problem. Use the new [`setupInstance` method on Model classes](/model-classes.html#setupinstance). ```js import feathersClient, { makeServicePlugin, BaseModel } from '../feathers-client' class Todo extends BaseModel { // Required for $FeathersVuex plugin to work after production transpile. static modelName = 'Todo' // Define default properties here static instanceDefaults() { return { email: '', password: '' } } // Updates `data.user` to be an instance of the `User` class. static setupInstance(data, { models }) { if (data.user) { data.user = new models.api.User(data.user) } return data } } const servicePath = 'todos' const servicePlugin = makeServicePlugin({ Model: Todo, service: feathersClient.service(servicePath), servicePath }) ``` When this record is instantiated, the `user` attribute will first be turned into a User [model instance](./model-classes.html), stored properly in the `/users` store. The `todo.user` attribute will be a reference to that user. No more duplicate data! Here's an example of how to set this up. There's another amazing benefit from these relationships. Because `feathers-vuex` listens to real-time events and keeps data up to date, when the user record changes, the `todo.user` automatically updates! ### Handling Sequelize Joins See [this issue](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/404) for a discussion on how to handle joins with Sequelize. It's important to specify `raw: false`, as shown in [this comment](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/404#issuecomment-571774598). ### Workflow for Saving Model Associations A great issue was opened on GitHub about the [Workflow for clone and save Model with associations](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/278). That's a great issue to read to get familiar with the workflow. ## Form Binding Use the Model classes to reduce the boilerplate required to work with forms and Vuex, even in strict mode! Every model instance has a `.clone()` method which can be used to get a fully-reactive copy of the record in the store. Here is a very simple version of how you could bind to a form and submit new data to the server. ```vue ``` ## Multiple Copies The previous version of `feathers-vuex` was hard-coded to allow for a single `current` record and one copy. It was pretty easy to hit that limit. This new release allows for keeping many more copies, one copy per stored record. To make it easier to comply with Vuex's `strict` mode, copies are not kept in the store by default, but are instead kept on `Model.copiesById`. You can make changes to the copies without having to make custom mutations, then you can commit them back into the store: ```js const { Todo } = this.$FeathersVuex // Create two records in the store (since they have ids, they get stored) const todo = new Todo({ id: 1, description: 'Become more aware of others.'}) const todo2 = new Todo({ id: 2, description: 'Heal one ailments through healthy eating.'}) // Create a deep-cloned copies in Todo.copiesById const todoCopy = todo.clone() const todoCopy2 = todo2.clone() // Try to clone a copy, and fail. todoCopy.clone() // --> Error: You cannot clone a copy. todoCopy2.clone() // --> Error: You cannot clone a copy. // Modify the copies. todoCopy.description.replace('others', 'self') todoCopy2.description.replace('one', 'all') // and update the original records todoCopy.commit() todoCopy2.commit() ``` You can use the `keepCopiesInStore` option to make this service keep all of its copies in `state.copiesById`. Remember that to comply with Vuex `strict` mode (if that's a concern for you), you'll have to write custom mutations. If it's not a concern (maybe you're the sole developer or whatever reason), you could technically turn off `strict` mode, enable `keepCopiesInStore`, and modify them however you desire, ignoring custom mutations. ```js import Vue from 'vue' import Vuex from 'vuex' import feathersVuex from 'feathers-vuex' import feathersClient from './feathers-client' const { service, auth, FeathersVuex } = feathersVuex(feathersClient, { idField: '_id' }) Vue.use(FeathersVuex) Vue.use(Vuex) export default new Vuex.Store({ plugins: [ service('todos', { keepCopiesInStore: true, instanceDefaults: { description: '', complete: false } }) ] }) ``` ## Enable Debug Logging If items aren't not getting added to the store properly, try setting the `debug` option on the `makeServicePlugin` to `true`. It enables some additional logging that may be useful for troubleshooting. ## Full nuxt example In this example we will create a nuxt configuration with all the features discribed in the [Nuxt Section](./nuxt.md). Our application is hosted in 2 different endpoints, one for the feathers api and another with our nuxt SSR, and we will also implement a permission system that will prevent users without an `admin` permission to get into some pages. For that you will need to [customize your payload](https://docs.feathersjs.com/cookbook/authentication/stateless.html#customizing-the-payload) in your feathers api. ```js // nuxt.config.js export default { env: { API_URL: process.env.API_URL || 'http://localhost:3030' }, router: { middleware: [ 'feathers' ] }, plugins: [ { src: '~/plugins/feathers-vuex.js' } ], modules: [ 'nuxt-client-init-module' ], build: { transpile: [ 'feathers-vuex' ] } } ``` The `feathers` middleware will redirect any user in a non public page to the login, but will also redirect a loged user away from any public pages. ```js // ~/middleware/feathers.js export default function ({ store, redirect, route }) { const { auth } = store.state if (auth.publicPages.length > 0 && !auth.publicPages.includes(route.name) && !auth.payload) { return redirect('/login') } else if (auth.publicPages.length > 0 && auth.publicPages.includes(route.name) && auth.payload) { return redirect('/feed') } } ``` The `admin` middleware will redirect any user that is not loged in or do not have the `admin` permission to a `not-permited` page. ```js // ~/middleware/admin.js export default function ({ store, redirect }) { const { auth } = store.state const permissions = auth.payload.permissions if ( !auth.payload || !permissions.includes('admin') ) { return redirect('/not-permited') } } ``` Add the `admin` middleware to the administrative pages ```js // ~/pages/** export default { ... middleware: ['admin'] ... } ``` In the feathers client configuration we will use a different transport for the nuxt-server > api comunication and the nuxt-client > api. When we are making request on the server we do not need the socket io realtime messaging system so we can use an rest configuration for better performance. ```js // ~/plugins/feathers.js import feathers from '@feathersjs/feathers' import rest from '@feathersjs/rest-client' import axios from 'axios' import socketio from '@feathersjs/socketio-client' import auth from '@feathersjs/authentication-client' import io from 'socket.io-client' import { CookieStorage } from 'cookie-storage' import { iff, discard } from 'feathers-hooks-common' import feathersVuex, { initAuth, hydrateApi } from 'feathers-vuex' // Get the api url from the environment variable const apiUrl = process.env.API_URL let socket let restClient // We won't use socket to comunicate from server to server if (process.client) { socket = io(apiUrl, { transports: ['websocket'] }) } else { restClient = rest(apiUrl) } const transport = process.client ? socketio(socket) : restClient.axios(axios) const storage = new CookieStorage() const feathersClient = feathers() .configure(transport) .configure(auth({ storage })) .hooks({ before: { all: [ iff( context => ['create', 'update', 'patch'].includes(context.method), discard('__id', '__isTemp') ) ] } }) export default feathersClient // Setting up feathers-vuex const { makeServicePlugin, makeAuthPlugin, BaseModel, models, FeathersVuex } = feathersVuex( feathersClient, { serverAlias: 'api', // optional for working with multiple APIs (this is the default value) idField: '_id', // Must match the id field in your database table/collection whitelist: ['$regex', '$options'], enableEvents: process.client // Prevent memory leak } ) export { makeAuthPlugin, makeServicePlugin, BaseModel, models, FeathersVuex, initAuth, hydrateApi } ``` I prefere install the `FeathersVuex` plugin in a separate file, it's more consistent with nuxt patterns. ```js // ~/plugins/feathers-vuex.js import Vue from 'vue' import { FeathersVuex } from './feathers' Vue.use(FeathersVuex) ``` Configure any service you want on `~/store/services/*.js`. ```js // ~/store/services/users.js import feathersClient, { makeServicePlugin, BaseModel } from '~/plugins/feathers' class User extends BaseModel { constructor(data, options) { super(data, options) } // Required for $FeathersVuex plugin to work after production transpile. static modelName = 'User' // Define default properties here static instanceDefaults() { return { email: '', password: '', permissions: [] } } } const servicePath = 'users' const servicePlugin = makeServicePlugin({ Model: User, service: feathersClient.service(servicePath), servicePath }) // Setup the client-side Feathers hooks. feathersClient.service(servicePath).hooks({ before: { all: [], find: [], get: [], create: [], update: [], patch: [], remove: [] }, after: { all: [], find: [], get: [], create: [], update: [], patch: [], remove: [] }, error: { all: [], find: [], get: [], create: [], update: [], patch: [], remove: [] } }) export default servicePlugin ``` Create your nuxt store with the right nuxt pattern, exporting an Vuex store will be deprecated on nuxt 3. ```js // ~/store/index.js import { makeAuthPlugin, initAuth, hydrateApi, models } from '~/plugins/feathers' const auth = makeAuthPlugin({ userService: 'users', state: { publicPages: [ 'login', 'signup' ] }, actions: { onInitAuth ({ state, dispatch }, payload) { if (payload) { dispatch('authenticate', { strategy: 'jwt', accessToken: state.accessToken }) .then((result) => { // handle success like a boss console.log('loged in') }) .catch((error) => { // handle error like a boss console.log(error) }) } } } }) const requireModule = require.context( // The path where the service modules live './services', // Whether to look in subfolders false, // Only include .js files (prevents duplicate imports`) /.js$/ ) const servicePlugins = requireModule .keys() .map(modulePath => requireModule(modulePath).default) export const modules = { // Custom modules } export const state = () => ({ // Custom state }) export const mutations = { // Custom mutations } export const actions = { // Custom actions nuxtServerInit ({ commit, dispatch }, { req }) { return initAuth({ commit, dispatch, req, moduleName: 'auth', cookieName: 'feathers-jwt' }) }, nuxtClientInit ({ state, dispatch, commit }, context) { hydrateApi({ api: models.api }) if (state.auth.accessToken) { return dispatch('auth/onInitAuth', state.auth.payload) } } } export const getters = { // Custom getters } export const plugins = [ ...servicePlugins, auth ] ``` ================================================ FILE: docs/composition-api.md ================================================ --- title: Composition API sidebarDepth: 3 --- # Feathers-Vuex Composition API In addition to the Renderless Components and the Mixins, Feathers-Vuex includes utilities that let you take advantage of the [Vue Composition API](https://github.com/vuejs/composition-api). It's important to note that the `@vue/composition-api` plugin must be registered BEFORE you import `App.vue`. This means that you need to modify your `main.js` file to look like this: ```js import Vue from 'vue' import VueCompositionApi from '@vue/composition-api' // Register the Composition API plugin BEFORE you import App.vue Vue.use(VueCompositionApi) import App from './App.vue' import router from './router' import store from './store/store' import './plugins/plugins' Vue.config.productionTip = true new Vue({ router, store, render: h => h(App) }).$mount('#app') ``` If you forget to register the plugin, first, you will see a console warning from Vue stating `[Vue warn]: Error in data(): "Error: [vue-composition-api] must call Vue.use(plugin) before using any function.". To fix this, make sure to register the plugin BEFORE you call any Composition utility functions, as shown above. ## Setup Before you can use the `useFind` and `useGet` composition functions, you'll need to [install the Vue Composition API](https://github.com/vuejs/composition-api#Installation) plugin. ## Detour: Reading a TypeScript interface The next few sections show various TypeScript interfaces, which are basically shorthand descriptions of the types of data that make up a variable. In this case, they're used to show the `options` object which can be passed to each of the composition api utilities. If this is your first time with interfaces, here's a quick primer as an alternative to reading the [TypeScript interface docs](https://www.typescriptlang.org/docs/handbook/interfaces.html): - In the [first interface example](#options), below, `UseFindOptions` is the name of the interface, similar to naming any other variable. When using TypeScript, you can import and pass interfaces around like variables. - Each line of the interface describes a property. - The part before the `:` is the name of the property. - The part after the `:` describes what type of variable it can be. - You can look at any `|` after the `:` as a conditional "or" - Any property followed by a `?` is optional. - Any property not followed by a `?` is required. ## useFind The `useFind` utility reduces boilerplate for querying with fall-through cache and realtime updates. To get started with it you provide a `model` class and a computed `params` object. Let's use the example of creating a User Guide, where we need to pull in the various `Tutorial` records from our `tutorials` service. We'll keep it simple in the template and just show a list of the names. ```html ``` Let's review each of the numbered comments, above: 1. Get a reference to the model class. With the Vue Composition API, there's no `this` object. It has been replaced by the context object. So, only when using the composition API, the `$FeathersVuex` object is found at `context.root.$FeathersVuex` 2. Create a computed property for the params. Return an object with a nested `query` object. ### Options Since we learned earlier [how to read a TypeScript interface](#detour-reading-a-typescript-interface), let's look at the TypeScript definition for the `UseFindOptions` interface. ```ts interface UseFindOptions { model: Function params: Params | Ref fetchParams?: Params | Ref queryWhen?: Ref qid?: string immediate?: boolean } ``` And here's a look at each individual property: - `model` must be a Feathers-Vuex Model class. The Model's `find` and `findInStore` methods are used to query data. - `params` is a FeathersJS Params object OR a Composition API `ref` (or `computed`, since they return a `ref` instance) which returns a Params object. - When provided alone (without the optional `fetchParams`), this same query is used for both the local data store and the API requests. - Explicitly returning `null` will prevent an API request from being made. - You can use `params.qid` to dynamically specify the query identifier for any API request. The `qid` is used for tracking pagination data and enabling the fall-through cache across multiple queries. - Set `params.paginate` to `true` to turn off realtime updates for the results and defer pagination to the API server. Pagination works the same as for the [makeFindMixin pagination](./mixins.html#pagination-with-fall-through-cacheing). - Set `params.debounce` to an integer and the API requests will automatically be debounced by that many milliseconds. For example, setting `debounce: 1000` will assure that the API request will be made at most every 1 second. - Set `params.temps` to `true` to include temporary (local-only) items in the results. Temporary records are instances that have been created but not yet saved to the database. - Set `params.copies` to `true` to include cloned items in the results. The queried items get replaced with the corresponding copies from `copiesById` - `fetchParams` This is a separate set of params that, when provided, will become the params sent to the API server. The `params` will then only be used to query data from the local data store. - Explicitly returning `null` will prevent an API request from being made. - `queryWhen` must be a `computed` property which returns a `boolean`. It provides a logical separation for preventing API requests *outside* of the `params`. - `qid` allows you to specify a query identifier (used in the pagination data in the store). This can also be set dynamically by returning a `qid` in the params. - `immediate`, which is `true` by default, determines if the internal `watch` should fire immediately. Set `immediate: false` and the query will not fire immediately. It will only fire on subsequent changes to the params. ### Returned Attributes Notice the `tutorialsData` in the previous example. You can see that there's an `tutorialsData.items` property, which is returned at the bottom of the `setup` method as `tutorials`. There are many more attributes available in the object returned from `useFind`. We can learn more about the return values by looking at its TypeScript interface, below. ```ts interface UseFindData { items: Ref paginationData: Ref servicePath: Ref qid: Ref isPending: Ref haveBeenRequested: Ref haveLoaded: Ref error: Ref debounceTime: Ref latestQuery: Ref isLocal: Ref find: Function } ``` Let's look at the functionality that each one provides: - `items` is the list of results. By default, this list will be reactive, so if new items are created which match the query, they will show up in this list automagically. - `servicePath` is the FeathersJS service path that is used by the current model. This is mostly only useful for debugging. - `isPending` is a boolean that indicates if there is an active query. It is set to `true` just before each outgoing request. It is set to `false` after the response returns. Bind to it in the UI to show an activity indicator to the user. - `haveBeenRequested` is a boolean that is set to `true` immediately before the first query is sent out. It remains true throughout the life of the component. This comes in handy for first-load scenarios in the UI. - `haveLoaded` is a boolean that is set to true after the first API response. It remains `true` for the life of the component. This also comes in handy for first-load scenarios in the UI. - `isLocal` is a boolean that is set to true if this data is local only. - `qid` is currently the primary `qid` provided in params. It might become more useful in the future. - `debounceTime` is the current number of milliseconds used as the debounce interval. - `latestQuery` is an object that holds the latest query information. It populates after each successful API response. The information it contains can be used to pull data from the `paginationData`. - `paginationData` is an object containing all of the pagination data for the current service. - `error` is null until an API error occurs. The error object will be serialized into a plain object and available here. - `find` is the find method used internally. You can manually make API requests. This is most useful for when you have `paginate: true` in the params. You can manually query refreshed data from the server, when desired. ### Working with Refs Pay special attention to the properties of type `Ref`, in the TypeScript interface, above. Those properties are Vue Composition API `ref` instances. This means that you need to reference their value by using `.value`. In the next example the `completeTodos` and `incompleteTodos` are derived from the `todos`, using `todos.value` ```html ``` ### Comparison to `makeFindMixin` If you have already used the `makeFindMixin`, the `useFind` composition function will be very familiar, since it offers the same functionality in a more powerful way. There are a few differences, though. 1. `useFind` is more TypeScript friendly. Since the mixins depended on adding dynamic attribute names that wouldn't overlap, TypeScript tooling and autocomplete weren't very effective. The attributes returned from `useFind` are always consistent. 1. Instead of providing a service name, you provide a service Model from the `$FeathersVuex` Vue plugin. 1. The default behavior of `useFind` is to immediately query the API server. The `makeFindMixin`, by default, would wait until the watcher noticed the change. This is to match the default behavior of `watch` in the Vue Composition API. You can pass `{ immediate: false }` in the `useFind` options, which will be passed directly to the internal `watch` on the params. Note that with the Vue Options API (aka the only way to write components in Vue 2.0) the models are found in `this.$FeathersVuex`. With the Vue Composition API, this object is now at `context.root.$FeathersVuex`. ## useGet The `useGet` Composition API utility provides the same fall-through cache functionality as `useFind`. It has a slightly simpler API, only requiring a `model` and `id` instead of the `params` object. Still, the `params` object can be used to send along additional query parameters in the request. Below is an example of how you might use the `useGet` utility. ```html ``` As promised, here is the example code for the `PaginationUi` component used above. It includes some TailwindCSS utility classes in the markup in order to show how one might use the `canPrev` and `canNext` properties. Keep in mind that this is just an example. You can provide whatever experience you want for your users by creating your own component. ```html ``` ## Patterns & Examples ### Server-Side Pagination Similar to what was introduced with the `makeFindMixin` in Feathers-Vuex 2.0, the `useFind` API supports server-side pagination. It is enabled by passing `paginate: true` in the `params` (or the `fetchParams` if you're using separate queries). For an overview of how it works, refer to the [makeFindMixin pagination docs](./mixins.html#pagination-with-fall-through-cacheing). ### Simultaneous Queries Let's look at an example where we have two separate tables and we want live-queried lists for both of them. This example will show a component for a doctor's office that pulls up a patient by `id` using `useGet` then retrieves all of the patient's `appointments` using `useFind`. ```html ``` ### Deferring Queries In the previous example, the requests for the `patient` and `appointments` are made at the same time because the user's `id` is available, already. What if we were required to load `appointments` after the `patient` record finished loading? We could change the `appointmentsParams` to return `null` until the `patient` record becomes available, as shown in the following example: ```html ``` Reviewing the above snippet, while there is no `patient` record, the `appointmentsParams` computed property returns `null` at comment `(1)`. This will prevent any query from going out to the API server. Once the `patient` has loaded, the full params object is returned at comment `(2)`. This allows the `useFind` utility to make the request. ### Showing Loading State This next example builds on the previous one and adds loading state for both the `patient` and the `appointments`. ```html ``` ### Using queryWhen The `queryWhen` option for both `useFind` and `useGet` comes in handy when you want to conditional prevent API queries. One use case for this is to prevent extra queries by checking if an item already exists in the vuex store. This next example shows how to stop the `get` request if you already have a patient record with the current `id`. ```html ``` In the above example, the `patientQueryWhen` computed property will return `true` if we don't already have a `Patient` record in the store with the current `props.id`. While you could also achieve similar results by performing this logic inside of a `params` computed property, the `queryWhen` option works great as a "master override" to prevent unneeded queries. ### Routing with useGet Apps will commonly have one or more routes with an `:id` param. This might be for viewing or editing data. Vue Router has a feature that makes it easy to write reusable components without having to directly reference the `$route` object. The key is to set the `props` attribute in a route definition to `true`. Here's an example route: ```js // router.js import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default new Router({ routes: [ { name: 'Post View', path: '/posts/:id', component: () => import(/* webpackChunkName: "posts" */ './views/Post.vue'), props: true } ] }) ``` Now, the `Post.vue` file only requires to have a `prop` named `id`. Vue Router will pass the params from the route as props to the component. See the [first useGet example](#useget) for a component that would work with the above route. The vue-router documentation has more information about [Passing Props to Route Components](https://router.vuejs.org/guide/essentials/passing-props.html#passing-props-to-route-components) ### Composing with Model types Both `useGet` and `useFind` have an optional type parameter for the Model type which is used as the type for the returned item(s). ```ts // Destructure Model class from global models object const { User } = Vue.$FeathersVuex.api // use useGet with User Model useGet(/* ... */) // use useFind with User Model useFind(/* ... */) ``` ## Conventions for Development ### Params are Computed You might notice throughout these docs that the params are consistently shown as `computed` properties. For long-term maintainability, this is the recommended practice. Computed properties are read-only, so you can't push changes into them. This encourages declarative programming. Think of a declarative query as having all of the instructions it needs to pull in data from whatever sources are required to build the query object. Writing declarative params will assist you in avoiding complex conditional conflicts as queries become more complex. In contrast, an imperatively-written query would be a reactive object that you directly modify. Think of imperative as pushing information into the query. eg: `params.query.user = props.userId`. When you have a lot of imperative code pushing parameters into the query, it's really easy to create conflicting logic. So, keep in mind that while Feathers-Vuex will definitely handle an imperative-style query, your code will likely be less maintainable over the long run. ### Naming Variables Having a variable naming convention can really assist the developer onboarding process and long run ease of use. Here are some guidelines that could prove useful while using the composition API utilities: - Params for `useFind` result in a list of records, and should therefore indicate plurality. - When used, params for `useGet` result in a single record, and should indicate singularity. ```js import { computed } from '@vue/composition-api' import { useFind, useGet } from 'feathers-vuex' export default { name: 'MyComponent', props: { id: { type: String, required: true } }, setup(props, context) { const { Comment } = context.root.$FeathersVuex.api // Plural "comments" in the params for useFind const commentsParams = computed(() => { return { query: {} } }) const commentsData = useFind({ model: Comment, params: commentsParams }) const { items: comments } = commentsData // Singular "comment" in the params for useGet const commentParams = computed(() => { return { query: {} } }) const commentData = useGet({ model: Comment, id: props.id, params: commentParams }) const { item: comment } = commentData return { comments, comment } } } ``` Variable naming becomes even more important when one service consumes the results of a previous service to make a query. Note: the destructuring of `commentsData` and `commentData`, above, could happen on the same line as `useFind` and `useGet`, but it's a bit more clear in the example to split it into separate steps. For users who are accustomed to destructuring, it makes perfect sense to do so: ```js // Destructure and rename "item" to "comment" in the same line as the call to `useGet` const { item: comment } = useGet({ model: Comment, id: props.id, params: commentParams }) return { comment } ``` ================================================ FILE: docs/data-components.md ================================================ --- title: Renderless Data Components sidebarDepth: 3 --- # Renderless Data Components There are three renderless data provider components: ``, `` and ``. They simplify performing queries against the store and/or the API server. They make the data available inside each component's default slot. To see why you might want to use these components, below are two example components that are functionally equivalent. Here's what it looks like to use the new component: ```html ``` The above example is functionally equivalent to this much longer example which doesn't use the new component: ```html ``` > To level up your skills, consider this content by Adam Wathan. He wrote a terrific *free* article about [Renderless Components in Vue.js](https://adamwathan.me/renderless-components-in-vuejs/). I highly recommend you read it. He also created the *paid/premium* [Advanced Vue Component Design](https://adamwathan.me/advanced-vue-component-design/) course. His material influenced the creation of this component. ## FeathersVuexFind The `FeathersVuexFind` component retrieves data from the API server, puts it in the Vuex store, then transparently retrieves the live, reactive data from the store and displays it to the user. ### Example ```vue
{{users}}
``` ### Props - `service {String}` - **required** the service path. This must match a service that has already been registered with FeathersVuex. - `query {Object}` **use `params` instead** - the query object. If only the `query` attribute is provided, the same query will be used for both the `find` getter and the `find` action. See the `fetchQuery` attribute for more information. When using server-side pagination, use the `fetchQuery` prop and the `query` prop for querying data from the local store. If the query is `null` or `undefined`, the query against both the API and store will be skipped. The find getter will return an empty array. - `watch {String|Array}` - specify the attributes of the `params` or `fetchParams` to watch. Pass 'params' to watch the entire params object. Pass 'params.query.name' to watch the 'name' property of the query. Watch is turned off by default, so the API server will only be queried once, by default. **Default: []** - `fetchQuery {Object}` **use `fetchParams` instead** - when provided, the `fetchQuery` serves as the query for the API server. The `query` param will be used against the service's local Vuex store. **Default: undefined** - `params {Object}` - the params object. If only the `params` attribute is provided, te same params will be used for both the `find` getter and the `find` action. See the `fetchParams` attribute for more information. - `fetchParams {Object}` - when provided, the `fetchParams` servers as the params for the API server. The `params` will be used against the service's local Vuex store. - `queryWhen {Boolean|Function}` - the query to the server will only be made when this evaluates to true. **Default: true** - `local {Boolean}` - when set to true, will only use the `query` prop to get data from the local Vuex store. It will disable queries to the API server. **Default:false** - `editScope {Function}` - a utility function that allows you to modify the scope data, and even add attributes to it, before providing it to the default slot. You can also use it to pull data into the current component's data (though that may be less recommended, it can come in handy). See the "Scope Data" section to learn more about what props are available in the scope object. **Default: scope => scope** - `temps {Boolean}` **use `params: { query: {}, temps: true }` instead** - Enable `temps` to include temporary records (from `state.tempsById`) in the find getter results. **Default: false** - `qid {String}` - The query identifier used for storing pagination data in the Vuex store. See the service module docs to see what you'll find inside. The default value is a random 10-character string. This means that by default, in theory, no two components will share the same pagination data, nor will they overwrite each other's pagination data. You can, of course, force them to use the same pagination data by giving them both the same `qid`, if there's a use case for that. **Default: randomString(10)** ### Scope Data - `items {Array}` - The resulting array of records for find operations. - `isFindPending {Boolean}` - When there's an active request to the API server, this will be `true`. This is not the same as the `isFindPending` from the Vuex state. The value in the Vuex state is `true` whenever **any** component is querying data from that same service. This `isFindPending` attribute is specific to each component instance. - `pagination {Object}` - pagination data from the Vuex store, keyed by the `qid` attribute. By default, this will be specific to this component instance. (If you find a use case for sharing pagination between component instances, you can give both components the same `qid` string as a prop.) - `queryInfo {Object}` - the queryInfo for the `pagination` object. Includes the `total` prop for server side pagination - `pageInfo {Object}` - the pageInfo includes the queried ids and is necessary for server side pagination ## FeathersVuexGet The `FeathersVuexGet` component allows fetching data from directly inside a template. It makes the slot scope available to the child components. Note that in `feathers-vuex@3.3.0` the component now includes support for `params` and `fetchParams` props. These are meant to replace the `query` and `fetchQuery` props. The `params` allow you, for example, to configure a project to pass custom params to the server. This would require use of custom hooks. ### Example ```html ``` ### Props - `service {String}` - **required** the service path. This must match a service that has already been registered with FeathersVuex. - `id {Number|String}` - when performing a `get` request, serves as the id for the request. This is automatically watched, so if the `id` changes, an API request will be made and the data will be updated. **Default: undefined** - `query {Object}` **use `params` instead** - the query object. If only the `query` attribute is provided, the same query will be used for both the `get` getter and the `get` action. See the `fetchQuery` attribute for more information. When using server-side pagination, use the `fetchQuery` prop and the `query` prop for querying data from the local store. If the query is `null` or `undefined`, the query against both the API and store will be skipped. The find getter will return an empty array. - `watch {String|Array}` - specify the attributes of the `params` or `fetchParams` to watch. Pass 'params' to watch the entire params object. Pass 'params.query.name' to watch the 'name' property of the query. Watch is turned off by default, so the API server will only be queried once, by default. The only exception is for the `id` prop. The `id` prop in the `FeathersVuexGet` component is always watched. **Default: []** - `fetchQuery {Object}` **use `fetchParams` instead** - when provided, the `fetchQuery` serves as the query for the API server. The `query` param will be used against the service's local Vuex store. **Default: undefined** - `params {Object}` - the params object. If only the `params` attribute is provided, te same params will be used for both the `get` getter and the `get` action. See the `fetchParams` attribute for more information. - `fetchParams {Object}` - when provided, the `fetchParams` servers as the params for the API server. The `params` will be used against the service's local Vuex store. - `queryWhen {Boolean|Function}` - the query to the server will only be made when this evaluates to true. **Default: true** - `local {Boolean}`: when set to true, will only use the `params` prop to get data from the local Vuex store. It will disable queries to the API server. **Default:false** - `editScope {Function}` - a utility function that allows you to modify the scope data, and even add attributes to it, before providing it to the default slot. You can also use it to pull data into the current component's data (though that may be less recommended, it can come in handy). See the "Scope Data" section to learn more about what props are available in the scope object. **Default: scope => scope** ### Scope Data - `item {Object}` - The resulting record for the get operation. - `isGetPending {Boolean}` - When there's an active request to the API server, this will be `true`. This is not the same as the `isGetPending` from the Vuex state. The value in the Vuex state is `true` whenever **any** component is querying data from that same service. This `isGetPending` attribute is specific to each component instance. ## FeathersVuexCount The `FeathersVuexCount` component allows displaying a count of records. It makes the slot scope available to the child components. It adds `$limit: 0` to the passed params in the background. This will only run a (fast) counting query against the database. > **Note:** it only works for services with enabled pagination! ```vue {{ total }} ``` ### Props - `service {String}` - The path of the service - `params {Object}` - The params object passed to the `count` getter/action. - `fetchParams {Object}` - A seperate params object for the `count` action - `queryWhen {Boolean|Function(params)}` - the query to the server will only be made when this evaluates to true. **Default: true** - `watch {String|Array}` - specify the attributes of the `params` or `fetchParams` to watch. Pass 'params' to watch the entire params object. Pass 'params.query.name' to watch the 'name' property of the query. Watch is turned off by default, so the API server will only be queried once, by default. **Default: []** - `local {Boolean}`: when set to true, will only use the `params` prop to get data from the local Vuex store. It will disable queries to the API server. **Default:false** ### Scope Data - `total {Number}` - The number of found records. - `isCountPending {Boolean}` - When there's an active request to the API server, this will be `true`. ## A note about the internal architecture These components use Vuex getters (to query data from the local store) and actions (to query data from the API server). When a `params` or `id` is provided, the components pull data from the API server and put it into the store. That same `params` or `id` is then used to pull data from the local Vuex store. Keep this in mind, especially when attempting to use server-side pagination. To use server-side pagination, use the `params` prop for pulling data from the local vuex store, then use the `fetchParams` prop to retrieve data from the API server. ## Registering the components These components are automatically registered globally when using the Feathers-Vuex Vue plugin. If you prefer to manually register the component, pass `{ components: false }` as options when using the FeathersVuex Vue plugin, then do the following: ```js import { FeathersVuexFind, FeathersVuexGet, FeathersVuexCount } from 'feathers-vuex' // in your component components: { FeathersVuexFind, FeathersVuexGet, FeathersVuexCount } // or globally registered Vue.component('FeathersVuexFind', FeathersVuexFind) Vue.component('FeathersVuexGet', FeathersVuexGet) Vue.component('FeathersVuexCount', FeathersVuexCount) ``` ## Scope Data When using these components, the scope data will become available to the `FeathersVuexFind`, `FeathersVuexGet` and `FeathersVuexCount` tags. It's accessible using the `v-slot="props"` attribute: ```html
{{props.items}}
``` It's also possible to modify the scope data by passing a function as the `edit-scope` prop. See the example for [modifying scope data](#Modify-the-scope-data) ### Destructuring props Use the object destructuring syntax to pull specific variables out of the `v-slot` object. In the following example, instead of using `v-slot="props"`, it directly accesses the `items` prop through destructuring: ```html
{{items}}
``` ### Renaming props with destructuring You can also rename scope props through the Object destructuring syntax. The `v-slot` in the next example shows how to give the items a more-descriptive name: ```html
{{categories}}
``` ## Usage Examples #### A basic find all In this example, only the `service` attribute is provided. There is no `query` nor `id` provided, so no queries are made. So `props.items` in this example returns an empty array. ```html
{{props.items}}
``` #### Fetch data from the API and the same data from the Vuex store This example fetches data from the API server because a query was provided. Internally, this same `query` is used for both the `find` action and the `find` getter. Read other examples to see how to use distinct queries. Be aware that if you use pagination directives like `$skip` or `$limit`, you must use two queries to get the records you desire. ```html
{{props.items}}
``` #### Only get data from the local Vuex store If you've already pulled a bunch of data from the server, you can use the `local` prop to only query the local data: ```html
{{props.items}}
``` #### Watch the query and re-fetch from the API Sometimes you want to query new data from the server whenever the query changes. Pass an array of attribute names to the `watch` attribute re-query whenever upon change. This example watches the entire query object: ```html
{{props.items}}
``` This next example watches a single prop from the query: ```html
{{props.items}}
``` You can also provide an array of strings to watch multiple properties: ```html
{{props.items}}
``` #### Use a distinct `params` and `fetchParams` In this scenario, the `fetchParams` is be used to grab a larger dataset from the API server (all todos with a matching `userId`). The `params` is used by the `find` getter to display a subset of this data from the store. If the `isComplete` attribute gets set to `true`, only completed todos will be displayed. Since a `fetchParams` is provided, the `watch` strings will be modified internally to watch the `fetchParams` object. This means if you are watching `params.query.userId` and you add a `fetchParams`, the component is smart enough to know you meant `fetchParams.query.userId`. You don't have to rewrite your `watch` attribute after adding a `fetchParams` prop. ```html ``` #### Modify the scope data The `edit-scope` function allows you to modify the scope before passing it down to the default slot. This feature can be super useful for preparing the data for the template. The `prepareCategories` method in this next example adds two properties to the scope data, which are used to create a nested category structure: ```html ``` #### server-side pagination When you want to use server-side pagination you need to pass the ids from the server to vuex. It can be done by a combination of `params`, `fetchParams` and `editScope` as described below. The `fetchParams`-prop is only computed after items from the server arrived. The ids for the `find` getter as well as the total amount of available values `total` are extracted by the `edit-scope` function and stored in `data`: ```html ``` #### Query when certain conditions are met Sometimes you only want to query the API server when certain conditions are met. This example shows how to query the API server when the `userSearch` has as least three characters. This property does not affect the internal `find` getter, so the `items` will still update when the `userSearch` property has fewer than three characters, just no API request will be made. The `isFindPending` attribute is used to indicate when data is being loaded from the server. ```html ``` #### Use a get request You can perform `get` requests with the `FeathersVuexGet` component and its `id` property. In the next example, when the `selectedUserId` changes, a get request will automatically fetch and display the matching user record. It also shows how to use the `isGetPending` prop to update the UI ```html
loading...
{{currentUser}}
``` ================================================ FILE: docs/example-applications.md ================================================ --- title: Example Applications sidebarDepth: 3 --- # Example Applications On this page you will find any example applications using Feathers-Vuex that have been shared by the community. If there's something you would like to see here, feel free to make a PR to add it to the [Community Examples list](#community-examples). ## Feathers Chat The [Feathers Chat Example for Feathers Vuex](https://github.com/feathersjs-ecosystem/feathers-chat-vuex) has been updated to `feathers-vuex@3.x` and everything has been rewritten with the Vue composition API. The old repo is now available at [https://github.com/feathersjs-ecosystem/feathers-chat-vuex-0.7](https://github.com/feathersjs-ecosystem/feathers-chat-vuex-0.7). The following information will assist you in seeing the "before" and "after" of the refactor to feathers-vuex@3.x. ![Feathers Chat](https://camo.githubusercontent.com/14b6b2d6dd2475c3b83eb1ade6aedbcd8cf94139/68747470733a2f2f646f63732e66656174686572736a732e636f6d2f6173736574732f696d672f66656174686572732d636861742e39313936303738352e706e67) ### Before and After Comparisons - The folder structure is similar, since this is a VueCLI application. Some of the components in the old version have been moved into the `views` folder. - `/components/Home.vue` is now `/views/Home.vue` - `/components/Signup.vue` is now `/views/Signup.vue` - `/components/Login.vue` is now `/views/Login.vue` - `/components/Chat/Chat.vue` is now `/views/Chat.vue` - The `/components` folder has been flattened. There are no more subfolders. - Component refactors: - [Login.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/eb9ba377c5705c1378bee72661a13dd0db48be05) - [Signup.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/478710ed84869d33a9286078496c1e5974a95067) - [Users.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/02b47149c80c27cdeb611c2f4438b4c62159c644) - [Messages.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/930743c1679cc4ed9d691532a7dff1d6a34398e6) - [Compuser.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/cd5c8898ede270d5e22f9c6ef1450d3f3c6278c9) - [Chat.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/39eb3e13f6921b0d0524ae4ac7942b9ce78b222c) - [Messages.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/e5cf7fb0cc8eab80ee3dc441afafb1399d69059e) ### More to Come The Feathers Chat example is a pretty simple application. Its primary purpose is to show off how easy it is to do realtime with FeathersJS. (FeathersJS continues to be the only framework that treats real-time communication as a first-class citizen with the same API across multiple transports.) But it doesn't properly showcase all of the great features in Feathers-Vuex 3.0. This requires a solution that: 1. Still allows comparison of Feathers Chat applications made with other frameworks. 2. Allows the version of Feathers Chat built with Feathers-Vuex to add features and showcase things you might actually use in production. If there are features which you would like to see implemented, please open an issue in the [feathers-chat-vuex Repo](https://github.com/feathersjs-ecosystem/feathers-chat-vuex) for your idea to be considered. ## Community Examples If you have created or know of an example application, please add it, here. - [Feathers-Chat-Vuex](https://github.com/feathersjs-ecosystem/feathers-chat-vuex) ================================================ FILE: docs/feathers-vuex-forms.md ================================================ --- title: Working with Forms sidebarDepth: 4 --- # Working with Forms The `FeathersVuexFormWrapper` and `FeathersVuexInputWrapper` are renderless components which assist in connecting your feathers-vuex data to a form. The next two sections review why they exist by looking at a couple of common patterns. Proceed to the [FeathersVuexFormWrapper](#feathersvuexformwrapper) or [FeathersVuexInputWrapper](#feathersvuexinputwrapper) sections to learn how to implement. ## The Mutation Multiplicity (anti) Pattern When working with Vuex, it's considered an anti-pattern to modify store data directly. Turn on Vuex strict mode, and it will throw an error every time you modify store data outside of a mutation. In my experience, the most common (anti)pattern that beginners use to work around this "limitation" is to 1. Read data from the store and use it for display in the UI. 2. Create custom mutations intended to modify the data in specific ways. 3. Use the mutations wherever they apply (usually implemented as one mutation per form). There are times when defining custom mutations is the most supportive pattern for the task, but I consider them to be more rare. The above pattern can result in a huge number of mutations, extra lines of code, and increased long-term maintenance costs. ## The Clone and Commit Pattern The "Clone and Commit" pattern provides an alternative to using a lot of mutations. This patterns looks more like this: 1. Read data from the store and use it for display in the UI. (Same as above) 2. Create and modify a clone of the data. 3. Use a single mutation to commit the changes back to the original record in the store. Sending most edits through a single mutation can really simplify the way you work with Vuex data. The Feathers-Vuex `BaseModel` class has `clone` and `commit` instance methods. These methods provide a clean API for working with items in the Vuex store and supporting Vuex strict mode: ```js import { models } from 'feathers-vuex' export default { name: 'MyComponent', created() { const { Todo } = models.api const todo = new Todo({ description: 'Plant the garden', isComplete: false }) const todoClone = todo.clone() todoClone.description = 'Plant half of the garden." todoClone.commit() } } ``` In the example above, modifying the `todo` variable would directly modify part of the Vuex store outside of a mutation (also known as a reducer in Redux), which is a generally unsupportive practice. Calling `todo.clone()` returns a reactive clone of the instance and keeps it outside the Vuex store. This means you can make changes to it all you want without causing any trouble with Vuex. You can then call `todoClone.commit()` to update the original record in the store. The `clone` and `commit` methods are used inside the FeathersVuexFormWrapper component. ## FeathersVuexFormWrapper The `FeathersVuexFormWrapper` component uses the "clone and commit" pattern to connect a single record to a child form within its default slot. ```vue ``` Here's another example of how you could use the form wrapper to both save the form and close a modal at the same time. (The modal is not shown in the template markup.) Notice how the `@save` handler is an inline function that sets `isModalVisible` to false, then on a new line it calls save. This is handled perfectly by Vue. ```vue ``` ### Props - `item`: {Object} a model instance from the Vuex store. - `watch`: {Boolean|Array} when enabled, if the original record is updated, the data will be re-cloned. The newly-cloned data will overwrite the `clone` data (in the slot scope). Default: `false`. - `eager`: {Boolean} While this is enabled, using the `save` method will first commit the result to the store then it will send a network request. The UI display will update immediately, without waiting for any response from the API server. Default: `true`. ### Slot Scope The default slot contains only four attributes. The `clone` data can be passed to the child component. The `save`, `reset`, and `remove` are meant to be bound to events emitted from the child component. - `clone`: {Object} The cloned record. Each record in the store can have a single clone. The clones are stored on the service's model class, by default. - `save`: {Function} When called, it commits the data and saves the record (with eager updating, by default. See the `eager` prop.) The save method calls `instance.save()`, internally, so you can pass a params object, if needed. - `reset`: {Function} When called, the clone data will be reset back to the data that is currently found in the store for the same record. - `remove`: {Function} When called, it removes the record from the API server and the Vuex store. ### Usage with `diffOnPatch` If you plan to use the `diffOnPatch` static Model method together with the `FeathersVuexFormWrapper`, be sure to set the `eager` prop to `false`. See [this GitHub issue](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/520) for more details. ## FormWrapper Example: CRUD Form ### TodoView It's a pretty common scenario to have the same form handle editing and creating data. Below is a basic example of how you could use the FeathersVuexFormWrapper for this. A few things to notice about the example: 1. It uses a `Todo` Model class to create and edit todos. The `$FeathersVuex` object is available on `this` only when the [Feathers-Vuex Vue plugin](./vue-plugin.md) is used. 2. It assumes that you have a route setup with an `:id` parameter. 3. It assumes that the data has a MongoDB-style `_id` property, where an SQL-based service would probably use `id`. ```vue ``` ### TodoEditor Next let's look at a minimal example of a 'TodoEditor' component which is a child of the `FeathersVuexFormWrapper` in the above example. A few things to notice about the below `TodoEditor` component: 1. It's minimal on purpose to show you the important parts of working with the `FeathersVuexFormWrapper`. 1. It emits the `save`, `reset`, and `remove` events, which are connected to the `FeathersVuexFormWrapper` in the above code snippet. 1. It's not styled to keep it simple. You'll probably want to add some styles. ;) 1. The Delete button immediately emits remove, so the instance will be deleted immediately. You probably want, instead, to show a prompt or confirmation dialog to ask the user to confirm deletion. 1. This is HTML, so the button `type` is important. If you forget to add `type="button"` to a button, it will default to `type="submit"`. Clicking the button would submit the form and call the `@submit.prevent` handler on the `
` element. This even applies to buttons inside child components of the form. You definitely want to remember to put `type` attributes on all of your buttons. ```vue ``` ### Vuelidate 2 Example Here's an example of how you might use the upcoming Vuelidate 2 (which is being rewritten to work with the Vue Composition API) to do form validation. Just to be clear, the validation library you use doesn't change how FeathersVuex will work. Since Vuelidate is likely the most popular validation library, this is an example to get you started. There may be some things to figure out to implement your use case. First, you'll need to install these dependencies: ```json { "dependencies": { "@vuelidate/core": "^2.0.0-alpha.0", "@vuelidate/validators": "^2.0.0-alpha.0" } } ``` Here's the full example, complete with TailwindCSS styles. ```html ``` ## FeathersVuexInputWrapper Building on the same ideas as the FeathersVuexFormWrapper, the FeathersVuexInputWrapper reduces boilerplate for working with the clone and commit pattern on a single input. One use case for this component is implementing an "edit-in-place" workflow. The following example shows how to use the FeathersVuexInputWrapper to automatically save a record upon `blur` on a text input: ```html ``` Notice that in the `save` handler in the above example, the `.patch` method is called on the user, passing in the data. Because the data contains only the user property which changed, the patch request will only send the data which has changed, saving precious bandwidth. ### Props The `FeathersVuexInputWrapper` has two props, both of which are required: - `item`: The original (non-cloned) model instance. - `prop`: The property name on the model instance to be edited. ### Default Slot Scope Only the default slot is used. The following props are available in the slot scope: - `current {clone|instance}`: returns the clone if it exists, or the original record. `current = clone || item` - `clone { clone }`: the internal clone. This is exposed for debugging purposes. - `prop {String}`: the value of the `prop` prop. If you have the prop stored in a variable in the outer scope, this is redundant and not needed. You could just use this from the outer scope. It mostly comes in handy when you are manually specifying the `prop` name on the component. - `createClone {Function}`: sets up the internal clone. Meant to be used as an event handler. - `handler {Function}`: has the signature `handler(event, callback)`. It prepared data before calling the callback function that must be provided from the outer scope. ### The Callback Function The `handler` function in the slot scope requires the use of a callback function as its second argument. Here's an example callback function followed by an explanation of its properties: ```js myCallback({ event, clone, prop, data }) { clone.commit() } ``` - `event {Event}`: the event which triggered the `handler` function in the slot scope. - `clone {clone}`: the cloned version of the `item` instance that was provided as a prop. - `prop {String}`: the name of the `prop` that is being edited (will always match the `prop` prop.) - `data {Object}`: An object containing the changes that were made to the object. Useful for calling `.patch({ data })` on the original instance. This callback needs to be customized to fit your business logic. You might patch the changes right away, as shown in this example callback function. ```js async save({ event, clone, prop, data }) { const user = clone.commit() return user.patch({ data }) } ``` Notice in the example above that the `save` function is `async`. This means that it returns a promise, which in this case is the `user.patch` request. Internally, the `handler` method will automatically set the internal `clone` object to `null`, which will cause the `current` computed property to return the original instance. Note that some types of HTML input elements will call `handler` repeatedly, so the handler needs to be debounced. See an example, below. ## InputWrapper Examples ### Text Input With a text input, you can use the `focus` and `blur` events ```html ``` ### Color Input Here is an example of using the FeathersVuexInputWrapper on a color input. Color inputs emit a lot of `input` and `change` events, so you'll probably want to debounce the callback function if you are going to immediately save changes. The example after this one shows how you might debounce. ```html ``` ### Color Input with Debounce Here is an example of using the FeathersVuexInputWrapper on a color input. Notice how the debounced callback function is provided to the `handler`. This is because color inputs trigger a `change` event every time their value changes. To prevent sending thousands of patch requests as the user changes colors, we use the debounced function to only send a request after 100ms of inactivity. Notice also that this example uses the Vue Composition API because creating a debounced function is much cleaner this way. ```vue ``` ================================================ FILE: docs/feathervuex-in-vuejs3-setup.md ================================================ # Using Vuejs 3 setup() Vuejs 3 introduced a new way of passing data from a parent to its child. This is valuable if the child is deep down the hierarchy chain. We want to include `FeathersVuex` in many child components. Through [Inject/Provide](https://v3.vuejs.org/guide/component-provide-inject.html#working-with-reactivity) we now have the ability to `inject` the `FeathersVuex`.`api` into our setup method. ## Setup() method The context is no longer passed into the setup() as a parameter: ``` setup(props, context) { // old way const { User } = root.$FeathersVuex.api } ``` We now must `inject` it into setup(): ``` export default defineComponent({ import { inject } from 'vue'; setup() { // both $FeatherVuex and $fv work here const models: any = inject('$FeathersVuex') const newUser = new models.api.User() return { newUser } } }) ``` If an custom alias is desired, pass the `alias` into the module install as detailed [here](https://github.com/feathersjs-ecosystem/feathers-vuex/blob/vue-demi/packages/feathers-vuex-vue3/src/app-plugin.ts). **Note:** You may auto import `inject` and other `vue` utilities using [unplugin-auto-import](https://github.com/antfu/unplugin-auto-import). Make sure to adjust the `auto-import.d.ts` file to match your `include[]` directory (`src` for vue-cli generated apps) ================================================ FILE: docs/getting-started.md ================================================ --- title: Getting Started sidebarDepth: 3 --- # Getting Started with Feathers-Vuex ## Installation ```bash npm install feathers-vuex @vue/composition-api --save ``` ```bash yarn add feathers-vuex @vue/composition-api ``` IMPORTANT: Feathers-Vuex is (and requires to be) published in ES6 format for full compatibility with JS classes. If your project uses Babel, it must be configured properly. See the [Project Configuration](#projectconfiguration) section for more information. ### With feathers-socketio A realtime-transport like Socket.io or Primus is required in order to take advantage of the real-time socket events built into Feathers-Vuex. The `feathers-hooks-common` package, specified below, is not required to work with Feathers-Vuex. ```bash npm i @feathersjs/feathers @feathersjs/socketio-client @feathersjs/authentication-client socket.io-client @vue/composition-api feathers-vuex feathers-hooks-common --save ``` ```bash yarn add @feathersjs/feathers @feathersjs/socketio-client @feathersjs/authentication-client socket.io-client @vue/composition-api feathers-vuex feathers-hooks-common ``` ### With feathers-rest Feathers-Vuex works with Feathers-Rest, but keep in mind that the `feathers-rest` client does not listen to socket events. The `feathers-hooks-common` package, specified below, is not required to work with Feathers-Vuex. ```bash npm i @feathersjs/feathers @feathersjs/rest-client @feathersjs/authentication-client @vue/composition-api feathers-hooks-common feathers-vuex --save ``` ```bash yarn add @feathersjs/feathers @feathersjs/rest-client @feathersjs/authentication-client @vue/composition-api feathers-hooks-common feathers-vuex ``` ## Project Configuration ### Vue-CLI If your project runs on Vue-CLI, add the following to your `vue.config.js` file: ```js module.exports = { transpileDependencies: ['feathers-vuex'] } ``` ### Quasar > In newer Quasar apps, the following `transpileDependencies` setup may not be necessary, anymore. See [this issue on GitHub](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/450) For Quasar apps, `transpileDependencies` can be updated in `quasar.conf.js` under build as ``` build: { transpileDependencies: ['feathers-vuex'] } ``` ### Nuxt If your project uses Nuxt, add the following to your `nuxt.config.js` file: ``` build: { transpile: ['feathers-vuex'], } ``` ### Resolving Build Issues If you have issues with sub-dependencies not loading correctly, you may want to check out [this GitHub issue](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/399). One of the suggestions is likely to fix the issue. Be sure to read the section of the docs dedicated to [Working With Nuxt](./nuxt.md). ## Vue DevTools Since Feathers-Vuex extensively uses Vuex under the hood, you'll want to make sure your VueJS developer tools are up to date AND setup properly. Specifically, the "New Vuex Backend" needs to be enabled. To setup the devtools 1. Open the Vue tab of the developer tools while viewing your Vue project in the browser. 1. Go to the Settings panel. 1. Enable the new Vuex backend: ![New Vuex Backend in Vue DevTools](/img/devtools.jpg) When the above setting is not enabled, the Vue Devtools will likely hang when you start working on a large project. ## Setup Using Feathers-Vuex happens in these steps: 1. [Setup the Feathers client and Feathers-Vuex](#setup-the-feathers-client-and-feathers-vuex) 2. [Define a Model class and service plugin for each service](#setup-one-or-more-service-plugins) 3. [Setup the auth plugin](#setup-the-auth-plugin), if required. 4. Register the plugins with the Vuex store. ### Feathers Client & Feathers-Vuex To setup `feathers-vuex`, we first need to setup the latest Feathers client. We can also setup feathers-vuex in the same file. Depending on your requirements, you'll need to install the feathers-client dependencies, as shown, above. Note that this example includes an app-level hook that removes attributes for handling temporary (local-only) records. ```js // src/feathers-client.js import feathers from '@feathersjs/feathers' import socketio from '@feathersjs/socketio-client' import auth from '@feathersjs/authentication-client' import io from 'socket.io-client' import { iff, discard } from 'feathers-hooks-common' import feathersVuex from 'feathers-vuex' const socket = io('http://localhost:3030', {transports: ['websocket']}) const feathersClient = feathers() .configure(socketio(socket)) .configure(auth({ storage: window.localStorage })) .hooks({ before: { all: [ iff( context => ['create', 'update', 'patch'].includes(context.method), discard('__id', '__isTemp') ) ] } }) export default feathersClient // Setting up feathers-vuex const { makeServicePlugin, makeAuthPlugin, BaseModel, models, FeathersVuex } = feathersVuex( feathersClient, { serverAlias: 'api', // optional for working with multiple APIs (this is the default value) idField: '_id', // Must match the id field in your database table/collection whitelist: ['$regex', '$options'] } ) export { makeAuthPlugin, makeServicePlugin, BaseModel, models, FeathersVuex } ``` ### Service Plugins The following example creates a User class and registers it with the new `makeServicePlugin` utility function. This same file is also a great place to add your service-level hooks, so they're shown, too. ```js // src/store/services/users.js import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client' class User extends BaseModel { constructor(data, options) { super(data, options) } // Required for $FeathersVuex plugin to work after production transpile. static modelName = 'User' // Define default properties here static instanceDefaults() { return { email: '', password: '' } } } const servicePath = 'users' const servicePlugin = makeServicePlugin({ Model: User, service: feathersClient.service(servicePath), servicePath }) // Setup the client-side Feathers hooks. feathersClient.service(servicePath).hooks({ before: { all: [], find: [], get: [], create: [], update: [], patch: [], remove: [] }, after: { all: [], find: [], get: [], create: [], update: [], patch: [], remove: [] }, error: { all: [], find: [], get: [], create: [], update: [], patch: [], remove: [] } }) export default servicePlugin ``` ### Auth Plugin If your application uses authentication, the Auth Plugin will probably come in handy. It's a couple of lines to setup: ```js // src/store/store.auth.js import { makeAuthPlugin } from '../feathers-client' export default makeAuthPlugin({ userService: 'users' }) ``` [Read more about the Auth Plugin](/auth-plugin.html). ### Vuex store This example uses Webpack's `require.context` feature. If you're not using Webpack, you'll need to manually import each module and list them in the `plugins` array. ```js // src/store/index.js import Vue from 'vue' import Vuex from 'vuex' import { FeathersVuex } from '../feathers-client' import auth from './store.auth' Vue.use(Vuex) Vue.use(FeathersVuex) const requireModule = require.context( // The path where the service modules live './services', // Whether to look in subfolders false, // Only include .js files (prevents duplicate imports`) /\.js$/ ) const servicePlugins = requireModule .keys() .map(modulePath => requireModule(modulePath).default) export default new Vuex.Store({ state: {}, mutations: {}, actions: {}, plugins: [...servicePlugins, auth] }) ``` ## Begin Using Feathers-Vuex There are a couple of ways to use Feathers-Vuex. Version 2.0 heavily focuses on abstracting away the Vuex syntax in favor of using [Model classes](/model-classes.html). The Model classes are a layer on top of the Vuex getters, mutations, and actions. You can, of course, also directly use the [service plugin's getters, mutations, and actions](/service-plugin.html). There are two plugins included: 1. The [Service Plugin](./service-plugin.md) adds a Vuex store for new services. 2. The [Auth Plugin](./auth-plugin.md) sets up the Vuex store for authentication / logout. To see `feathers-vuex` in a working vue-cli application, check out [`feathers-chat-vuex`](https://github.com/feathersjs-ecosystem/feathers-chat-vuex). ### Global Configuration The following default options are available for configuration: ```js const defaultOptions = { // only configured globally serverAlias: 'api', keepCopiesInStore: false, // also configurable per service idField: 'id', tempIdField: '__id', nameStyle: 'short', debug: false, addOnUpsert: false, autoRemove: false, enableEvents: true, preferUpdate: false, replaceItems: false, skipRequestIfExists: false, paramsForServer: ['$populateParams'], whitelist: [], handleEvents: { created: (item, { model, models }) => options.enableEvents, patched: (item, { model, models }) => options.enableEvents, updated: (item, { model, models }) => options.enableEvents, removed: (item, { model, models }) => options.enableEvents }, } ``` - `serverAlias` - **Default:** `api` - Models are keyed by `serverAlias`. Access the `$FeathersVuex` Plugin and its models in your components by `this.$FeathersVuex.api.${Model}` - `keepCopiesInStore` - **Default:** `false` - Set to true to store cloned copies in the store instead of on the Model. - `idField {String}` - **Default:** `'id'` - The field in each record that will contain the id - `tempIdField {Boolean}` - **Default:** `'__id'` - The field in each temporary record that contains the id - `nameStyle {'short'|'path'}` - **Default:** `'short'` - Use the full service path as the Vuex module name, instead of just the last section. - `debug {Boolean}` - **Default:** `false` - Enable some logging for debugging - `addOnUpsert {Boolean}` - **Default:** `false` - If `true` add new records pushed by 'updated/patched' socketio events into store, instead of discarding them. - `autoRemove {Boolean}` - **Default:** `false` - If `true` automatically remove records missing from responses (only use with feathers-rest) - `preferUpdate {Boolean}` - **Default:** `false` - If `true`, calling `model.save()` will do an `update` instead of a `patch`. - `replaceItems {Boolean}` - **Default:** `false` - If `true`, updates & patches replace the record in the store. Default is false, which merges in changes. - `skipRequestIfExists {Boolean}` - **Default:** `false` - For get action, if `true` the record already exists in store, skip the remote request. - `paramsForServer {Array}` - **Default:** `['$populateParams']` - Custom query operators that are ignored in the find getter, but will pass through to the server. It is preconfigured to work with the `$populateParams` custom operator from [feathers-graph-populate](https://feathers-graph-populate.netlify.app/). - `whitelist {Array}` - **Default:** `[]` - Custom query operators that will be allowed in the find getter. - `enableEvents {Boolean}` - **Default:** `true` - If `false` socket event listeners will be turned off. See the services - `handleEvents {Object}`: For this to work `enableEvents` must be `true` - `created {Function}` - **Default:** `(item, { model, models }) => options.enableEvents` - handle `created` events, return true to add to the store - `patched {Function}` - **Default:** `(item, { model, models }) => options.enableEvents` - handle `created` events, return true to update in the store - `updated {Function}` - **Default:** `(item, { model, models }) => options.enableEvents` - handle `created` events, return true to update in the store - `removed {Function}` - **Default:** `(item, { model, models }) => options.enableEvents` - handle `removed` events, return true to remove from the store Also see the [Configs per Service](/service-plugin.html#configuration) ### Note about feathers-reactive Previous versions of this plugin required both RxJS and `feathers-reactive` to receive realtime updates. `feathers-vuex@1.0.0` has socket messaging support built in and takes advantage of Vuex reactivity, so RxJS and `feathers-reactive` are no longer required or supported. Each service module can also be individually configured. ================================================ FILE: docs/index.md ================================================ --- home: true heroImage: https://github.com/feathersjs-ecosystem/feathers-vuex/raw/master/service-logo.png heroText: Feathers-Vuex 3.x tagLine: Integration of FeathersJS, Vue, and Nuxt for the artisan developer actionText: Get Started actionLink: ./api-overview.md features: - title: Realtime by Default details: It's fully powered by Vuex and FeathersJS, lightweight, & realtime out of the box. - title: Simplified Auth & Services details: Includes service and auth plugins powered by Vuex. All plugins can be easily customized to fit your app. Fully flexible. - title: Best Practices, Baked In details: Vue Composition API 😎 Common Redux patterns included. Fall-through cache by default. Query the Vuex store like a database. footer: MIT Licensed | Copyright © 2017-present Marshall Thompson --- ================================================ FILE: docs/mixins.md ================================================ --- title: Mixins sidebarDepth: 3 --- # Mixins `Feathers-Vuex` mixins provide quick and easy best practices directly inside a component's viewModel. They are similar to [Renderless Data Components](./components.md), but are more powerful for two reasons. 1. You can do lots of them together. Handle multiple queries against multiple services at the same time. The Renderless Data Components aren't capable of handling more than one query without doing ugly nesting. 2. They bring the data directly into the component's actual viewModel. The Renderless Data Components only pull the data into the template scope, so the only clean way to get access to the data was by passing it to a component as props. This is a great solution until you run into number 1, above. If you're not using the [Feathers-Vuex Composition API](./composition-api.md), the mixins are probably going to be your preferred solution for development. ## Usage Here are the steps to using mixins: 1. Import the `makeFindMixin` utility from FeathersVuex. 2. Register it in a component's mixins once for each query to be made in the component. 3. Provide a set of params in a computed property (getter only) 4. Iterate over the computed "items" prop named after the service. ```vue ``` In the above example, any records returned from the server will automatically show up when they become available. It also automatically responds to realtime events when you're using one of FeathersJS's realtime transports, like Socket.io. Notice in the above example that using the mixin automatically makes the `serverTasks` available in the template. The mixins automatically setup a few properties in the viewModel based on the camelCased name of the service. You can also provide a `name` attribute to override the defaults: ## makeFindMixin ### Options - `service {String|Function}` - **required** the service path. This must match a service that has already been registered with FeathersVuex. - **{String}** - The service namespace - **{Function}** - Any provided function will become a computed property in the component and will be used to determine its value. - `name {String}` - The name to use in all of the dynamically-generated property names. See the section about Dynamically Generated Props - `items {String}` - The attribute name to use for the records. - `params {String|Function}` - One of two possible params attributes. (The other is `fetchParams`) When only `params` is provided, it will be used for both the `find` getter and the `find` action. When using server-side pagination, use `fetchParams` for server communciation and the `params` prop for pulling data from the local store. If the params is `null` or `undefined`, the query against both the API will be skipped. The find getter will return an empty array. **Default {String}: `${camelCasedService}Params`** (So, by default, it will attempt to use the property on the component called serviceName + "Params") - **{String}** - The name of the attribute in the current component which holds or returns the query object. - **{Function}** - A provided function will become a computed property in the current component. - `watch {String|Array}` - specifies the attributes of the `params` or `fetchParams` to watch. When a watched prop changes, a new request will be made to the API server. Pass 'params' to watch the entire params object. Pass 'params.query.name' to watch the 'name' property of the query. Watch is turned off by default, meaning only one initial request is made. **Default {String}: `${camelCasedService}Params`** - **{Boolean}** - If `true`: `[${camelCasedService}Params]` will be watched, else `[]` - **{String}** - The name of the component's prop to use as the value. Transformed to an `Array` - **{Array}** - Names of the component's prop - `fetchParams {String|Function}` - when provided, the `fetchParams` serves as the params for the API server request. When `fetchParams` is used, the `param` attribute will be used against the service's local Vuex store. **Default: undefined** - **{String}** - The name of the attribute in the current component which holds or returns the params object. - **{Function}** - A provided function will become a computed property in the current component. - `queryWhen {Boolean|String|Function}` - the query to the server will only be made when this evaluates to true. **Default: true** - **{Boolean}** - As a boolean, the value provided determines whether this is on or off. - **{String}** - The name of the component's prop to use as the value. - **{Function}** - Any provided function will become a method in the component and will receive the current params object as an argument. - `local {Boolean|String|Function}` - when true, will only use the `params` prop to pull data from the local Vuex store. It will disable queries to the API server. The value of `local` will override `queryWhen`. **Default:false** - **{Boolean}** - As a boolean, the value provided determines whether this is on or off. - **{String}** - The name of the component's prop to use as the value. - **{Function}** - Any provided function will become a computed property in the component and will be used to determine its value. - `qid {String}` - The "query identifier" ("qid", for short) is used for storing pagination data in the Vuex store. See the service module docs to see what you'll find inside. The `qid` and its accompanying pagination data from the store will eventually be used for cacheing and preventing duplicate queries to the API. ### Injected Properties With `makeFindMixin` the following properties will be injected into your component and can become handy to use manually. Since the names of the mixin are basically dynamically generated by the `service` and `name` props you pass. Here the general names to understand what's going on under the hood: #### Dynamically Generated Props: ```vue ``` ## makeGetMixin ### Options - `id {String|Function}` - when performing a `get` request, serves as the id for the request. This is automatically watched, so if the `id` changes, an API request will be made and the data will be updated. If `undefined` or `null`, no request will be made. **Default: undefined** - **{String}** - The name of the component's prop to use as the value. - **{Function}** - Any provided function will become a computed property in the component and will be used to determine its value. - `service {String|Function}` - **required** the service path. This must match a service that has already been registered with FeathersVuex. - **{String}** - The service namespace - **{Function}** - Any provided function will become a computed property in the component and will be used to determine its value. - `name {String}` - The name to use in all of the dynamically-generated property names. See the section about Dynamically Generated Props - `item {String}` - The attribute name to use for the record. - `params {String|Function}` - One of two possible params attributes. (The other is `fetchParams`) When only `params` is provided, it will be used for both the `find` getter and the `find` action. When using server-side pagination, use `fetchParams` for server communciation and the `params` prop for pulling data from the local store. If the params is `null` or `undefined`, the query against both the API will be skipped. The find getter will return an empty array. **Default {String}: `${camelCasedSingularizedService}Params`** (So, by default, it will attempt to use the property on the component called serviceName + "Params") - **{String}** - The name of the attribute in the current component which holds or returns the query object. - **{Function}** - A provided function will become a computed property in the current component. - `watch {Boolean|String|Array}` - specifies the attributes of the `params` or `fetchParams` to watch. When a watched prop changes, a new request will be made to the API server. Pass 'params' to watch the entire params object. Pass 'params.query.name' to watch the 'name' property of the query. Watch is turned off by default, meaning only one initial request is made. **Default {Array}: `[]`** - **{Boolean}** - If `true`: `[${camelCasedService}Params]` will be watched, else `[]` - **{String}** - The name of the component's prop to use as the value. Transformed to an `Array` - **{Array}** - Names of the component's prop - `fetchParams {String|Function}` - when provided, the `fetchParams` serves as the params for the API server request. When `fetchParams` is used, the `param` attribute will be used against the service's local Vuex store. **Default: undefined** - **{String}** - The name of the attribute in the current component which holds or returns the params object. - **{Function}** - A provided function will become a computed property in the current component. - `queryWhen {Boolean|String|Function}` - the query to the server will only be made when this evaluates to true. **Default: true** - **{Boolean}** - As a boolean, the value provided determines whether this is on or off. - **{String}** - The name of the component's prop to use as the value. - **{Function}** - Any provided function will become a method in the component and will receive the current params object as an argument. - `local {Boolean|String|Function}` - when true, will only use the `params` prop to pull data from the local Vuex store. It will disable queries to the API server. The value of `local` will override `queryWhen`. **Default:false** - **{Boolean}** - As a boolean, the value provided determines whether this is on or off. - **{String}** - The name of the component's prop to use as the value. - **{Function}** - Any provided function will become a computed property in the component and will be used to determine its value. ### Injected Properties With `makeGetMixin` the following properties will be injected into your component and can become handy to use manually. Since the names of the mixin are basically dynamically generated by the `service` and `name` props you pass. Here the general names to understand what's going on under the hood: #### Dynamically Generated Props: ```vue ``` ## Patterns & Examples ### Dynamically Generated Props Based on what options you provide to each mixin, some dynamically-generated props will be added to the current component. Note that the example below only shows the return values from the computes, not the functions. ```js makeFindMixin({ service: 'videos' }) = { data: () => ({ isFindVideosPending: false, haveVideosBeenRequestedOnce: false, haveVideosLoadedOnce: false, videosLocal: false, videosQid: 'default', videosQueryWhen: true, videosWatch: [] }), // Only showing the return values, not the actual functions computed: { // pulled from the store using the find getter videos: [ /* results */ ], // The pagination data with matching qid from the store videosPaginationData: { queriedAt: 1539682100148, // the timestamp of the last query query: {}, // The last query used with this qid ids: [], // The ids of the records returned in the response limit: 20, // based on the response from the server skip: 0, // The value of the $skip param in the query total: 1 // The total as reported by the server. }, // The mixin will expect to find this. This won't be created automatically. videosQuery () {} } } ``` If you were to handle two queries from the same service, you would use the `name` attribute to rename one of them. The results would be named accordingly. Note that the example below only shows the return values from the computes, not the functions. ```js makeFindMixin({ service: 'videos', name: 'myVideos' }) = { data: () => ({ isFindMyVideosPending: false, haveMyVideosBeenRequestedOnce: false, haveMyVideosLoadedOnce: false, myVideosLocal: false, myVideosQid: 'default', myVideosQueryWhen: true, myVideosWatch: [] }), // Only showing the return values, not the actual functions computed: { // pulled from the store using the find getter myVideos: [ /* results */ ], // The pagination data with matching qid from the store myVideosPaginationData: { queriedAt: 1539682100148, // the timestamp of the last query query: {}, // The last query used with this qid ids: [], // The ids of the records returned in the response limit: 20, // based on the response from the server skip: 0, // The value of the $skip param in the query total: 1 // The total as reported by the server. }, // The mixin will expect to find this. This won't be created automatically. myVideosQuery () {} } } ``` ### Using a dynamic service It's possible to change the service name on the fly. To do this, pass a function (which becomes a computed property) that returns another string property from the viewModel. Below is an example of how to set that up. The `serviceName` attribute is set to `"videos"`, initially. The `setTimeout` in the `created` method changes the value to `"users"` after three seconds. When the serviceName changes, the users service is queried automatically. The `items` property will then update to be the newly fetched users instead of the video records that it contained before. The `items` option is used to rename the items to something more generic. ```html ``` In the above example, the mixin data would look like this: ```js const mixedInDataFromAboveExample = { data: () => ({ isFindServicePending: false, serviceLocal: false, serviceQid: 'default', serviceQueryWhen: true, serviceWatch: [] }), // Only showing the return values, not the actual functions computed: { items: [ /* results */ ], // The pagination data with matching qid from the store servicePaginationData: {}, // The mixin will expect to find this. This won't be created automatically. serviceQuery () {} } } ``` ### Pagination with fall-through cacheing The `makeFindMixin` in `feathers-vuex@2.x` features a great new, high performance, fall-through cacheing feature, which only uses a single query! Read the service module documentation for details of how it works under the hood. It really makes easy work of high-performance pagination. To use the pagination, provide `$limit` and `$skip` attributes in `params.query`. This is exactly the same way you would normally do with any FeathersJS query. So this is completely transparent to how you'd normally do it. > Note: By default, the pagination feature is turned on. To simplify updating existing apps using `feathers-vuex`, you can turn this feature off in any part of your app by passing `paginate: false` in the params for that particular query. This will completely restore the previous behavior and re-enable live lists. Let's extend the first example on this page to support pagination. We'll do the following: 1. Setup the `makeFindMixin` to use the `watch` property. 2. Add a `data` attribute to the component with `limit` and `skip` properties. 3. Reference the `limit` and `skip` in `params.query`. 4. Add methods for `previousPage` and `nextPage` 5. Create buttons for changing the limit and skip. ```vue ``` In the above example, since we've enabled the `watch` attribute on the makeFindMixin, every time the params change, the query will run again. `feathers-vuex` will keep track of the queries and the pages that are visited, noting which records are returned on each page. When a page is revisited, the data in the store will *immedately* display to the user. The query will (by default) go out to the API server, and data will be updated in the background when the response arrives. ### Debouncing requests What happens when a query with a watcher is attached to an attribute that might change rapidly? A lot of API requests can get sent in succession. If too many are sent, some of them will start to fail (a.k.a. bounce). The `makeFindMixin` has a built-in utility for debouncing requests. Enabling it makes it so requests only are sent after a specific amount of time has passed. To enable it, pass a `debounce` attribute in the `params`, as shown in the next example. Let's build on our previous example by adding a `search` feature where the user can type some input. Here are the steps: 1. Add an attribute to the data to which we will bind user input. We'll call it `search`. 2. Modify params to include the `search` attribute in a supportive way. 3. Enable the the debounce feature. 4. Add an `input:text` to the template which binds to the attribute in step 1. ```vue ``` Notice a couple of things in the above example. We enabled the internal `debounce` feature by simply adding `debounce: 500` to the params (outside the query). This means that as the user types, requests will be queued inside a 500 ms interval. The request will be sent as soon as the user stops typing for 500 milliseconds. For example, if the user types a single character, waits ~400ms, then types a second character, the first request will be cancelled and another request will be sent 500ms after typing the second character. It's more likely that these requests will not bounce. :) We also added a `$regex` search to the params. This is a MongoDB feature, which naturally also works with Mongoose services (since Mongoose is a tool built for MongoDB). If you're using another type of service, you will need to come up with a solution for performing searches safely. The solution will vary depending on the database used. Feel free to make a PR for using something else that could be useful to the community! We love those! ### Enabling live lists with pagination The new fall-through cacheing pagination does not currently support live sorting of lists. This means that when a new record arrives from the database, it doesn't automatically get sorted into the correct page and shuffle the other records around it. The lists will update as the user navigates to previous/next pages. Coming up with a solution for this will be a top priority after 2.x ships. In the meantime, here are some alternatives. ### Use `paginate:false` in the params Restore the previous default behavior by putting `paginate:false` in the params. This is the easiest way to upgrade existing apps using the `makeFindMixin`. Look at the `todosParams` in this example: ```vue ``` The `paginate` property will not be sent to the server, but it will locally disable the pagination and enable the live lists from the `find` getter. ### Refresh the current query after changes This is a simplistic approach. In some cases, when you expect the data to have changed in whatever list you are currently showing to the user, you can simply call the find action for that list and get new data from the server. In the below example, whenever the `TodoEntryForm` component emits the `created` event, it triggers the `findTodos` method. Note: you wouldn't want to do `@created="findTodos` because that would potentially pass the new todo as the params like `findTodos(newTodo)` instead of `findTodos()`. Passing no params will automatically use the `todosParams` for the query. ```vue ``` ### Use the "FetchQuery" params Let's suppose we have a todos service that we're mixing into our component: ```js makeFindMixin({ service: 'todos', watch: true }) ``` The `makeFindMixin` by default will look for a single set of params called `todosParams`. If it finds only this set of params, the params will be used for fetching data and pulling it from the Vuex store. However, if another set of params, called the `todosFetchParams`, this new set of params will be used to fetch data and the `todosParams` will be used against the internal store. In this scenario, the internal pagination tracking is also turned off, which allows you to make queries directly against the Vuex store again. ```vue ``` ### Debugging the makeFindMixin **Important: For the built in pagination features to work, you must not directly manipulate the `context.params` object in any hooks.** If the makeFindMixin is not returning any results, but you can see the results coming in across the websocket or rest transport, make sure you're not directly modifying the `context.params` object in a hook, as mentioned in bold, above. ;) The best place to debug if this is your issue is in `make-find-mixin` in the `[ITEMS]` computed property. Set a breakpoint at `const items = getItemsFromQueryInfo(pagination, queryInfo, keyedById)`. Maybe even make it a conditional breakpoint around the `serviceName` variable: `serviceName === 'assets' && Object.keys(keyedById).length > 0`. When you hit the above breakpoint, check the `keyedById` variable. If it has records, but the `items` is an empty array, there may be a problem with the `queryInfo` not matching from the `context.params` getting modified. ================================================ FILE: docs/model-classes.md ================================================ --- title: Data Modeling sidebarDepth: 3 --- # Data Modeling with Model Classes Feathers-Vuex 1.0 introduced some lightweight data modeling. Every service had its own, internal `FeathersVuexModel`. In version 2.0 this `FeathersVuexModel` is now called the `BaseModel` and is extendable, so you can add your own functionality. ## Extending the BaseModel Class While [setting up Feathers-Vuex](/getting-started.html#feathers-client-feathers-vuex), we exported the `BaseModel` class so that we could extend it. The below example shows how to import and extend the `BaseModel`. Each service must now have its own unique Model class. ```js import feathersClient, { makeServicePlugin, BaseModel } from '../feathers-client' class User extends BaseModel { // Required for $FeathersVuex plugin to work after production transpile. static modelName = 'User' // Define default properties here static instanceDefaults() { return { email: '', password: '' } } } const servicePath = 'users' const servicePlugin = makeServicePlugin({ Model: User, service: feathersClient.service(servicePath), servicePath }) ``` In case you're wondering, the `modelName` property is used to get around transpilation errors when using Babel with ES3 or ES5. Babel is still installed by default in most projects and generators. The `modelName` is used instead of the `name` property to provide a reliable name AFTER transpilation. If you're working in an environment that doesn't support static properties on classes, you can always specify the static properties using the dot operator: ```js class User extends BaseModel {} User.modelName = 'User' User.instanceDefaults = function() { return { email: '', password: '' } } ``` ### BaseModel typing BaseModel typing gives helpful IDE autocomplete and errors when using Model classes. Since Feathers-Vuex doesn't know what your data looks like, you will need to help define your underlying model data's interface. By default, Model classes are `string` indexable with value of type `any`. This isn't super helpful... ```ts // Just like before, we define our class as an extension of BaseModel class User extends BaseModel { /* ... */ } // Augment the User Model interface interface User { email: string password: string } ``` Now, whenever we access a `User` model, all fields defined in the interface will be available in IDE auto-complete/intellisense along with the model methods/props. If you already have a User interface defined under a different name, just define a new interface with the same name as your Model class like so ```ts // if our User interface already exists as UserRecord interface User extends UserRecord {} ``` To further enhance typing, you can augment FeathersVuex types to aid development in other parts of your app. It's important to note the differences in the following example if you do or do not setup a `serverAlias`. ```ts // src/store/user.store.ts import { ServiceState } from 'feathers-vuex' class User extends BaseModel { /* ... */ } interface User { /* ... */ } const servicePath = 'users' declare module "feathers-vuex" { interface FeathersVuexStoreState { [servicePath]: ServiceState } // Only if you setup FeathersVuex without a serverAlias!! interface FeathersVuexGlobalModels { User: typeof User } } // Only if you setup FeathersVuex with a serverAlias!! declare module "src/store" { interface MyApiModels { User: typeof User } } ``` If you have setup a `serverAlias`, you need to add the following to `src/store/index.ts`. ```ts // src/store/index.ts export interface MyApiModels { /* Let each service augment this interface */ } declare module "feathers-vuex" { interface FeathersVuexGlobalModels { 'my-api-name': MyApiModels } } ``` Replace `my-api-name` with the `serverAlias` you used when setting up FeathersVuex. ## Model attributes The following attributes are available on each model: - `servicePath {String}` - `Model.servicePath` is the path passed to create the FeathersClient service. - `namespace {String}` - `Model.namespace` holds the value that was used to register the module with Vuex. This will match the `servicePath` unless you've provided a custom namespace in the [Service Module options](./index.md#Use). - `store {Vuex Store}` - Use `Model.store` to access the Vuex store. [example](./common-patterns.md#Accessing-the-store-from-hooks) ## Model Methods ### find(params) Model classes have a `find` method, which is a proxy to the [`find` action](./service-plugin.html#find-params). ```js // In your Vue component created () { const { Todo } = this.$FeathersVuex.api Todo.find({ query: {} }).then(/* ... */) } ``` ### findInStore(params) Model classes have a `findInStore` method, which is a proxy to the [`find` getter](./service-plugin.html#Service-Getters). ```js // In your Vue component created () { const { Todo } = this.$FeathersVuex.api const todos = Todo.findInStore({ query: {} }) } ``` ### count(params) Model classes have a `count` method, which is a proxy to the `count` action. On the Feathers server, `$limit: 0` results in a fast count query. (./service-plugin.html#find-params). > **Note:** it only works for services with enabled pagination! ```js // In your Vue component async created () { const { Todo } = this.$FeathersVuex.api const todosCount = await Todo.count({ query: { priority: 'critical' }}) // or Todo.count().then((total) => { this.todoCount = total }) } ``` ### countInStore(params) Model classes have a `countInStore` method, which is a proxy to the [`count` getter](./service-plugin.html#Service-Getters). ```js // In your Vue component created () { const { Todo } = this.$FeathersVuex.api const todosCount = Todo.countInStore({ query: { priority: 'critical' }}) } ``` ### get(id, params) Model classes have a `get` method, which is a proxy to the [`get` action](./service-plugin.html#get-id-or-get-id-params). Notice that the signature is more Feathers-like, and doesn't require using an array to passing both id and params. ```js // In your Vue component created () { const { Todo } = this.$FeathersVuex.api Todo.get(this.id).then(/* ... */) } ``` ### getFromStore(id, params) Model classes have a `getFromStore` method, which is a proxy to the [`get` getter](./service-plugin.html#Service-Getters). Notice that the signature is more Feathers-like, and doesn't require using an array to passing both id and params. ```js // In your Vue component created () { const { Todo } = this.$FeathersVuex.api const todo = Todo.getFromStore(this.id) } ``` ### instanceDefaults `instanceDefaults(data, { store, models })` The `instanceDefaults` API was created in version 1.7 to prevent requiring to specify data for new instances created throughout the app. Depending on the complexity of the service's "business logic", it can save a lot of boilerplate. Notice that it is similar to the `setupInstance` method added in 2.0. The instanceDefaults method should ONLY be used to return default values for a new instance. Use `setupInstance` to handle other transformations on the data. Starting with version 2.0, `instanceDefaults` must be provided as a function. The function will be called with the following arguments and should return an object of default properties for new instances. - `data {Object}` - The instance data - An `utils` object containing these props: - `store` - The vuex store - `models {Object}` The `globalModels` object, which is the same as you'll find inside a component at `this.$FeathersVuex`. As an example, a User model class might look like this: ```js instanceDefaults(data, { store, models }) { return { firstName: '', lastName: '', email: '', password: '', isAdmin: false } } ``` With the above attributes in place, you no longer have to manually specify any of the listed attributes. You can, however, provided data to replace them. Calling `new User({ firstName: 'Marshall' })` will create the instance with the `firstName` filled in, already. One important note, the `isAdmin` attribute is specified in the above example in order to allow immediate binding in a form. You would pretty much NEVER allow specifying `isAdmin` from the client and storing it on the server. Attributes related to roles and app security should pretty much ALWAYS be written in hooks on the API server. ### setupInstance `setupInstance(data, { store, models })` A new `setupinstance` class method is now available in version 2.0. This method allows you to transform the data and setup the final instance based on incoming data. For example, you can access the `models` object to reference other service Model classes and create data associations. The function will be called during model instance construction with the following arguments and should return an object containing properties that'll be merged into the new instance. - `data {Object}` - The instance data - A `utils` object containing these props: - `store` - The vuex store - `models {Object}` The `globalModels` object, which is the same as you'll find inside a component at `this.$FeathersVuex`. For an example of how you might use `setupInstance`, suppose we have two services: Users and Posts. Assume that the API request to get a user includes their `posts`, already populated on the data. The `instanceDefaults` allows us to convert the array of `posts` into an array of `Post` instances. > If you're looking for a great solution for populating data to work with Feathers-Vuex, check out [feathers-graph-populate](https://feathers-graph-populate.netlify.app/). ```js // The setupInstance method on an imaginary User model. setupInstance(data, { store, models }) { if (data.posts) { // Turn posts into an array of Post instances data.posts = data.posts.map(post => new models.api.Post(post)) } return data } ``` With the above `setupInstance` method in place, each `User` instance now stores a direct reference to the `Post` records in the store. ### on Register event handlers to listen to events. ### once Register an event handler that only occurs once. ### off Remove an event handler. ## Model Events Model classes are EventEmitter instances which emit service events when received (technically, EventEmitter methods are mixed onto each Model class). All FeathersJS events are supported. Oh, and one more thing: it works with `feathers-rest` (you won't receive socket events, but you can listen for when instances are created in other parts of the app.) Here’s an example of how to use it in a component: ```js export default { created() { this.$FeathersVuex.api.Todo.on(‘created’, this.handleTodoCreated) }, destroyed() { this.$FeathersVuex.api.Todo.off(‘created’, this.handleTodoCreated) }, methods: { handleTodoCreated(todo) { console.log(todo) } } } ``` Since they have all of the EventEmitter methods, Model classes can be used as a data-layer Event Bus. You can even use custom event names: ```js const { Todo } = this.$FeathersVuex.api Todo.on('custom-event', data => { console.log(data) // { test: true } }) Todo.emit('custom-event', { test: true }) ``` ## Creating instances The [FeathersVuex plugin for Vue](./vue-plugin.md) allow convenient access to all Model constructors. You can create a Model instance by getting a reference to a Model class from the `$FeathersVuex` object: ```js // In your Vue component created () { const { Todo } = this.$FeathersVuex.api const todo = new Todo({ description: 'Do something!' }) } ``` You can also reference this directly from the Vue module: ```js import Vue from 'vue' const { Todo } = Vue.$FeathersVuex.api const todo = new Todo({ description: 'Do something!' }) ``` The examples above show instantiating a new Model instance without an `id` field. In this case, the record is not added to the Vuex store. If you instantiate a record **with an `id`** field, it **will** get added to the Vuex store. *Note: This field is customizable using the `idField` option for this service.* Now that we have Model instances, let's take a look at the functionality they provide. Each instance will include the following methods: - `.save()` - `.create()` - `.patch()` - `.update()` - `.clone()` - `.commit()` - `.reset()` and the following readonly attributes: - `isCreatePending` - `create` is currently pending on this model - `isUpdatePending` - `update` is currently pending on this model - `isPatchPending` - `patch` is currently pending on this model - `isRemovePending` - `remove` is currently pending on this model - `isSavePending` - Any of `create`, `update` or `patch` is currently pending on this model - `isPending` - Any method is currently pending on this model *Remember, if a record already has an attribute with any of these method names, it will be overwritten with the method.* These methods give access to many of the store `actions` and `mutations`. Using Model instances, you no longer have to use `mapActions` for `create`, `patch`, `update`, or `remove`. You also no longer have to use `mapMutations` for `createCopy`, `commitCopy`, or `resetCopy`. ```js store.dispatch('todos/find', { query: {} }) .then(response => { const { data } = response const todo = data[0] todo.description = 'Read Nuxt.js docs' todo.save() // Calls store.dispatch('todos/patch', [item.id, item, {}]) }) ``` ## Instance Methods ### `instance.save(params)` The `save` method is a convenience wrapper for the `create/patch` methods, by default. If the records has no `_id`, the `instance.create()` method will be used. The `params` argument will be used in the Feathers client request. See the [Feathers Service](https://docs.feathersjs.com/guides/basics/services.html#service-methods) docs, for reference on where params are used in each method. ```js // In your Vue component created () { const { Todo } = this.$FeathersVuex.api const todo = new Todo({ description: 'Do something!' }) todo.save() // --> Creates the todo on the server. } ``` Once the `create` response returns, the record will have an `_id`. If you call `instance.save()` again, it will call `instance.patch()`. Which method is used depends soletly on the data having an id (that matches the `options.idfield` for this service). As mentioned, `save` performs either `create` or `patch`, but you can use the `preferUpdate` option to change the behavior to `create/update`. ### `instance.create(params)` The `create` method calls the `create` action (service method) using the instance data. The `params` argument will be used in the Feathers client request. See the [Feathers Service](https://docs.feathersjs.com/guides/basics/services.html#service-methods) docs, for reference. You might not ever need to use `.create()`, but can instead use the `.save()` method. Let `feathers-vuex` call `create` or `patch`. ```js const { Todo } = this.$FeathersVuex.api const data = { description: 'Do something!' } const todo = new Todo(data) todo.create() // --> Creates the todo on the server using the instance data ``` ### `instance.patch(params)` The `patch` method calls the `patch` action (service method) using the instance data. The instance's id field is used for the `patch` id. The `params` argument will be used in the Feathers client request. See the [Feathers Service](https://docs.feathersjs.com/guides/basics/services.html#service-methods) docs, for reference. Similar to the `.create()` method, you might not ever need to use `.patch()` if you just use `.save()` and let `feathers-vuex` figure out how to handle it. ```js const { Todo } = this.$FeathersVuex.api const todo = new Todo({ id: 1, description: 'Do something!' }) todo.description = 'Do something else' todo.patch() // --> Sends a `patch` request the with the id and description. ``` As of version 3.9.0, you can provide an object as `params.data`, and Feathers-Vuex will use `params.data` as the patch data. This allows patching with partial data: ```js import { models } from 'feathers-vuex' const { Todo } = models.api const todo = new Todo({ description: 'Do Something', isComplete: false }) todo.patch({ data: { isComplete: true } }) ``` ### `instance.update(params)` The `update` method calls the `update` action (service method) using the instance data. The instance's id field is used for the `update` id. The `params` argument will be used in the Feathers client request. See the [Feathers Service](https://docs.feathersjs.com/guides/basics/services.html#service-methods) docs, for reference. Use `.update()` whenever you want to completely replace the data on the server with the instance data. You can also set the `preferUpdate` option to `true` to make `.save()` call `.update()` when an id field is present on the instance. ```js const { Todo } = this.$FeathersVuex.api const todo = new Todo({ id: 1, description: 'Do something!' }) todo.description = 'Do something else' todo.update() // --> Sends a `update` request the with all instance data. ``` ### `instance.remove(params)` The `remove` method calls the `remove` action (service method) using the instance data. The instance's id field is used for the `remove` id. The `params` argument will be used in the Feathers client request. See the [Feathers Service](https://docs.feathersjs.com/guides/basics/services.html#service-methods) docs, for reference. ```js const { Todo } = this.$FeathersVuex.api const todo = new Todo({ id: 1, description: 'Do something!' }) todo.save() .then(todo => { todo.remove() // --> Deletes the record from the server }) ``` ### `instance.clone()` The `.clone()` method creates a deep copy of the record and stores it on `Model.copiesById`. This allows you to make changes to the clone and not update visible data until you commit or save the data. ```js const { Todo } = this.$FeathersVuex.api const todo = new Todo({ id: 1, description: 'Do something!' }) const todoCopy = todo.clone() todoCopy.description = 'Do something else!' todoCopy.commit() // --> Update the data in the store. console.log(todo.description) // --> 'Do something else!' console.log(todoCopy.description) // --> 'Do something else!' ``` There's another use case for using `.clone()`. Vuex has a `strict` mode that's really useful in development. It throws errors if any changes occur in the Vuex store `state` outside of mutations. Clone really comes in handy here, because you can make changes to the clone without having to write custom Vuex mutations. When you're finished making changes, call `.commit()` to update the store. This gives you `strict` mode compliance with little effort! > Note: You could previously use the `keepCopiesInStore` option to keep copies in `state.copiesById`. In 2.0, this feature is deprecated and will be removed from the next release. ### `instance.commit()` ```js const { Todo } = this.$FeathersVuex.api const todo = new Todo({ id: 1, description: 'Do something!' }) const todoCopy = todo.clone() todoCopy.description = 'Do something else!' todoCopy.commit() // --> Update the data in the store. console.log(todo.description) // --> 'Do something else!' console.log(todoCopy.description) // --> 'Do something else!' ``` ### `instance.reset()` ```js const { Todo } = this.$FeathersVuex.api const todo = new Todo({ id: 1, description: 'Do something!' }) const todoCopy = todo.clone() todoCopy.description = 'Do something else!' todoCopy.reset() // --> Resets the record to match the one in the store. console.log(todo.description) // --> 'Do something!' console.log(todoCopy.description) // --> 'Do something!' ``` ================================================ FILE: docs/nuxt.md ================================================ --- title: Nuxt --- # Nuxt ### Access `$FeathersVuex` models in Nuxt `asyncData` In `feathers-vuex@2.x`, you can get access to the `$FeathersVuex` object by importing the `models` object from the main export: ``` import { models } from 'feathers-vuex' ``` The `models` and `$FeathersVuex` variables are the same object. ## Preventing Memory Leaks The default settings of Feathers-Vuex include having realtime events enabled by default. This will result in increased memory usage over time on the SSR server. It can be turned off when you configure `feathers-vuex`. The example below has been modified from the example of [Setting up the Feathers Client & Feathers-Vuex](./getting-started.html#feathers-client-feathers-vuex). Look specifically at the `enableEvents` option. ```js const { makeServicePlugin, makeAuthPlugin, BaseModel, models, FeathersVuex } = feathersVuex( feathersClient, { serverAlias: 'api', idField: '_id', whitelist: ['$regex', '$options'], enableEvents: process.client // No events for SSR server } ) ``` ## Working with Auth & Nuxt `feathers-vuex@1.0.0^` ships with utilities that help with Nuxt auth related to JSON Web Tokens (JWT). The most important utility is the `initAuth` utility. It's for use during Nuxt's `nuxtServerInit` method, and sets up auth data automatically. `initAuth` will do the following: 1. Get the accessToken from the `req` passed in 2. Get the payload from the token 3. commit the token and payload to the store with `setAccessToken` and `setPayload` 4. Set the access token on the feathers client instance so that the next time authenticate is called, it will have the JWT from the `req` to authenticate with the server. Here's an example store that uses it: ```js // ~/plugins/feathers-client.js import feathers from '@feathersjs/feathers' import socketio from '@feathersjs/socketio-client' import auth from '@feathersjs/authentication-client' import io from 'socket.io-client' import { iff, discard } from 'feathers-hooks-common' import feathersVuex, { initAuth } from 'feathers-vuex' const socket = io('http://localhost:3030', {transports: ['websocket']}) const feathersClient = feathers() .configure(socketio(socket)) .configure(auth({ storage: window.localStorage })) .hooks({ before: { all: [ iff( context => ['create', 'update', 'patch'].includes(context.method), discard('__id', '__isTemp') ) ] } }) export default feathersClient // Setting up feathers-vuex const { makeServicePlugin, makeAuthPlugin, BaseModel, models, FeathersVuex } = feathersVuex( feathersClient, { serverAlias: 'api', // optional for working with multiple APIs (this is the default value) idField: '_id', // Must match the id field in your database table/collection whitelist: ['$regex', '$options'], enableEvents: process.client // No events for SSR server } ) export { makeAuthPlugin, makeServicePlugin, initAuth, BaseModel, models, FeathersVuex } ``` ```js // ~/store/index.js import { makeAuthPlugin, initAuth, models } from '~/plugins/feathers' const auth = makeAuthPlugin({ userService: 'users', state: { publicPages: [ 'login', 'signup' ] } }) const requireModule = require.context( // The path where the service modules live './services', // Whether to look in subfolders false, // Only include .js files (prevents duplicate imports`) /.js$/ ) const servicePlugins = requireModule .keys() .map(modulePath => requireModule(modulePath).default) export const state = () => ({ // Your custom state }) export const mutations = { // Your custom mutations } export const actions = { nuxtServerInit ({ commit, dispatch }, { req }) { return initAuth({ commit, dispatch, req, moduleName: 'auth', cookieName: 'feathers-jwt' }) } } export const getters = { // Your custom getters } export const plugins = [ ...servicePlugins, auth ] ``` Notice in the above example, I've added a `publicPages` property to the auth state. Let's now use this state to redirect the browser when it's not on a public page and there's no auth: In your Nuxt project, create the file `/middleware/auth.js`. Then edit the `nuxt.config.js` and add after the `head` property, add a string that references this routing middleware so it looks like this: ```js // nuxt.config.js router: { middleware: ['auth'] } ``` Now open the middleware and paste the following content. All it does is redirect the page if there's no auth data in the store. ```js // If it's a private page and there's no payload, redirect. export default function (context) { const { store, redirect, route } = context const { auth } = store.state if (!auth.publicPages.includes(route.name) && !auth.payload) { return redirect('/login') } } ``` For a summary, the `initAuth` function will make auth available in the state without much configuration. ## Authentication storage with Nuxt Since Nuxt is running both client- and server side, it has limits on the availability of certain browser specific variables like `window`. Because of that, trying to configure the feathers client to use `window.localStorage` will result in an error or unexpected / not working behaviour. There's a simple solution though: When you configure the auth module in your feathers-client, use [cookie-storage](https://www.npmjs.com/package/cookie-storage) instead of `window.localStorage` to store the authentication data inside a cookie. ```js import { CookieStorage } from 'cookie-storage' const feathersClient = feathers() .configure(auth({ storage: new CookieStorage() })) ``` ## Server and Client in different end points If you have your feathersjs server in a different end point from your client (Ex. api.yourdomain.com - feathers / yourdomain.com - vue) your cookies wont be shared beetween server and client so you will have to authenticate your client manualy. The best solution is to use [nuxt-client-init-module](https://www.npmjs.com/package/nuxt-client-init-module). First we add it to our app using `npm install nuxt-client-init-module` or `yarn add nuxt-client-init-module`, and add it to our `nuxt.config.js` modules: ```js export default { ... modules: [ 'nuxt-client-init-module' ], } ``` Now, based on the auth example above, we will edit our `~/store/index.js` file. ```js // ~/store/index.js import { makeAuthPlugin, initAuth, models } from '~/plugins/feathers' const auth = makeAuthPlugin({ userService: 'users', state: { publicPages: [] }, actions: { // Handles initial authentication onInitAuth ({ state, dispatch }, payload) { if (payload) { dispatch('authenticate', { strategy: 'jwt', accessToken: state.accessToken }) .then((result) => { // handle success like a boss console.log('loged in') }) .catch((error) => { // handle error like a boss console.log(error) }) } } } }) ... export const actions = { nuxtServerInit ({ commit, dispatch }, { req }) { return initAuth({ commit, dispatch, req, moduleName: 'auth', cookieName: 'feathers-jwt' }) }, nuxtClientInit ({ state, dispatch, commit }, context) { // Run the authentication with the access token hydrated from the server store if (state.auth.accessToken) { return dispatch('auth/onInitAuth', state.auth.payload) } } } ... ``` ## Server side hydration When using nuxt SSR and you make requests in the server, using `fetch` or `asyncData`, nuxt will send this data and hydrate the store on client init. Because this hydration is done by nuxt, the documents do not inherit from their right classes and all documents are created as simple javascript objects. `feathers-vuex@3.x.x^` ships with the `hydrateApi` utility for this use case. We only have to pass the api's that we need to hydarate on client start to the `nuxtClientInit`. ```js // ~/plugins/feathers-client.js import feathers from '@feathersjs/feathers' import socketio from '@feathersjs/socketio-client' import auth from '@feathersjs/authentication-client' import io from 'socket.io-client' import { iff, discard } from 'feathers-hooks-common' import feathersVuex, { initAuth, hydrateApi } from 'feathers-vuex' ... export { makeAuthPlugin, makeServicePlugin, initAuth, hydrateApi, BaseModel, models, FeathersVuex } ``` ```js // ~/store/index.js import { makeAuthPlugin, initAuth, hydrateApi, models } from '~/plugins/feathers' ... export const actions = { nuxtServerInit ({ commit, dispatch }, { req }) { return initAuth({ commit, dispatch, req, moduleName: 'auth', cookieName: 'feathers-jwt' }) }, nuxtClientInit ({ state, dispatch, commit }, context) { hydrateApi({ api: models.api }) // Call once for each API to be updated hydrateApi({ api: models.otherApi }) // Run the authentication with the access token hydrated from the server store if (state.auth.accessToken) { return dispatch('auth/onInitAuth', state.auth.payload) } } } ... ``` ## Resolving Build Issues If you have issues with sub-dependencies not loading correctly, you may want to check out [this GitHub issue](https://github.com/feathersjs-ecosystem/feathers-vuex/issues/399). One of the suggestions is likely to fix the issue. ## Full nuxt configuration example [Check a full nuxt exemple in the common patterns section](./common-patterns.md#full-nuxt-example) ================================================ FILE: docs/service-plugin.md ================================================ --- title: Service Plugin sidebarDepth: 3 --- # Service Plugin The `makeServicePlugin` method creates a vuex plugin which connects a Feathers service to the Vuex store. Once you create a plugin, you must register it in the Vuex store's `plugins` section. ## Setup See the [setup documentation](./getting-started.html#service-plugins) to learn the basics of setting up a Service Plugin. ## Configuration The following options are supported on `makeServicePlugin`: ```js import Model from "../users.model"; const servicePath = 'users' const servicePlugin = makeServicePlugin({ // necesarry Model, service: feathersClient.service(servicePath), // optional and configurable also by global config idField: 'id', tempIdField: '__id', nameStyle: 'short', debug: false, addOnUpsert: false, autoRemove: false, preferUpdate: false, replaceItems: false, skipRequestIfExists: false, paramsForServer: ['$populateParams'], whitelist: [], enableEvents: true, handleEvents: { created: (item, { model, models }) => options.enableEvents, patched: (item, { model, models }) => options.enableEvents, updated: (item, { model, models }) => options.enableEvents, removed: (item, { model, models }) => options.enableEvents }, // optional and only configurable per service servicePath: '', namespace: null, modelName: 'User', instanceDefaults: () => ({}), setupInstance: instance => instance, state: {}, getters: {}, mutations: {}, actions: {}, //... }); ``` The following options can also be configured in [Global Configuration](getting-started.html#global-configuration) for every service initialized using `feathers-client.js`: - `idField {String}` - **Default:** `globalConfig: 'id'` - The field in each record that will contain the id - `tempIdField {Boolean}` - **Default:** `globalConfig: '__id'` - The field in each temporary record that contains the id - `nameStyle {'short'|'path'}` - **Default:** `globalConfig: 'short'` - Use the full service path as the Vuex module name, instead of just the last section. - `debug {Boolean}` - **Default:** `globalConfig: false` - Enable some logging for debugging - `addOnUpsert {Boolean}` - **Default:** `globalConfig: false` - If `true` add new records pushed by 'updated/patched' socketio events into store, instead of discarding them. - `autoRemove {Boolean}` - **Default:** `globalConfig: false` - If `true` automatically remove records missing from responses (only use with feathers-rest) - `preferUpdate {Boolean}` - **Default:** `globalConfig: false` - If `true`, calling `model.save()` will do an `update` instead of a `patch`. - `replaceItems {Boolean}` - **Default:** `globalConfig: false` - If `true`, updates & patches replace the record in the store. Default is false, which merges in changes. - `skipRequestIfExists {Boolean}` - **Default:** `globalConfig: false` - For get action, if `true` the record already exists in store, skip the remote request. - `paramsForServer {Array}` - **Default:** `['$populateParams']` - Custom query operators that are ignored in the find getter, but will pass through to the server. It is preconfigured to work with the `$populateParams` custom operator from [feathers-graph-populate](https://feathers-graph-populate.netlify.app/). - `whitelist {Array}` - Custom query operators that will be allowed in the find getter. - `enableEvents {Boolean}` - **Default:** `globalConfig: true` - If `false` socket event listeners will be turned off - `handleEvents {Object}`: For this to work `enableEvents` must be `true` - `created {Function}` - **Default:** `(item, { model, models }) => options.enableEvents` - handle `created` events, return true to add to the store - `patched {Function}` - **Default:** `(item, { model, models }) => options.enableEvents` - handle `created` events, return true to update in the store - `updated {Function}` - **Default:** `(item, { model, models }) => options.enableEvents` - handle `created` events, return true to update in the store - `removed {Function}` - **Default:** `(item, { model, models }) => options.enableEvents` - handle `removed` events, return true to remove from the store - The following options can only configured individually per service plugin - `servicePath {String}`- Not all Feathers service plugins expose the service path, so it can be manually specified when missing. - `namespace {String}`, - **Default:** `nameStyle === 'short' ? ${afterLastSlashOfServicePath} : ${stripSlashesFromServicePath}` - Customize the Vuex module name. Overrides nameStyle. - `modelName {String}` - **Default:** `${ServicePlugin.Model.modelName}` - `instanceDefaults {Function}` - **Default:** `() => ({})` - Override this method to provide default data for new instances. If using Model classes, specify this as a static class property. - `setupInstance {Function}` - **Default:** `instance => instance` - Override this method to setup data types or related data on an instance. If using Model classes, specify this as a static class property. - `state {Object}` - **Default:**: `null` - Pass custom `states` to the service plugin or modify existing ones - `getters {Object}` - **Default:** `null` - Pass custom `getters` to the service plugin or modify existing ones - `mutations {Object}` - **Default:** `null` - Pass custom `mutations` to the service plugin or modify existing ones - `actions {Object}` - **Default:** `null` - Pass custom `actions` to the service plugin or modify existing ones ## Realtime by Default Service plugins automatically listen to all socket messages received by the Feathers Client. This can be disabled by setting `enableEvents: false` in the options, as shown above. ## The FeathersClient Service Once the service plugin has been registered with Vuex, the FeathersClient Service will have a new `service.FeathersVuexModel` property. This provides access to the service's [Model class](./model-classes.html). ```js import { models } from 'feathers-vuex' feathersClient.service('todos').FeathersVuexModel === models.api.Todo // true ``` ## Service State Each service comes loaded with the following default state: ```js { ids: [], idField: 'id', keyedById: {}, tempsById: {}, tempsByNewId: {}, pagination: { defaultLimit: null, defaultSkip: null }, servicePath: 'v1/todos' modelName: 'Todo', autoRemove: false, replaceItems: false, pagination: { ids: [] limit: 0 skip: 0 ip: 0 total: 0, mostRecent: any }, paramsForServer: ['$populateParams'], whitelist: [], isFindPending: false, isGetPending: false, isCreatePending: false, isUpdatePending: false, isPatchPending: false, isRemovePending: false, errorOnfind: undefined, errorOnGet: undefined, errorOnCreate: undefined, errorOnUpdate: undefined, errorOnPatch: undefined, errorOnRemove: undefined, isIdCreatePending: [], isIdUpdatePending: [], isIdPatchPending: [], isIdRemovePending: [], } ``` - `ids {Array}` - an array of plain ids representing the ids that belong to each object in the `keyedById` map. - `idField {String}` - the name of the field that holds each item's id. *Default: `'id'`* - `keyedById {Object}` - a hash map keyed by the id of each item. - `tempsById {Object}` - a hash map of temporary records, [keyed by tempIdField](./getting-started.html#global-configuration) of each item - `tempsByNewId {Object}` - temporary storage for temps while getting transferred from tempsById to keyedById - `servicePath {String}` - the full service path, even if you alias the namespace to something else. - `modelName {String}` - the key in the $FeathersVuex plugin where the model will be found. - `autoRemove {Boolean}` - indicates that this service will not automatically remove results missing from subsequent requests. Only use with feathers-rest. Default is false. - `replaceItems {Boolean}` - When set to true, updates and patches will replace the record in the store instead of merging changes. Default is false - `pagination {Object}` - provides informaiton about the last made queries - `paramsForServer {Array}` - Custom query operators that are ignored in the find getter, but will pass through to the server. - `whitelist {Array}` - Custom query operators that will be allowed in the find getter. The following state attributes allow you to bind to the pending state of requests: - `isFindPending {Boolean}` - `true` if there's a pending `find` request. `false` if not. - `isGetPending {Boolean}` - `true` if there's a pending `get` request. `false` if not. - `isCreatePending {Boolean}` - `true` if there's a pending `create` request. `false` if not. - `isUpdatePending {Boolean}` - `true` if there's a pending `update` request. `false` if not. - `isPatchPending {Boolean}` - `true` if there's a pending `patch` request. `false` if not. - `isRemovePending {Boolean}` - `true` if there's a pending `remove` request. `false` if not. The following state attribute will be populated with any request error, serialized as a plain object: - `errorOnFind {Error}` - `errorOnGet {Error}` - `errorOnCreate {Error}` - `errorOnUpdate {Error}` - `errorOnPatch {Error}` - `errorOnRemo {Error}` The following state attributes allow you to bind to the pending state of requests *per item ID* - `isIdCreatePending {Array}` - Contains `id` if there's a pending `create` request for `id`. - `isIdUpdatePending {Array}` -Contains `id` if there's a pending `update` request for `id`. - `isIdPatchPending {Array}` - Contains `id` if there's a pending `patch` request for `id`. - `isIdRemovePending {Array}` - Contains `id` if there's a pending `remove` request for `id`. ## Service Getters Service modules include the following getters: - `list {Array}` - an array of items. The array form of `keyedById` Read only. - `find(params) {Function}` - a helper function that allows you to use the [Feathers Adapter Common API](https://docs.feathersjs.com/api/databases/common) and [Query API](https://docs.feathersjs.com/api/databases/querying) to pull data from the store. This allows you to treat the store just like a local Feathers database adapter (but without hooks). - `params {Object}` - an object with a `query` object and optional properties. You can set the following properties: - `params.query {Boolean}` - The `query` is in the FeathersJS query format. - `params.temps {Boolean}` - **Default:** `false` - if `true` also consider temporary records from `tempsById` - `params.copies {Boolean}` - **Default:** `false` - if `true`: first search for the regular records and then replace the records with the related copies from `copiesById` - `count(params) {Function}` - a helper function that counts items in the store matching the provided query in the params and returns this number - `params {Object}` - an object with a `query` object and an optional `temps` boolean property. - `get(id[, params]) {Function}` - a function that allows you to query the store for a single item, by id. It works the same way as `get` requests in Feathers database adapters. - `id {Number|String}` - the id of the data to be retrieved by id from the store. - `params {Object}` - an object containing a Feathers `query` object. The following getters ease access to per-instance pending status - `isCreatePendingById(id) {Function}` - Check if `create` is pending for `id` - `isUpdatePendingById(id) {Function}` - Check if `update` is pending for `id` - `isPatchPendingById(id) {Function}` - Check if `patch` is pending for `id` - `isRemovePendingById(id) {Function}` - Check if `remove` is pending for `id` - `isSavePendingById(id) {Function}` - Check if `create`, `update`, or `patch` is pending for `id` - `isPendingById(id) {Function}` - Check if `create`, `update`, `patch` or `remove` is pending for `id` ## Service Mutations The following mutations are included in each service module. > **Note:** you would typically not call these directly, but instead with `store.commit('removeItem', 'itemId')`. Using vuex's mapMutations on a Vue component can simplify that to `this.removeItem('itemId')` ### `addItem(state, item)` Adds a single item to the `keyedById` map. - `item {Object}` - The item to be added to the store. ### `addItems(state, items)` Adds an array of items to the `keyedById` map. - `items {Array}` - the items to be added to the store. ### `updateItem(state, item)` Updates an item in the store to match the passed in `item`. - `item {Object}` the item, including `id`, to replace the currently-stored item. ### `updateItems(state, items)` Updates multiple items in the store to match the passed in array of items. - `items {Array}` - An array of items. ### `removeItem(state, item)` Removes a single item. `item` can be - `item {Number|String|Object}` - The item or id of the item to be deleted. ### `removeTemps(state, tempIds)` // Removes temp records. Also cleans up tempsByNewId - `items {Array}` - An array of ids or of objects with tempIds that will be removed from the data store ### `removeItems(state, items)` Removes the passed in items or ids from the store. - `items {Array}` - An array of ids or of objects with ids that will be removed from the data store. ### `clearAll(state)` Clears all data from `ids`, `keyedById`, and `currentId` ### Mutations for Managing Pending State The following mutations are called automatically by the service actions, and will rarely, if ever, need to be used manually. - `setPending(state, method)` - sets the `is${method}Pending` attribute to true - `setIdPending(state, { method, id })` - adds `id` to `isId${method}Pending` array - `unsetPending(state, method)` - sets the `is${method}Pending` attribute to false - `unsetIdPending(state, { method, id })` - removes `id` from `isId${method}Pending` array ### Mutations for Managing Errors The following mutations are called automatically by the service actions, and will rarely need to be used manually. - `setError(state, { method, error })` - sets the `errorOn${method}` attribute to the error - `clearError(state, method)` - sets the `errorOn${method}` attribute to `null` ## Service Actions An action is included for each of the Feathers service interface methods. These actions will affect changes in both the Feathers API server and the Vuex store. All of the [Feathers Service Methods](https://docs.feathersjs.com/api/databases/common#service-methods) are supported. Because Vuex only supports providing a single argument to actions, there is a slight change in syntax that works well. If you need to pass multiple arguments to a service method, pass an array to the action with the order of the array elements matching the order of the arguments. See each method for examples. > Note: If you use the Feathers service methods, directly, the store will not change. Only the actions will cause store changes. ### `find(params)` Query an array of records from the server & add to the Vuex store. - `params {Object}` - An object containing a `query` object and an optional `paginate` boolean. You can set `params.paginate` to `false` to disable pagination for a single request. ```js let params = {query: {completed: true}} store.dispatch('todos/find', params) ``` See the section about pagination, below, for more information that is applicable to the `find` action. Make sure your returned records have a unique field that matches the `idField` option for the service plugin. ### `afterFind(response)` The `afterFind` action is called by the `find` action after a successful response is added to the store. It is called with the current response. By default, it is a no-op (it literally does nothing), and is just a placeholder for you to use when necessary. See the sections on [customizing the default store](#Customizing-a-Service’s-Default-Store) and [Handling custom server responses](./common-patterns.html#Handling-custom-server-responses) for example usage. ### `count(params)` Count items on the server matching the provided query. - `params {Object}` - An object containing a `query` object. In the background `$limit: 0` will be added to the `query` to perform a (fast) counting query against the database. > **Note:** it only works for services with enabled pagination! ```js let params = {query: {completed: false}} store.dispatch('todos/count', params) ``` This will run a (fast) counting query against the database and return a page object with the total and an empty data array. ### `get(id)` or `get([id, params])` Query a single record from the server & add to Vuex store - `id {Number|String}` - the `id` of the record being requested from the API server. - `params {Object}` - An object containing a `query` object. ```js store.dispatch('todos/get', 1) // Use an array to pass params let params = {} store.dispatch('todos/get', [1, params]) ``` Make sure your returned records have a unique field that matches the `idField` option for the service plugin. ### `create(data|ParamArray)` Create one or multiple records. Note that the method is overloaded to accept two types of arguments. If you want a consistent interface for creating single or multiple records, use the array syntax, described below. Creating multiple records requires using the `paramArray` syntax. - `data {Object|ParamArray}` - if an object is provided, a single record will be created. ```js let newTodo = {description: 'write good tests'} store.dispatch('todos/create', newTodo) ``` - `data {ParamArray}` - if an array is provided, it is assumed to have this structure: - `ParamArray {Array}` - array containing the two parameters that Feathers' `service.create` method accepts. - `data {Object|Array}` - the data to create. Providing an object creates a single record. Providing an array of objects creates multiple records. - `params {Object}` - optional - an object containing a `query` object. Can be useful in rare situations. Make sure your returned records have a unique field that matches the `idField` option for the service plugin. ### `update(paramArray)` Update (overwrite) a record. - `paramArray {Array}` - array containing the three parameters update accepts. - `id {Number|String}` - the `id` of the existing record being requested from the API server. - `data {Object}` - the data that will overwrite the existing record - `params {Object}` - An object containing a `query` object. ```js let data = {id: 5, description: 'write your tests', completed: true} let params = {} // Overwrite item 1 with the above data (FYI: Most databases won't let you change the id.) store.dispatch('todos/update', [1, data, params]) ``` Alternatively in a Vue component ```js import { mapActions } from 'vuex' export default { methods: { ...mapActions('todos', [ 'update' ]), addTodo () { let data = {id: 5, description: 'write your tests', completed: true} this.update([1, data, {}]) } } } ``` Make sure your returned records have a unique field that matches the `idField` option for the service plugin. ### `patch(paramArray)` Patch (merge in changes) one or more records - `paramArray {Array}` - array containing the three parameters patch takes. - `id {Number|String}` - the `id` of the existing record being requested from the API server. - `data {Object}` - the data that will be merged into the existing record - `params {Object}` - An object containing a `query` object. If params.data is provided, it will be used as the patch data, providing a simple way to patch with partial data. ```js let data = {description: 'write your tests', completed: true} let params = {} store.dispatch('todos/patch', [1, data, params]) ``` Make sure your returned records have a unique field that matches the `idField` option for the service plugin. ### `remove(id)` Remove/delete the record with the given `id`. - `id {Number|String}` - the `id` of the existing record being requested from the API server. ```js store.dispatch('todos/remove', 1) ``` Make sure your returned records have a unique field that matches the `idField` option for the service plugin. ## Service Events By default, the service plugin listens to all of the FeathersJS events: - `created` events will add new record to the store. - `patched` events will add (if new) or update (if present) the record in the store. - `updated` events will add (if new) or update (if present) the record in the store. - `removed` events will remove the record from the store, if present. This behavior can be turned off completely by passing `enableEvents: false` in either the global Feathers-Vuex options or in the service plugin options. If you configure this at the global level, the service plugin level will override it. For example, if you turn off events at the global level, you can enable them for a specific service by setting `enableEvents: true` on that service's options. ### Custom Event Handlers As of version 3.1, you can customize the behavior of the event handlers, or even perform side effects based on the event data. This is handled through the new `handleEvents` option on the service plugin. Here is an example of how you might use this: ```js handleEvents: { created: (item, { model, models }) => { // Perform a side effect to remove any record with the same `name` const existing = Model.findInStore({ query: { name: item.name }}).data[0] if (existing) { existing.remove() } // Perform side effects with other models. const { SomeModel } = models.api new SomeModel({ /* some custom data */ }).save() // Access the store through model.store const modelState = model.store.state[model.namespace] if (modelState.keyedById[5]) { console.log('we accessed the vuex store') } // If true, the new item will be stored. return true }, updated: () => false, // Ignore `updated` events. patched: item => item.hasPatchedAttribute && item.isWorthKeeping, removed: item => true // The default value, will remove the record from the store } ``` As shown above, each handler has two possible uses: 1. Control the default behavior of the event by returning a boolean. - For `created`, `patched`, and `updated` a truthy return will add or update the item in the store. - For `removed` a truthy return will remove the item from the store, if present. 2. Perform side effects using the current service `model` or with other `models`. The `models` object is the same as the `$FeathersVuex` object in the Vue plugin. Each handler receives the following arguments: - `item`: the record sent from the API server - `utils`: an object containing the following properties - `model` The current service's Model class. - `models` The same as the `$FeathersVuex` object, gives you access to each api with their respective model classes. You do not have to specify a handler for every event. Any that are not specified in your service-specific `handleEvents`, will fall back to using the `handleEvents` handler in your global options. If none are defined for the service or globally, the default behavior is controlled by the `enableEvents` option. #### Handling complex events If your application emits the standard Feathers service events inside a nested object with additional data, you can use `handleEvents` to tell FeathersVuex what part of that data is actually the model data that should be used to update the store. To do this, use `handleEvents` as described before, but return a tuple `[affectsStore, modelData]` from your handler. - `affectsStore` a truthy value indicates the event should update the store - `modelData` is the model data used to update the store For example, you've configured your Feathers API to emit `patched` events for your `Todos` service that include context about the event which look like ```json { "$context": { "time": 1445411009000, "userId": 121, "deviceId": "Marty's iPhone" }, "event": { "id": 88, "text": "Get back to the past", "done": true } } ``` For this service to play nicely with FeathersVuex, you'll need to use `handleEvents` ```js handleEvents: { patched: (item, { model, models }) => { // Perform any side effects... // If the first element is truthy, the item will update the store // The second element is the actual model data to add to the store return [true, item.event] } } ``` The original event data is bubbled to [Model events](./model-classes.md#model-events) so listeners receive the full event context. ## Pagination and the `find` action Both the `find` action and the `find` getter support pagination. There are differences in how they work. Important: For the built in pagination features to work, you must not directly manipulate the `context.params` object in any before hooks. You can still use before hooks as long as you clone the params object, then make changes to the clone. ### The `find` action The `find` action queries data from the remote server. It returns a promise that resolves to the response from the server. The presence of pagination data will be determined by the server. `feathers-vuex@1.0.0` can store pagination data on a per-query basis. The `pagination` store attribute maps queries to their most-recent pagination data. The default pagination state looks like this: ```js { pagination: { defaultLimit: null, defaultSkip: null } } ``` You should never manually change these values. They are managed internally. There's not a lot going on, by default. The `defaultLimit` and `defaultSkip` properties are null until a query is made on the service without `$limit` or `$skip`. In other words, they remain `null` until an empty query comes through, like the this one: **`params = { query: {} }`** ```js { pagination : { defaultLimit: 25, defaultSkip: 0, default: { mostRecent: { query: {}, queryId: '{}', queryParams: {}, pageId: '{$limit:25,$skip:0}', pageParams: { $limit: 25, $skip: 0 }, queriedAt: 1538594642481 }, '{}': { total: 155, queryParams: {}, '{$limit:25,$skip:0}': { pageParams: { $limit: 25, $skip: 0 }, ids: [ 1, 2, 3, 4, '...etc', 25 ], queriedAt: 1538594642481 } } } } } ``` It looks like a lot just happened, so let's walk through it. First, notice that we have values for `defaultLimit` and `defaultSkip`. These come in handy for the `find` getter, which will be covered later. ### The `qid` The state now also contains a property called `default`. This is the default `qid`, which is a "query identifier" that you choose. Unless you're building a small demo, your app will require to storing pagination information for more than one query. For example, two components could make two distinct queries against this service. You can use the `params.qid` (query identifier) property to assignn identifier to the query. If you set a `qid` of `mainListView`, for example, the pagination for this query will show up under `pagination.mainListView`. The `pagination.default` property will be used any time a `params.qid` is not provided. Here's an example of what this might look like: **`params = { query: {}, qid: 'mainListView' }`** ```js // Data in the store { pagination : { defaultLimit: 25, defaultSkip: 0, mainListView: { mostRecent: { query: {}, queryId: '{}', queryParams: {}, pageId: '{$limit:25,$skip:0}', pageParams: { $limit: 25, $skip: 0 }, queriedAt: 1538594642481 }, '{}': { total: 155, queryParams: {}, '{$limit:25,$skip:0}': { pageParams: { $limit: 25, $skip: 0 }, ids: [ 1, 2, 3, 4, '...etc', 25 ], queriedAt: 1538594642481 } } } } } ``` The above example is almost exactly the same as the previous one. The only difference is that the `default` key is now called `mainListView`. This is because we provided that value as the `qid` in the params. Let's move on to the properties under the `qid`. ### The `mostRecent` object The `mostRecent` propery contains information about the most recent query. These properties provide insight into how pagination works. The two most important properties are the `queryId` and the `pageId`. - The `queryId` describes the set of data we're querying. It's a stable, stringified version of all of the query params **except** for `$limit` and `$skip`. - The `pageId` holds information about the current "page" (as in "page-ination"). A page is described using `$limit` and `$skip`. The `queryParams` and `pageParams` are the non-stringified `queryId` and `pageId`. The `query` attribute is the original query that was provided in the request params. Finally, the `queriedAt` is a timestamp of when the query was performed. ### The `queryId` and `pageId` tree The rest of the `qid` object is keyed by `queryId` strings. Currently, we only have a single `queryId` of `'{}'`. In the `queryId` object we have the `total` numer of records (as reported by the server) and the `pageId` of `'{$limit:25,$skip:0}'` ```js '{}': { // queryId total: 155, queryParams: {}, '{$limit:25,$skip:0}': { // pageId pageParams: { $limit: 25, $skip: 0 }, ids: [ 1, 2, 3, 4, '...etc', 25 ], queriedAt: 1538594642481 } } ``` The `pageId` object contains the `queriedAt` timestamp of when we last queried this page of data. It also contains an array of `ids`, holding only the `ids` of the records returned from the server. ### Additional Queries and Pages As more queries are made, the pagination data will grow to represent what we have in the store. In the following example, we've made an additional query for sorted data in the `mainListView` `qid`. We haven't filtered the list down any, so the `total` is the same as before. We have sorted the data by the `isComplete` attribute, which changes the `queryId`. You can see the second `queryId` object added to the `mainListView` `qid`: **`params = { query: {}, qid: 'mainListView' }`**
**`params = { query: { $limit: 10, $sort: { isCompleted: 1 } }, qid: 'mainListView' }`** ```js // Data in the store { pagination : { defaultLimit: 25, defaultSkip: 0, mainListView: { mostRecent: { query: { $sort: { isCompleted: 1 } }, queryId: '{$sort:{isCompleted:1}}', queryParams: { $sort: { isCompleted: 1 } }, pageId: '{$limit:10,$skip:0}', pageParams: { $limit: 10, $skip: 0 }, queriedAt: 1538595856481 }, '{}': { total: 155, queryParams: {}, '{$limit:25,$skip:0}': { pageParams: { $limit: 25, $skip: 0 }, ids: [ 1, 2, 3, 4, '...etc', 25 ], queriedAt: 1538594642481 } }, '{$sort:{isCompleted:1}}': { total: 155, queryParams: {}, '{$limit:10,$skip:0}': { pageParams: { $limit: 10, $skip: 0 }, ids: [ 4, 21, 19, 29, 1, 95, 62, 21, 67, 125 ], queriedAt: 1538594642481 } } } } } ``` In summary, any time a query param other than `$limit` and `$skip` changes, we get a new `queryId`. Whenever `$limit` and `$skip` change, we get a new `pageId` inside the current `queryId`. ### Why use this pagination structure Now that we've reviewed how pagination tracking works under the hood, you might be asking "Why?" There are a few reasons: 1. Improve performance with cacheing. It's now possible to skip making a query if we already have valid data for the current query. The [`makeFindMixin`](./mixins.html) mixin makes this very easy with its built-in `queryWhen` feature. 2. Allow fall-through cacheing of paginated data. A common challenge occurs when you provide the same query params to the `find` action and the `find` getter. As you'll learn in the next section, the `find` getter allows you to make queries against the Vuex store as though it were a Feathers database adapter. But what happens when you pass `{ $limit: 10, $skip: 10 }` to the action and getter?
First, lets review what happens with the `find` action. The database is aware of all 155 records, so it skips the first 10 and returns the next 10 records. Those records get populated in the store, so the store now has 10 records. Now we pass the query to the `find` getter and tell it to `$skip: 10`. It skips the only 10 records that are in the store and returns an empty array! That's definitely not what we wanted.
Since we're now storing this pagination structure, we can build a utility around the `find` getter which will allow us to return the same data with the same query. The data is still reactive and will automatically update when a record changes. There's one limitation to this solution. What happens when you add a new record that matches the current query? Depending on where the new record would be sorted into the current query, part or all of the cache is no longer valid. It will stay this way until a new query is made. To get live (reactive) lists, you have to use the `find` getter with its own distinct query, removing the `$limit` and `$skip` values. This way, when a new record is created, it will automatically get added to the array in the proper place. ## Pagination and the `find` getter The `find` getter queries data from the local store using the same Feathers query syntax as on the server. It is synchronous and returns the results of the query with pagination. Pagination cannot be disabled. It accepts a params object with a `query` attribute. It does not use any other special attributes. The returned object looks just like a paginated result that you would receive from the server: **`params = { query: {} }`** ```js // The returned results object { data: [{ _id: 1, ...etc }, ...etc], limit: 0, skip: 0, total: 3 } ``` ## Customizing a Service's Default Store ### New `extend` option for `makeServicePlugin` As of version `3.14.0`, the `makeServicePlugin` now supports an `extend` method that allows customizing the store and gives access to the actual Vuex `store` object, as shown in this example: ```js import { makeServicePlugin } from ‘feathers-vuex’ import { feathersClient } from ‘./feathers-client.js’ class Todo { /* truncated */ } export default makeServicePlugin({ Model: Todo, service: feathersClient.service(‘todos’), extend({ store, module }) { // Listen to other parts of the store store.watch(/* truncated */) return { state: {}, getters: {}, mutations: {}, actions: {} } } }) ``` ### Deprecated options for customizing the store Before version `3.14.0`, you can customize the store using the options for `state`, `getters`, `mutations`, and `actions`, as shown below. This method is now deprecated and will be removed from Feathers-Vuex 4.0. ```js // src/store/services/users.js import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client' class Asset extends BaseModel { constructor(data, options) { super(data, options) } // Required for $FeathersVuex plugin to work after production transpile. static modelName = 'Asset' // Define default properties here static instanceDefaults() { return { email: '', password: '' } } } const servicePath = 'assets' const servicePlugin = makeServicePlugin({ Model: Asset, service: feathersClient.service(servicePath), servicePath, state: { test: true }, getters: { getSomeData () { return 'some data' } }, mutations: { setTest (state, val) { state.test = val; }, }, actions: { // Overwriting the built-in `afterFind` action. afterFind ({ commit, dispatch, getters, state }, response) { // Do something with the response. // Keep in mind that the data is already in the store. }, asyncStuff ({ state, getters, commit, dispatch }, args) { commit('setTestToTrue') return new Promise.resolve("") } } }) export default servicePlugin ``` ================================================ FILE: docs/vue-plugin.md ================================================ --- title: Vue Plugin --- # The Vue Plugin This `feathers-vuex` release includes a Vue plugin which gives all of your components easy access to the data modeling classes. It also automatically registers the included components. The below example is based on the [setup instructions in the API overview](/api-overview.html#setup). ```js // src/store/store.js import Vue from 'vue' import Vuex from 'vuex' import { FeathersVuex } from '../feathers-client' import auth from './store.auth' Vue.use(Vuex) Vue.use(FeathersVuex) const requireModule = require.context( // The path where the service modules live './services', // Whether to look in subfolders false, // Only include .js files (prevents duplicate imports`) /.js$/ ) const servicePlugins = requireModule .keys() .map(modulePath => requireModule(modulePath).default) export default new Vuex.Store({ state: {}, mutations: {}, actions: {}, plugins: [...servicePlugins, auth] }) ``` ## Using the Vue Plugin Once registered, you'll have access to the `this.$FeathersVuex` object. *In version 2.0, there is a breaking change to this object's structure.* Instead of directly containing references to the Model classes, the top level is keyed by `serverAlias`. Each `serverAlias` then contains the Models, keyed by name. This allows Feathers-Vuex 2.0 to support multiple FeathersJS servers in the same app. This new API means that the following change is required wherever you reference a Model class: ```js // 1.x way new this.$FeathersVuex.User({}) // 2.x way new this.$FeathersVuex.api.User({}) // Assuming default serverAlias of `api`. new this.$FeathersVuex.myApi.user({}) // If you customized the serverAlias to be `myApi`. ``` The name of the model class is automatically inflected to singular, initial caps, based on the last section of the service path (split by `/`). Here are some examples of what this looks like: | Service Name | Model Name in `$FeathersVuex` | | ------------------------- | ----------------------------- | | /cart | Cart | | /todos | Todo | | /v1/districts | District | | /some/deeply/nested/items | Item | The `$FeathersVuex` object is available on the Vue object, directly at `Vue.$FeathersVuex`, as well as on the prototype, making it available in components: ```js // In your Vue component created () { const todo = new this.$FeathersVuex.Todo({ description: 'Do something!' }) // `todo` is now a model instance } ``` ## New in 2.0 In Feathers-Vuex 2.0, the $FeathersVuex object is available as the 'models' export in the global package scope. This means you can do the following anywhere in your app: ```js import { models } from 'feathers-vuex' const user = new models.api.User({ email: 'test@test.com' }) ``` ## Included Components When you register the Vue Plugin, a few components are automatically globally registered: - The [Renderless Data components](/data-components.html) - The [`FeathersVuexFormWrapper` component](/feathers-vuex-forms.html#feathersvuexformwrapper) - The [`FeathersVuexInputWrapper` component](/feathers-vuex-forms.html#feathersvuexinputwrapper) - The [`FeathersVuexPagination` component](/composition-api.html#feathersvuexpagination) You can pass `components: false` in the options to not globally register the component: ```js Vue.use(FeathersVuex, { components: false }) ``` ================================================ FILE: mocha.opts ================================================ --compilers js:babel-core/register test/node.test.js ================================================ FILE: notes.old.md ================================================ ## Extending the built-in Model classes If you desire to extend the built-in Models **store/index.js:** ```js import Vue from 'vue' import Vuex from 'vuex' import feathersVuex from 'feathers-vuex' import feathersClient from '../feathers-client' const { service, auth, FeathersVuex } = feathersVuex(feathersClient, { idField: '_id' }) const { serviceModule, serviceModel, servicePlugin } = service const api1Client = feathersVuex(feathersClient, { idField: '_id', apiPrefix: 'api1' }) const api2Client = feathersVuex(feathersClient2, { idField: '_id' }) Vue.use(FeathersVuex) const todoModule = serviceModule('todos') // const Model = serviceModel(todoModule) // TodoModel is an extensible class const Model = serviceModel() class TodoModel = extends Model {} const todoPlugin = servicePlugin(todoModule, TodoModel) const TaskModel extends Model {} export { TaskModel } created () { this.todo = new this.$FeathersVuex.api1.Todo(data) } Vue.use(Vuex) Vue.use(FeathersVuex) export default new Vuex.Store({ plugins: [ servicePlugin('/tasks', TaskModel), // With our potentially customized TodoModel service('todos'), // Specify custom options per service service('/v1/tasks', { idField: '_id', // The field in each record that will contain the id nameStyle: 'path', // Use the full service path as the Vuex module name, instead of just the last section namespace: 'custom-namespace', // Customize the Vuex module name. Overrides nameStyle. autoRemove: true, // Automatically remove records missing from responses (only use with feathers-rest) enableEvents: false, // Turn off socket event listeners. It's true by default addOnUpsert: true, // Add new records pushed by 'updated/patched' socketio events into store, instead of discarding them. It's false by default skipRequestIfExists: true, // For get action, if the record already exists in store, skip the remote request. It's false by default modelName: 'Task' }) // Add custom state, getters, mutations, or actions, if needed. See example in another section, below. service('things', { state: {}, getters: {}, mutations: {}, actions: {} }) auth() ] }) ``` ================================================ FILE: package.json ================================================ { "name": "feathers-vuex", "description": "FeathersJS, Vue, and Nuxt for the artisan developer", "version": "3.16.0", "homepage": "https:feathers-vuex.feathersjs-ecosystem.com", "main": "dist/", "module": "dist/", "types": "dist/", "keywords": [ "vue", "feathers", "feathers-plugin" ], "license": "MIT", "repository": { "type": "git", "url": "git://github.com/feathersjs-ecosystem/feathers-vuex.git" }, "author": { "name": "Marshall Thompson", "email": "marshall@creativeideal.net", "url": "https://github.com/marshallswain" }, "funding": { "type": "Github sponsor", "url": "https://github.com/sponsors/marshallswain" }, "contributors": [], "bugs": { "url": "https://github.com/feathersjs-ecosystem/feathers-vuex/issues" }, "engines": { "node": ">= 4.6.0" }, "scripts": { "prepublish": "npm run compile", "publish": "git push origin --tags && git push origin", "release:pre": "npm version prerelease && npm publish --tag pre", "release:patch": "npm version patch && npm publish", "release:minor": "npm version minor && npm publish", "release:major": "npm version major && npm publish", "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", "compile": "shx rm -rf lib/ && tsc && npm run lint-dist", "lint-dist": "prettier --write \"dist/**/*.js\"", "watch": "shx rm -rf lib/ && babel --watch -d lib/ src/", "lint": "standard --fix", "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --opts mocha.opts", "test": "cross-env TS_NODE_PROJECT='tsconfig.test.json' mocha --require ts-node/register 'test/**/*.test.ts'", "testee": "testee test/index.html --browsers firefox", "start": "npm run compile && node example/app", "docs": "vuepress dev docs", "docs:build": "vuepress build docs" }, "prettier": { "singleQuote": true, "semi": false, "trailingComma": "none", "tabWidth": 2 }, "eslintConfig": { "root": true, "env": { "node": true, "mocha": true }, "extends": [ "plugin:@typescript-eslint/recommended", "prettier/@typescript-eslint", "plugin:prettier/recommended" ], "rules": { "linebreak-style": [ "warn", "unix" ], "prettier/prettier": [ "warn", { "fix": true, "singleQuote": true, "semi": false, "trailingComma": "none", "arrowParens": "avoid" } ] }, "parserOptions": { "parser": "@typescript-eslint/parser", "ecmaVersion": 2018, "sourceType": "module" } }, "steal": { "map": { "assert": "chai/chai" }, "meta": { "chai/chai": { "format": "global", "exports": "chai.assert" } }, "plugins": [ "chai" ] }, "directories": { "lib": "lib" }, "peerDependencies": { "@vue/composition-api": "*" }, "dependencies": { "@feathersjs/adapter-commons": "^4.5.2", "@feathersjs/commons": "^4.5.3", "@feathersjs/errors": "^4.5.3", "@types/feathersjs__feathers": "^3.1.5", "@types/inflection": "^1.5.28", "@types/lodash": "^4.14.150", "@types/npm": "^2.0.31", "bson-objectid": "^1.3.0", "debug": "^4.1.1", "events": "^3.1.0", "fast-copy": "^2.1.0", "fast-json-stable-stringify": "^2.1.0", "inflection": "^1.12.0", "jwt-decode": "^2.2.0", "lodash": "^4.17.15", "lodash.isobject": "^3.0.2", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "lodash.omit": "^4.5.0", "lodash.pick": "^4.4.0", "lodash.trim": "^4.5.1", "serialize-error": "^5.0.0", "sift": "^9.0.4" }, "devDependencies": { "@feathersjs/authentication-client": "^4.5.4", "@feathersjs/authentication-jwt": "^2.0.10", "@feathersjs/client": "^4.5.4", "@feathersjs/feathers": "^4.5.3", "@feathersjs/rest-client": "^4.5.4", "@feathersjs/socketio-client": "^4.5.4", "@types/chai": "^4.2.11", "@types/mocha": "^7.0.2", "@typescript-eslint/eslint-plugin": "^2.31.0", "@typescript-eslint/parser": "^2.31.0", "@vue/composition-api": "^1.2.4", "@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-typescript": "^5.0.2", "@vue/test-utils": "^1.0.2", "axios": "^0.21.1", "babel-cli": "^6.26.0", "babel-core": "^6.26.3", "babel-eslint": "^10.1.0", "babel-plugin-add-module-exports": "^1.0.2", "babel-preset-es2015": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "body-parser": "^1.19.0", "can-fixture-socket": "^2.0.3", "chai": "^4.2.0", "cross-env": "^7.0.2", "date-fns": "^2.13.0", "deep-object-diff": "^1.1.0", "eslint": "^6.8.0", "eslint-config-prettier": "^6.11.0", "eslint-plugin-prettier": "^3.1.3", "eslint-plugin-vue": "^6.2.2", "feathers-memory": "^4.1.0", "istanbul": "^1.1.0-alpha.1", "jsdom": "^16.2.2", "jsdom-global": "^3.0.2", "mocha": "^7.1.2", "omit-deep-lodash": "^1.1.4", "prettier": "^2.0.5", "shx": "^0.3.2", "socket.io-client": "^2.3.0", "standard": "^14.3.3", "steal": "^2.2.4", "steal-mocha": "^2.0.1", "steal-typescript": "^0.5.0", "testee": "^0.9.1", "ts-node": "^8.10.1", "typescript": "^3.8.3", "vue": "^2.6.11", "vue-server-renderer": "^2.6.11", "vue-template-compiler": "^2.6.11", "vuepress": "^1.4.1", "vuepress-theme-default-prefers-color-scheme": "^1.0.7", "vuex": "^3.3.0" } } ================================================ FILE: src/FeathersVuexCount.ts ================================================ import { randomString } from './utils' export default { props: { service: { type: String, required: true }, params: { type: Object, default: () => { return { query: {} } } }, queryWhen: { type: [Boolean, Function], default: true }, // If separate params are desired to fetch data, use fetchParams // The watchers will automatically be updated, so you don't have to write 'fetchParams.query.propName' fetchParams: { type: Object }, watch: { type: [String, Array], default: () => [] }, local: { type: Boolean, default: false } }, data: () => ({ isCountPending: false, serverTotal: null }), computed: { total() { if (!this.local) { return this.serverTotal } else { const { params, service, $store, temps } = this return params ? $store.getters[`${service}/count`](params) : 0 } }, scope() { const { total, isCountPending } = this return { total, isCountPending } } }, methods: { findData() { const params = this.fetchParams || this.params if ( typeof this.queryWhen === 'function' ? this.queryWhen(this.params) : this.queryWhen ) { this.isCountPending = true if (params) { return this.$store .dispatch(`${this.service}/count`, params) .then(response => { this.isCountPending = false this.serverTotal = response }) } } }, fetchData() { if (!this.local) { if (this.params) { return this.findData() } else { // TODO: access debug boolean from the store config, somehow. // eslint-disable-next-line no-console console.log( `No query and no id provided, so no data will be fetched.` ) } } } }, created() { if (!this.$FeathersVuex) { throw new Error( `You must first Vue.use the FeathersVuex plugin before using the 'FeathersVuexFind' component.` ) } if (!this.$store.state[this.service]) { throw new Error( `The '${this.service}' plugin not registered with feathers-vuex` ) } const watch = Array.isArray(this.watch) ? this.watch : [this.watch] if (this.fetchParams || this.params) { watch.forEach(prop => { if (typeof prop !== 'string') { throw new Error(`Values in the 'watch' array must be strings.`) } if (this.fetchParams) { if (prop.startsWith('params')) { prop = prop.replace('params', 'fetchParams') } } this.$watch(prop, this.fetchData) }) this.fetchData() } }, render() { return this.$scopedSlots.default(this.scope) } } ================================================ FILE: src/FeathersVuexFind.ts ================================================ import { randomString, getQueryInfo } from './utils' import _get from 'lodash/get' export default { props: { service: { type: String, required: true }, query: { type: Object, default: null }, queryWhen: { type: [Boolean, Function], default: true }, // If a separate query is desired to fetch data, use fetchQuery // The watchers will automatically be updated, so you don't have to write 'fetchQuery.propName' fetchQuery: { type: Object }, /** * Can be used in place of the `query` prop to provide more params. Only params.query is * passed to the getter. */ params: { type: Object, default: null }, /** * Can be used in place of the `fetchQuery` prop to provide more params. Only params.query is * passed to the getter. */ fetchParams: { type: Object, default: null }, watch: { type: [String, Array], default() { return [] } }, local: { type: Boolean, default: false }, editScope: { type: Function, default(scope) { return scope } }, qid: { type: String, default() { return randomString(10) } }, /** * Set `temps` to true to include temporary records from the store. */ temps: { type: Boolean, default: false } }, data: () => ({ isFindPending: false, queryId: null, pageId: null }), computed: { items() { let { query, service, $store, temps } = this let { params } = this query = query || {} params = params || { query, temps } return $store.getters[`${service}/find`](params).data }, pagination() { return this.$store.state[this.service].pagination[this.qid] }, queryInfo() { if (this.pagination == null || this.queryId == null) return {} return _get(this.pagination, this.queryId, {}) }, pageInfo() { if ( this.pagination == null || this.queryId == null || this.pageId == null ) return {} return _get(this.pagination, [this.queryId, this.pageId], {}) }, scope() { const { items, isFindPending, pagination, queryInfo, pageInfo } = this const defaultScope = { isFindPending, pagination, items, queryInfo, pageInfo } return this.editScope(defaultScope) || defaultScope } }, methods: { findData() { const query = this.fetchQuery || this.query let params = this.fetchParams || this.params if ( typeof this.queryWhen === 'function' ? this.queryWhen(this.params || this.query) : this.queryWhen ) { this.isFindPending = true if (params || query) { if (params) { params = Object.assign({}, params, { qid: this.qid || 'default' }) } else { params = { query, qid: this.qid || 'default' } } return this.$store .dispatch(`${this.service}/find`, params) .then(response => { this.isFindPending = false const { queryId, pageId } = getQueryInfo(params, response) this.queryId = queryId this.pageId = pageId }) } } }, fetchData() { if (!this.local) { if (this.params || this.query) { return this.findData() } else { // TODO: access debug boolean from the store config, somehow. // eslint-disable-next-line no-console console.log( `No query and no id provided, so no data will be fetched.` ) } } } }, created() { if (!this.$FeathersVuex) { throw new Error( `You must first Vue.use the FeathersVuex plugin before using the 'FeathersVuexFind' component.` ) } if (!this.$store.state[this.service]) { throw new Error( `The '${this.service}' plugin not registered with feathers-vuex` ) } const watch = Array.isArray(this.watch) ? this.watch : [this.watch] if (this.fetchQuery || this.query || this.params) { watch.forEach(prop => { if (typeof prop !== 'string') { throw new Error(`Values in the 'watch' array must be strings.`) } if (this.fetchQuery) { if (prop.startsWith('query')) { prop = prop.replace('query', 'fetchQuery') } } if (this.fetchParams) { if (prop.startsWith('params')) { prop = prop.replace('params', 'fetchParams') } } this.$watch(prop, this.fetchData) }) this.fetchData() } }, render() { return this.$scopedSlots.default(this.scope) } } ================================================ FILE: src/FeathersVuexFormWrapper.ts ================================================ export default { name: 'FeathersVuexFormWrapper', model: { prop: 'item', event: 'update:item' }, props: { item: { type: Object, required: true }, /** * By default, when you call the `save` method, the cloned data will be * committed to the store BEFORE saving tot he API server. Set * `:eager="false"` to only update the store with the API server response. */ eager: { type: Boolean, default: true }, // Set to false to prevent re-cloning if the object updates. watch: { type: Boolean, default: true } }, data: () => ({ clone: null, isDirty: false }), computed: { isNew() { return (this.item && this.item.__isTemp) || false } }, watch: { item: { handler: 'setup', immediate: true, deep: true } }, methods: { setup() { if (this.item) { this.isDirty = false // Unwatch the clone to prevent running watchers during reclone if (this.unwatchClone) { this.unwatchClone() } this.clone = this.item.clone() // Watch the new clone. this.unwatchClone = this.$watch('clone', { handler: 'markAsDirty', deep: true }) } }, save(params) { if (this.eager) { this.clone.commit() } return this.clone.save(params).then(response => { this.$emit('saved', response) if (this.isNew) { this.$emit('saved-new', response) } return response }) }, reset() { this.clone.reset() this.isDirty = false this.$emit('reset', this.item) }, async remove() { await this.item.remove() this.$emit('removed', this.item) return this.item }, markAsDirty() { if (!this.isDirty) { this.isDirty = true } } }, render() { const { clone, save, reset, remove, isDirty, isNew } = this return this.$scopedSlots.default({ clone, save, reset, remove, isDirty, isNew }) } } ================================================ FILE: src/FeathersVuexGet.ts ================================================ /* eslint-disable @typescript-eslint/explicit-function-return-type */ export default { props: { /** * The path of the service from which to pull records. */ service: { type: String, required: true }, /** * Must match the `serverAlias` that was provided in the service's configuration. */ serverAlias: { type: String, default: 'api' }, /** * By default, `query` is used to get data from the Vuex store AND the API request. * If you specify a `fetchQuery`, then `query` will only be used for the Vuex store. */ query: { type: Object, default: null }, /** * If a separate query is desired to fetch data, use fetchQuery * The watchers are automatically updated, so you don't have to write 'fetchQuery.propName' */ fetchQuery: { type: Object }, /** * Can be used in place of the `query` prop to provide more params. Only params.query is * passed to the getter. */ params: { type: Object, default: null }, /** * Can be used in place of the `fetchQuery` prop to provide more params. Only params.query is * passed to the getter. */ fetchParams: { type: Object, default: null }, /** * When `queryWhen` evaluates to false, no API request will be made. */ queryWhen: { type: [Boolean, Function], default: true }, // For get requests id: { type: [Number, String], default: null }, /** * Specify which properties in the query to watch and re-trigger API requests. */ watch: { type: [String, Array], default() { return [] } }, /** * Set `local` to true to only requests from the Vuex data store and not make API requests. */ local: { type: Boolean, default: false }, /** * This function is called by the getter and allows you to intercept the `item` in the * response to pass it into the parent component's scope. It's a dirty little cheater * function (because it's called from a getter), but it actually works well ;) */ editScope: { type: Function, default(scope) { return scope } } }, data: () => ({ isFindPending: false, isGetPending: false }), computed: { item() { const getArgs = this.getArgs(this.query) if (this.id) { if (getArgs.length === 1) { return this.$store.getters[`${this.service}/get`](this.id) || null } else { const args = [this.id] const query = getArgs[1].query if (query) { args.push(query) } return this.$store.getters[`${this.service}/get`](args) || null } } else { return null } }, scope() { const { item, isGetPending } = this const defaultScope = { item, isGetPending } return this.editScope(defaultScope) || defaultScope } }, methods: { getArgs(queryToUse) { const query = queryToUse || this.fetchQuery || this.query const params = this.fetchParams || this.params const getArgs = [this.id] if (params) { getArgs.push(params) } else if (query && Object.keys(query).length > 0) { getArgs.push({ query }) } return getArgs }, getData() { const getArgs = this.getArgs() if ( typeof this.queryWhen === 'function' ? this.queryWhen(...getArgs) : this.queryWhen ) { this.isGetPending = true if (this.id) { return this.$store .dispatch( `${this.service}/get`, getArgs.length === 1 ? this.id : getArgs ) .then(response => { this.isGetPending = false return response }) } } }, fetchData() { if (this.local || this.id === 'new') { return } else if ( this.fetchQuery || this.query || this.params || (this.id !== null && this.id !== undefined) ) { return this.getData() } else { // eslint-disable-next-line no-console console.log(`No query and no id provided, so no data will be fetched.`) } } }, created() { if (!this.$FeathersVuex) { throw new Error( `You must first Vue.use the FeathersVuex plugin before using the 'FeathersVuexGet' component.` ) } if (!this.$store.state[this.service]) { throw new Error( `The '${this.service}' plugin is not registered with feathers-vuex` ) } const watch = Array.isArray(this.watch) ? this.watch : [this.watch] if ( this.fetchQuery || this.query || this.params || (this.id !== null && this.id !== undefined) ) { watch.forEach(prop => { if (typeof prop !== 'string') { throw new Error(`Values in the 'watch' array must be strings.`) } if (this.fetchQuery) { if (prop.startsWith('query')) { prop.replace('query', 'fetchQuery') } } this.$watch(prop, this.fetchData) }) this.fetchData() } }, render() { return this.$scopedSlots.default(this.scope) } } ================================================ FILE: src/FeathersVuexInputWrapper.ts ================================================ import _debounce from 'lodash/debounce' export default { name: 'FeathersVuexInputWrapper', props: { item: { type: Object, required: true }, prop: { type: String, required: true }, debounce: { type: Number, default: 0 } }, data: () => ({ clone: null }), computed: { current() { return this.clone || this.item } }, watch: { debounce: { handler(wait) { this.debouncedHandler = _debounce(this.handler, wait) }, immediate: true } }, methods: { createClone(e) { this.clone = this.item.clone() }, cleanup() { this.$nextTick(() => { this.clone = null }) }, handler(e, callback) { if (!this.clone) { this.createClone() } const maybePromise = callback({ event: e, clone: this.clone, prop: this.prop, data: { [this.prop]: this.clone[this.prop] } }) if (maybePromise && maybePromise.then) { maybePromise.then(this.cleanup) } else { this.cleanup() } } }, render() { const { current, prop, createClone } = this const handler = this.debounce ? this.debouncedHandler : this.handler return this.$scopedSlots.default({ current, prop, createClone, handler }) } } ================================================ FILE: src/FeathersVuexPagination.ts ================================================ import { h, computed, watch } from '@vue/composition-api' export default { name: 'FeathersVuexPagination', props: { /** * An object containing { $limit, and $skip } */ value: { type: Object, // eslint-disable-next-line @typescript-eslint/explicit-function-return-type default: () => null }, /** * The `latestQuery` object from the useFind data */ latestQuery: { type: Object, // eslint-disable-next-line @typescript-eslint/explicit-function-return-type default: () => null } }, // eslint-disable-next-line setup(props, context) { /** * The number of pages available based on the results returned in the latestQuery prop. */ const pageCount = computed(() => { const q = props.latestQuery if (q && q.response) { return Math.ceil(q.response.total / props.value.$limit) } else { return 1 } }) /** * The `currentPage` is calculated based on the $limit and $skip values provided in * the v-model object. * * Setting `currentPage` to a new numeric value will emit the appropriate values out * the v-model. (using the default `input` event) */ const currentPage = computed({ set(pageNumber: number) { if (pageNumber < 1) { pageNumber = 1 } else if (pageNumber > pageCount.value) { pageNumber = pageCount.value } const $limit = props.value.$limit const $skip = $limit * (pageNumber - 1) context.emit('input', { $limit, $skip }) }, get() { const params = props.value if (params) { return pageCount.value === 0 ? 0 : params.$skip / params.$limit + 1 } else { return 1 } } }) watch( () => pageCount.value, () => { const lq = props.latestQuery if (lq && lq.response && currentPage.value > pageCount.value) { currentPage.value = pageCount.value } } ) const canPrev = computed(() => { return currentPage.value - 1 > 0 }) const canNext = computed(() => { return currentPage.value < pageCount.value }) function toStart(): void { currentPage.value = 1 } function toEnd(): void { currentPage.value = pageCount.value } function toPage(pageNumber): void { currentPage.value = pageNumber } function next(): void { currentPage.value++ } function prev(): void { currentPage.value-- } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type return () => { if (context.slots.default) { return context.slots.default({ currentPage: currentPage.value, pageCount: pageCount.value, canPrev: canPrev.value, canNext: canNext.value, toStart, toEnd, toPage, prev, next }) } else { return h('div', {}, [ h('p', `FeathersVuexPagination uses the default slot:`), h('p', `#default="{ currentPage, pageCount }"`) ]) } } } } ================================================ FILE: src/auth-module/auth-module.actions.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import fastCopy from 'fast-copy' import { globalModels as models } from '../service-module/global-models' import { getNameFromPath } from '../utils' export default function makeAuthActions(feathersClient) { return { authenticate(store, dataOrArray) { const { commit, state, dispatch } = store const [data, params] = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray] commit('setAuthenticatePending') if (state.errorOnAuthenticate) { commit('clearAuthenticateError') } return feathersClient .authenticate(data, params) .then(response => { return dispatch('responseHandler', response) }) .catch(error => { commit('setAuthenticateError', error) commit('unsetAuthenticatePending') return Promise.reject(error) }) }, responseHandler({ commit, state, dispatch }, response) { if (response.accessToken) { commit('setAccessToken', response.accessToken) commit('setPayload', response) // Handle when user is returned in the authenticate response let user = response[state.responseEntityField] if (user) { if (state.serverAlias && state.userService) { const Model = Object.keys(models[state.serverAlias]) .map(modelName => models[state.serverAlias][modelName]) .find(model => getNameFromPath(model.servicePath) === getNameFromPath(state.userService)) if (Model) { // Copy user object to avoid setupInstance modifying payload state user = new Model(fastCopy(user)) } } commit('setUser', user) commit('unsetAuthenticatePending') } else if ( state.userService && response.hasOwnProperty(state.entityIdField) ) { return dispatch( 'populateUser', response[state.entityIdField] ).then(() => { commit('unsetAuthenticatePending') return response }) } return response // If there was not an accessToken in the response, allow the response to pass through to handle two-factor-auth } else { return response } }, populateUser({ commit, state, dispatch }, userId) { return dispatch(`${state.userService}/get`, userId, { root: true }).then( user => { commit('setUser', user) return user } ) }, logout({ commit }) { commit('setLogoutPending') return feathersClient .logout() .then(response => { commit('logout') commit('unsetLogoutPending') return response }) .catch(error => { return Promise.reject(error) }) } } } ================================================ FILE: src/auth-module/auth-module.getters.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ export default function makeAuthGetters({ userService }) { const getters = {} if (userService) { Object.assign(getters, { // A reactive user object user(state, getters, rootState) { if (!state.user) { return null } const { idField } = rootState[userService] const userId = state.user[idField] return rootState[userService].keyedById[userId] || null }, isAuthenticated(state, getters) { return !!getters.user } }) } return getters } ================================================ FILE: src/auth-module/auth-module.mutations.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { serializeError } from 'serialize-error' export default function makeAuthMutations() { return { setAccessToken(state, payload) { state.accessToken = payload }, setPayload(state, payload) { state.payload = payload }, setUser(state, payload) { state.user = payload }, setAuthenticatePending(state) { state.isAuthenticatePending = true }, unsetAuthenticatePending(state) { state.isAuthenticatePending = false }, setLogoutPending(state) { state.isLogoutPending = true }, unsetLogoutPending(state) { state.isLogoutPending = false }, setAuthenticateError(state, error) { state.errorOnAuthenticate = Object.assign({}, serializeError(error)) }, clearAuthenticateError(state) { state.errorOnAuthenticate = null }, setLogoutError(state, error) { state.errorOnLogout = Object.assign({}, serializeError(error)) }, clearLogoutError(state) { state.errorOnLogout = null }, logout(state) { state.payload = null state.accessToken = null if (state.user) { state.user = null } } } } ================================================ FILE: src/auth-module/auth-module.state.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { AuthState } from './types' export default function setupAuthState({ userService, serverAlias, responseEntityField = 'user', entityIdField = 'userId' }) { const state: AuthState = { accessToken: null, // The JWT payload: null, // The JWT payload entityIdField, responseEntityField, isAuthenticatePending: false, isLogoutPending: false, errorOnAuthenticate: null, errorOnLogout: null, user: null, // For a reactive user object, use the `user` getter. userService: null, serverAlias } // If a userService string was passed, add a user attribute if (userService) { Object.assign(state, { userService }) } return state } ================================================ FILE: src/auth-module/make-auth-plugin.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { FeathersVuexOptions } from '../service-module/types' import setupState from './auth-module.state' import setupGetters from './auth-module.getters' import setupMutations from './auth-module.mutations' import setupActions from './auth-module.actions' const defaults = { namespace: 'auth', userService: '', // Set this to automatically populate the user (using an additional request) on login success. serverAlias: 'api', debug: false, state: {}, // for custom state getters: {}, // for custom getters mutations: {}, // for custom mutations actions: {} // for custom actions } export default function authPluginInit( feathersClient, globalOptions: FeathersVuexOptions ) { if (!feathersClient || !feathersClient.service) { throw new Error('You must pass a Feathers Client instance to feathers-vuex') } return function makeAuthPlugin(options) { options = Object.assign( {}, defaults, { serverAlias: globalOptions.serverAlias }, options ) if (!feathersClient.authenticate) { throw new Error( 'You must register the @feathersjs/authentication-client plugin before using the feathers-vuex auth module' ) } if (options.debug && options.userService && !options.serverAlias) { console.warn( 'A userService was provided, but no serverAlias was provided. To make sure the user record is an instance of the User model, a serverAlias must be provided.' ) } const defaultState = setupState(options) const defaultGetters = setupGetters(options) const defaultMutations = setupMutations() const defaultActions = setupActions(feathersClient) return function setupStore(store) { const { namespace } = options store.registerModule(namespace, { namespaced: true, state: Object.assign({}, defaultState, options.state), getters: Object.assign({}, defaultGetters, options.getters), mutations: Object.assign({}, defaultMutations, options.mutations), actions: Object.assign({}, defaultActions, options.actions) }) } } } ================================================ FILE: src/auth-module/types.ts ================================================ export interface AuthState { accessToken: string payload: {} entityIdField: string responseEntityField: string isAuthenticatePending: boolean isLogoutPending: boolean errorOnAuthenticate: Error errorOnLogout: Error user: {} userService: string serverAlias: string } ================================================ FILE: src/index.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import FeathersVuexFind from './FeathersVuexFind' import FeathersVuexGet from './FeathersVuexGet' import FeathersVuexFormWrapper from './FeathersVuexFormWrapper' import FeathersVuexInputWrapper from './FeathersVuexInputWrapper' import FeathersVuexPagination from './FeathersVuexPagination' import makeFindMixin from './make-find-mixin' import makeGetMixin from './make-get-mixin' import { globalModels as models } from './service-module/global-models' import { clients, addClient } from './service-module/global-clients' import makeBaseModel from './service-module/make-base-model' import prepareMakeServicePlugin from './service-module/make-service-plugin' import prepareMakeAuthPlugin from './auth-module/make-auth-plugin' import useFind from './useFind' import useGet from './useGet' import { FeathersVuexOptions, HandleEvents, Model, ModelStatic, ModelSetupContext, Id, FeathersVuexStoreState, FeathersVuexGlobalModels, GlobalModels } from './service-module/types' import { initAuth, hydrateApi } from './utils' import { FeathersVuex } from './vue-plugin/vue-plugin' import { ServiceState } from './service-module/service-module.state' import { AuthState } from './auth-module/types' const events = ['created', 'patched', 'updated', 'removed'] const defaults: FeathersVuexOptions = { autoRemove: false, // Automatically remove records missing from responses (only use with feathers-rest) addOnUpsert: false, // Add new records pushed by 'updated/patched' socketio events into store, instead of discarding them enableEvents: true, // Listens to socket.io events when available idField: 'id', // The field in each record that will contain the id tempIdField: '__id', debug: false, // Set to true to enable logging messages. keepCopiesInStore: false, // Set to true to store cloned copies in the store instead of on the Model. nameStyle: 'short', // Determines the source of the module name. 'short', 'path', or 'explicit' paramsForServer: ['$populateParams'], // Custom query operators that are ignored in the find getter, but will pass through to the server. $populateParams is for https://feathers-graph-populate.netlify.app/ preferUpdate: false, // When true, calling model.save() will do an update instead of a patch. replaceItems: false, // Instad of merging in changes in the store, replace the entire record. serverAlias: 'api', handleEvents: {} as HandleEvents, skipRequestIfExists: false, // For get action, if the record already exists in store, skip the remote request whitelist: [] // Custom query operators that will be allowed in the find getter. } export default function feathersVuex(feathers, options: FeathersVuexOptions) { if (!feathers || !feathers.service) { throw new Error( 'The first argument to feathersVuex must be a feathers client.' ) } // Setup the event handlers. By default they just return the value of `options.enableEvents` defaults.handleEvents = events.reduce((obj, eventName) => { obj[eventName] = () => options.enableEvents || true return obj }, {} as HandleEvents) options = Object.assign({}, defaults, options) if (!options.serverAlias) { throw new Error( `You must provide a 'serverAlias' in the options to feathersVuex` ) } addClient({ client: feathers, serverAlias: options.serverAlias }) const BaseModel = makeBaseModel(options) const makeServicePlugin = prepareMakeServicePlugin(options) const makeAuthPlugin = prepareMakeAuthPlugin(feathers, options) return { makeServicePlugin, BaseModel, makeAuthPlugin, FeathersVuex, models: models as GlobalModels, clients } } export { initAuth, hydrateApi, FeathersVuexFind, FeathersVuexGet, FeathersVuexFormWrapper, FeathersVuexInputWrapper, FeathersVuexPagination, FeathersVuex, makeFindMixin, makeGetMixin, models, clients, useFind, useGet, AuthState, Id, Model, ModelStatic, ModelSetupContext, ServiceState, FeathersVuexGlobalModels, FeathersVuexStoreState } ================================================ FILE: src/make-find-mixin.ts ================================================ /* eslint no-console: 0, @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import debounce from 'lodash/debounce' import _get from 'lodash/get' import { getItemsFromQueryInfo, getQueryInfo, getServiceCapitalization, getServicePrefix } from './utils' export default function makeFindMixin(options) { const { service, params, fetchParams, queryWhen = () => true, local = false, qid = 'default', items, debug } = options let { name, watch = [] } = options if (typeof watch === 'string') { watch = [watch] } else if (typeof watch === 'boolean' && watch) { watch = ['params'] } if ( !service || (typeof service !== 'string' && typeof service !== 'function') ) { throw new Error( `The 'service' option is required in the FeathersVuex make-find-mixin and must be a string.` ) } if (typeof service === 'function' && !name) { name = 'service' } const nameToUse = (name || service).replace(/-/g, '_') const prefix = getServicePrefix(nameToUse) const capitalized = getServiceCapitalization(nameToUse) const SERVICE_NAME = `${prefix}ServiceName` let ITEMS = items || prefix if (typeof service === 'function' && name === 'service' && !items) { ITEMS = 'items' } const ITEMS_FETCHED = `${ITEMS}Fetched` const IS_FIND_PENDING = `isFind${capitalized}Pending` const PARAMS = `${prefix}Params` const FETCH_PARAMS = `${prefix}FetchParams` const WATCH = `${prefix}Watch` const QUERY_WHEN = `${prefix}QueryWhen` const ERROR = `${prefix}Error` const FIND_ACTION = `find${capitalized}` const FIND_GETTER = `find${capitalized}InStore` const HAVE_ITEMS_BEEN_REQUESTED_ONCE = `have${capitalized}BeenRequestedOnce` const HAVE_ITEMS_LOADED_ONCE = `have${capitalized}LoadedOnce` const PAGINATION = `${prefix}PaginationData` const MOST_RECENT_QUERY = `${prefix}LatestQuery` const LOCAL = `${prefix}Local` const QID = `${prefix}Qid` const data = { [IS_FIND_PENDING]: false, [HAVE_ITEMS_BEEN_REQUESTED_ONCE]: false, [HAVE_ITEMS_LOADED_ONCE]: false, [WATCH]: watch, [QID]: qid, [MOST_RECENT_QUERY]: null, [ERROR]: null } // Should only be used with actual fetching API calls. const getParams = ({ providedParams, params, fetchParams }) => { if (providedParams) { return providedParams } else { // Returning null fetchParams allows the query to be skipped. return fetchParams || fetchParams === null ? fetchParams : params } } const mixin = { data() { return data }, computed: { [PAGINATION]() { return this.$store.state[this[SERVICE_NAME]].pagination }, [ITEMS]() { const serviceName = this[SERVICE_NAME] const serviceState = this.$store.state[serviceName] // If both queries are provided, we're not using fall-through pagination. if ( (this[FETCH_PARAMS] && this[PARAMS]) || (this[PARAMS] && !this[PARAMS].paginate) ) { return this.$store.getters[`${serviceName}/find`](this[PARAMS]).data } // User can pass `paginate: true` to force pagination. const params = this[PARAMS] // Check for pagination data for this query. if (params) { const { defaultSkip, defaultLimit } = serviceState.pagination const skip = params.query.$skip || defaultSkip const limit = params.query.$limit || defaultLimit const pagination = this[PAGINATION][params.qid || this[QID]] || {} const response = skip != null && limit != null ? { limit, skip } : {} const queryInfo = getQueryInfo(params, response) const items = getItemsFromQueryInfo( pagination, queryInfo, serviceState.keyedById ) if (items && items.length) { return items } } return [] }, // Queries the Vuex store with the exact same query that was sent to the API server. [ITEMS_FETCHED]() { if (this[FETCH_PARAMS]) { return this[FIND_GETTER](this[FETCH_PARAMS]).data } else { return this[ITEMS] } }, // Exposes `findItemsInStore [FIND_GETTER]() { return params => { const serviceName = this[SERVICE_NAME] return this.$store.getters[`${serviceName}/find`](params) } } }, methods: { [`${FIND_ACTION}DebouncedProxy`](params) { const paramsToUse = getParams({ providedParams: params, params: this[PARAMS], fetchParams: this[FETCH_PARAMS] }) if (paramsToUse && paramsToUse.debounce) { const cachedDebounceFunction = this[`${FIND_ACTION}Debounced`] const mostRecentTime = this[`${FIND_ACTION}MostRecentDebounceTime`] if ( !cachedDebounceFunction || mostRecentTime != paramsToUse.debounce ) { this[`${FIND_ACTION}MostRecentDebounceTime`] = paramsToUse.debounce this[`${FIND_ACTION}Debounced`] = debounce( this[FIND_ACTION], paramsToUse.debounce ) } return this[`${FIND_ACTION}Debounced`](paramsToUse) } else { return this[FIND_ACTION](paramsToUse) } }, [FIND_ACTION](params) { const serviceName = this[SERVICE_NAME] const paramsToUse = getParams({ providedParams: params, params: this[PARAMS], fetchParams: this[FETCH_PARAMS] }) const shouldExecuteQuery = typeof this[QUERY_WHEN] === 'function' ? this[QUERY_WHEN](paramsToUse) : this[QUERY_WHEN] if (shouldExecuteQuery) { if (paramsToUse) { // Set the qid. paramsToUse.query = paramsToUse.query || {} paramsToUse.qid = paramsToUse.qid || this[QID] this[QID] = paramsToUse.qid this[IS_FIND_PENDING] = true this[HAVE_ITEMS_BEEN_REQUESTED_ONCE] = true return this.$store .dispatch(`${serviceName}/find`, paramsToUse) .then(response => { // To prevent thrashing, only clear ERROR on response, not on initial request. this[ERROR] = null this[HAVE_ITEMS_LOADED_ONCE] = true const queryInfo = getQueryInfo(paramsToUse, response) queryInfo.response = response queryInfo.isOutdated = false this[MOST_RECENT_QUERY] = queryInfo this[IS_FIND_PENDING] = false return response }) .catch(error => { this[ERROR] = error return error }) } } else { if (this[MOST_RECENT_QUERY]) { this[MOST_RECENT_QUERY].isOutdated = true } } }, getPaginationForQuery(params = {}) { const pagination = this[PAGINATION] const { qid, queryId, pageId } = getQueryInfo(params) const queryInfo = _get(pagination, [qid, queryId], {}) const pageInfo = _get(pagination, [qid, queryId, pageId], {}) return { queryInfo, pageInfo } } }, // add the created hook only if the local option is falsy ...(!local && { created() { if (debug) { console.log( `running 'created' hook in makeFindMixin for service "${service}" (using name ${nameToUse}")` ) console.log(PARAMS, this[PARAMS]) console.log(FETCH_PARAMS, this[FETCH_PARAMS]) } const pType = Object.getPrototypeOf(this) if ( pType.hasOwnProperty(PARAMS) || pType.hasOwnProperty(FETCH_PARAMS) ) { watch.forEach(prop => { if (typeof prop !== 'string') { throw new Error(`Values in the 'watch' array must be strings.`) } prop = prop.replace('params', PARAMS) if (pType.hasOwnProperty(FETCH_PARAMS)) { if (prop.startsWith(PARAMS)) { prop = prop.replace(PARAMS, FETCH_PARAMS) } } this.$watch(prop, function() { // If the request is going to be debounced, set IS_FIND_PENDING to true. // Without this, there's not a way to show a loading indicator during the debounce timeout. const paramsToUse = getParams({ providedParams: null, params: this[PARAMS], fetchParams: this[FETCH_PARAMS] }) if (paramsToUse && paramsToUse.debounce) { this[IS_FIND_PENDING] = true } return this[`${FIND_ACTION}DebouncedProxy`]() }) }) return this[FIND_ACTION]() } else { // TODO: Add this message to the logging: // "Pass { local: true } to disable this warning and only do local queries." console.log( `No "${PARAMS}" or "${FETCH_PARAMS}" attribute was found in the makeFindMixin for the "${service}" service (using name "${nameToUse}"). No queries will be made.` ) } } }) } function hasSomeAttribute(vm, ...attributes) { return attributes.some(a => { return vm.hasOwnProperty(a) || Object.getPrototypeOf(vm).hasOwnProperty(a) }) } function setupAttribute( NAME, value, computedOrMethods = 'computed', returnTheValue = false ) { if (typeof value === 'boolean') { data[NAME] = !!value } else if (typeof value === 'string') { mixin.computed[NAME] = function() { // If the specified computed prop wasn't found, display an error. if (!returnTheValue) { if (!hasSomeAttribute(this, value, NAME)) { throw new Error( `Value for ${NAME} was not found on the component at '${value}'.` ) } } return returnTheValue ? value : this[value] } } else if (typeof value === 'function') { mixin[computedOrMethods][NAME] = value } } setupAttribute(SERVICE_NAME, service, 'computed', true) setupAttribute(PARAMS, params) setupAttribute(FETCH_PARAMS, fetchParams) setupAttribute(QUERY_WHEN, queryWhen, 'computed') setupAttribute(LOCAL, local) return mixin } ================================================ FILE: src/make-get-mixin.ts ================================================ /* eslint no-console: 0, @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import inflection from 'inflection' export default function makeFindMixin(options) { const { service, params, fetchParams, queryWhen, id, local = false, qid = 'default', item, debug } = options let { name, watch = [] } = options if (typeof watch === 'string') { watch = [watch] } else if (typeof watch === 'boolean' && watch) { watch = ['query'] } if ( !service || (typeof service !== 'string' && typeof service !== 'function') ) { throw new Error( `The 'service' option is required in the FeathersVuex make-find-mixin and must be a string.` ) } if (typeof service === 'function' && !name) { name = 'service' } const nameToUse = (name || service).replace(/-/g, '_') const singularized = inflection.singularize(nameToUse) const prefix = inflection.camelize(singularized, true) const capitalized = prefix.charAt(0).toUpperCase() + prefix.slice(1) const SERVICE_NAME = `${prefix}ServiceName` let ITEM = item || prefix if (typeof service === 'function' && name === 'service' && !item) { ITEM = 'item' } const IS_GET_PENDING = `isGet${capitalized}Pending` const PARAMS = `${prefix}Params` const FETCH_PARAMS = `${prefix}FetchParams` const WATCH = `${prefix}Watch` const QUERY_WHEN = `${prefix}QueryWhen` const ERROR = `${prefix}Error` const GET_ACTION = `get${capitalized}` const GET_GETTER = `get${capitalized}FromStore` const HAS_ITEM_BEEN_REQUESTED_ONCE = `has${capitalized}BeenRequestedOnce` const HAS_ITEM_LOADED_ONCE = `has${capitalized}LoadedOnce` const LOCAL = `${prefix}Local` const QID = `${prefix}Qid` const ID = `${prefix}Id` const data = { [IS_GET_PENDING]: false, [HAS_ITEM_BEEN_REQUESTED_ONCE]: false, [HAS_ITEM_LOADED_ONCE]: false, [WATCH]: watch, [QID]: qid, [ERROR]: null } const mixin = { data() { return data }, computed: { [ITEM]() { return this[ID] ? this.$store.getters[`${this[SERVICE_NAME]}/get`](this[ID]) : null }, [QUERY_WHEN]() { return true }, // Exposes `getFromStore` [GET_GETTER]() { return id => { const serviceName = this[SERVICE_NAME] return this.$store.getters[`${serviceName}/get`](id) } } }, methods: { [GET_ACTION](id, params) { const paramsToUse = params || this[FETCH_PARAMS] || this[PARAMS] const idToUse = id || this[ID] if (this[QUERY_WHEN]) { this[IS_GET_PENDING] = true this[HAS_ITEM_BEEN_REQUESTED_ONCE] = true if (idToUse != null) { return this.$store .dispatch(`${this[SERVICE_NAME]}/get`, [idToUse, paramsToUse]) .then(response => { // To prevent thrashing, only clear ERROR on response, not on initial request. this[ERROR] = null this[HAS_ITEM_LOADED_ONCE] = true this[IS_GET_PENDING] = false return response }) .catch(error => { this[ERROR] = error return error }) } } } }, // add the created lifecycle hook only if local option is falsy ...(!local && { created() { if (debug) { console.log( `running 'created' hook in makeGetMixin for service "${service}" (using name ${nameToUse}")` ) console.log(ID, this[ID]) console.log(PARAMS, this[PARAMS]) console.log(FETCH_PARAMS, this[FETCH_PARAMS]) } const pType = Object.getPrototypeOf(this) if ( this.hasOwnProperty(ID) || pType.hasOwnProperty(ID) || pType.hasOwnProperty(PARAMS) || pType.hasOwnProperty(FETCH_PARAMS) ) { if (!watch.includes(ID)) { watch.push(ID) } watch.forEach(prop => { if (typeof prop !== 'string') { throw new Error(`Values in the 'watch' array must be strings.`) } prop = prop.replace('query', PARAMS) if (pType.hasOwnProperty(FETCH_PARAMS)) { if (prop.startsWith(PARAMS)) { prop.replace(PARAMS, FETCH_PARAMS) } } this.$watch(prop, function() { return this[GET_ACTION]() }) }) return this[GET_ACTION]() } else { console.log( `No "${ID}", "${PARAMS}" or "${FETCH_PARAMS}" attribute was found in the makeGetMixin for the "${service}" service (using name "${nameToUse}"). No queries will be made.` ) } } }) } function hasSomeAttribute(vm, ...attributes) { return attributes.some(a => { return vm.hasOwnProperty(a) || Object.getPrototypeOf(vm).hasOwnProperty(a) }) } function setupAttribute( NAME, value, computedOrMethods = 'computed', returnTheValue = false ) { if (typeof value === 'boolean') { data[NAME] = !!value } else if (typeof value === 'string') { mixin.computed[NAME] = function() { // If the specified computed prop wasn't found, display an error. if (!returnTheValue) { if (!hasSomeAttribute(this, value, NAME)) { throw new Error( `Value for ${NAME} was not found on the component at '${value}'.` ) } } return returnTheValue ? value : this[value] } } else if (typeof value === 'function') { mixin[computedOrMethods][NAME] = value } } setupAttribute(SERVICE_NAME, service, 'computed', true) setupAttribute(ID, id) setupAttribute(PARAMS, params) setupAttribute(FETCH_PARAMS, fetchParams) setupAttribute(QUERY_WHEN, queryWhen, 'computed') setupAttribute(LOCAL, local) return mixin } ================================================ FILE: src/service-module/global-clients.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import _get from 'lodash/get' /** * A global object that holds references to all Model Classes in the application. */ export const clients: { [k: string]: any } = { byAlias: {}, byHost: {} } /** * prepareAddModel wraps options in a closure around addModel * @param options */ export function addClient({ client, serverAlias }) { // Save reference to the clients by host uri, if it was available. let uri = '' if (client.io) { uri = _get(client, 'io.io.uri') } if (uri) { clients.byHost[uri] = client } // Save reference to clients by serverAlias. clients.byAlias[serverAlias] = client } export function clearClients() { function deleteKeys(path) { Object.keys(clients[path]).forEach(key => { delete clients[path][key] }) } deleteKeys('byAlias') deleteKeys('byHost') } ================================================ FILE: src/service-module/global-models.ts ================================================ /* eslint no-console: 0, @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { FeathersVuexOptions } from './types' /** * A global object that holds references to all Model Classes in the application. */ export const globalModels: { [k: string]: any } = {} /** * prepareAddModel wraps options in a closure around addModel * @param options */ export function prepareAddModel(options: FeathersVuexOptions) { const { serverAlias } = options return function addModel(Model) { globalModels[serverAlias] = globalModels[serverAlias] || { byServicePath: {} } const name = Model.modelName || Model.name if (globalModels[serverAlias][name] && options.debug) { // eslint-disable-next-line no-console console.error(`Overwriting Model: models[${serverAlias}][${name}].`) } globalModels[serverAlias][name] = Model globalModels[serverAlias].byServicePath[Model.servicePath] = Model } } export function clearModels() { Object.keys(globalModels).forEach(key => { const serverAliasObj = globalModels[key] Object.keys(serverAliasObj).forEach(key => { delete globalModels[key] }) delete globalModels[key] }) } ================================================ FILE: src/service-module/make-base-model.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { FeathersVuexOptions, Id, ModelInstanceOptions, Model, ModelStatic, GlobalModels, StoreState, AnyData, PatchParams } from './types' import { globalModels, prepareAddModel } from './global-models' import { mergeWithAccessors, checkNamespace, getId, Params } from '../utils' import _merge from 'lodash/merge' import _get from 'lodash/get' import { EventEmitter } from 'events' import { ModelSetupContext } from './types' import { Store } from 'vuex' import { GetterName } from './service-module.getters' const defaultOptions = { clone: false, commit: true, merge: true } /** Ensures value has EventEmitter instance props */ function assertIsEventEmitter(val: unknown): asserts val is EventEmitter { if ( !Object.keys(EventEmitter.prototype).every((eeKey) => Object.prototype.hasOwnProperty.call(val, eeKey) ) ) { throw new Error(`Expected EventEmitter, but got ${val}`) } } /** * * @param options */ export default function makeBaseModel(options: FeathersVuexOptions) { const addModel = prepareAddModel(options) const { serverAlias } = options // If this serverAlias already has a BaseModel, return it const ExistingBaseModel = _get(globalModels, [serverAlias, 'BaseModel']) if (ExistingBaseModel) { return ExistingBaseModel as ModelStatic } abstract class BaseModel implements Model { // Think of these as abstract static properties public static servicePath: string public static namespace: string public static keepCopiesInStore = options.keepCopiesInStore // eslint-disable-next-line public static instanceDefaults(data: AnyData, ctx: ModelSetupContext) { return data } // eslint-disable-next-line public static setupInstance(data: AnyData, ctx: ModelSetupContext) { return data } public static diffOnPatch(data: AnyData) { return data } // Monkey patched onto the Model class in `makeServicePlugin()` public static store: Store public static idField: string = options.idField public static tempIdField: string = options.tempIdField public static preferUpdate: boolean = options.preferUpdate public static serverAlias: string = options.serverAlias public static readonly models = globalModels as GlobalModels // Can access other Models here public static readonly copiesById: { [key: string]: Model | undefined [key: number]: Model | undefined } = {} public __id: string public __isClone: boolean public __isTemp: boolean public static merge = mergeWithAccessors public static modelName = 'BaseModel' public constructor(data: AnyData, options: ModelInstanceOptions) { // You have to pass at least an empty object to get a tempId. data = data || {} options = Object.assign({}, defaultOptions, options) const { store, keepCopiesInStore, copiesById: copiesByIdOnModel, models, instanceDefaults, idField, tempIdField, setupInstance, getFromStore, namespace, _commit } = this.constructor as typeof BaseModel const id = getId(data, idField) const hasValidId = id !== null && id !== undefined const tempId = data && data.hasOwnProperty(tempIdField) ? data[tempIdField] : undefined const hasValidTempId = tempId !== null && tempId !== undefined const copiesById = keepCopiesInStore ? store?.state[namespace].copiesById : copiesByIdOnModel if (store?.state?.[namespace]?.replaceItems !== true) { const existingItem = hasValidId && !options.clone ? getFromStore.call(this.constructor, id) : null // If it already exists, update the original and return if (existingItem) { data = setupInstance.call(this, data, { models, store }) || data _commit.call(this.constructor, 'mergeInstance', data) return existingItem } } // If cloning and a clone already exists, update and return the original clone. Only one clone is allowed. const existingClone = (hasValidId || hasValidTempId) && options.clone ? copiesById[id] || copiesById[tempId] : null if (existingClone) { // This must be done in a mutation to avoid Vuex errors. _commit.call(this.constructor, 'merge', { dest: existingClone, source: data }) return existingClone } // Mark as a clone if (options.clone) { Object.defineProperty(this, '__isClone', { value: true, enumerable: false }) } // Setup instanceDefaults if (instanceDefaults && typeof instanceDefaults === 'function') { const defaults = instanceDefaults.call(this, data, { models, store }) || data mergeWithAccessors(this, defaults) } // Handles Vue objects or regular ones. We can't simply assign or return // the data due to how Vue wraps everything into an accessor. if (options.merge !== false) { mergeWithAccessors( this, setupInstance.call(this, data, { models, store }) || data ) } // Add the item to the store if (!options.clone && options.commit !== false && store) { _commit.call(this.constructor, 'addItem', this) } return this } /** * Calls `getter`, passing this model's ID as the parameter * @param getter name of getter to call */ private getGetterWithId(getter: GetterName): unknown { const { _getters, idField, tempIdField } = this .constructor as typeof BaseModel const id = getId(this, idField) != null ? getId(this, idField) : this[tempIdField] return _getters.call(this.constructor, getter, id) } get isCreatePending(): boolean { return this.getGetterWithId('isCreatePendingById') as boolean } get isUpdatePending(): boolean { return this.getGetterWithId('isUpdatePendingById') as boolean } get isPatchPending(): boolean { return this.getGetterWithId('isPatchPendingById') as boolean } get isRemovePending(): boolean { return this.getGetterWithId('isRemovePendingById') as boolean } get isSavePending(): boolean { return this.getGetterWithId('isSavePendingById') as boolean } get isPending(): boolean { return this.getGetterWithId('isPendingById') as boolean } public static getId(record: Record): string { const { idField } = this.constructor as typeof BaseModel return getId(record, idField) } public static find(params?: Params) { return this._dispatch('find', params) } public static findInStore(params?: Params) { return this._getters('find', params) } public static count(params?: Params) { return this._dispatch('count', params) } public static countInStore(params?: Params) { return this._getters('count', params) } public static get(id: Id, params?: Params) { if (params) { return this._dispatch('get', [id, params]) } else { return this._dispatch('get', id) } } public static getFromStore(id: Id, params?: Params) { return this._getters('get', id, params) } /** * An alias for store.getters. Can only call function-based getters, since * it's meant for only `find` and `get`. * @param method the vuex getter name without the namespace * @param payload if provided, the getter will be called as a function */ public static _getters(name: GetterName, idOrParams?: any, params?: any) { const { namespace, store } = this if (checkNamespace(namespace, this, options.debug)) { if (!store.getters.hasOwnProperty(`${namespace}/${name}`)) { throw new Error(`Could not find getter named ${namespace}/${name}`) } return store.getters[`${namespace}/${name}`](idOrParams, params) } } /** * An alias for store.commit * @param method the vuex mutation name without the namespace * @param payload the payload for the mutation */ public static _commit(method: string, payload: any): void { const { namespace, store } = this if (checkNamespace(namespace, this, options.debug)) { store.commit(`${namespace}/${method}`, payload) } } /** * An alias for store.dispatch * @param method the vuex action name without the namespace * @param payload the payload for the action */ public static _dispatch(method: string, payload: any) { const { namespace, store } = this if (checkNamespace(namespace, this, options.debug)) { return store.dispatch(`${namespace}/${method}`, payload) } } /** * make the server side documents hydrated on client a FeathersVuexModel */ public static hydrateAll() { const { namespace, store } = this const state = store.state[namespace] const commit = store.commit // Replace each plain object with a model instance. Object.keys(state.keyedById).forEach((id) => { const record = state.keyedById[id] commit(`${namespace}/removeItem`, record) commit(`${namespace}/addItem`, record) }) } /** * clone the current record using the `createCopy` mutation */ public clone(data: AnyData): this { const { idField, tempIdField } = this.constructor as typeof BaseModel if (this.__isClone) { throw new Error('You cannot clone a copy') } const id = getId(this, idField) != null ? getId(this, idField) : this[tempIdField] return this._clone(id, data) } private _clone(id, data = {}) { const { store, namespace, _commit, _getters } = this .constructor as typeof BaseModel const { keepCopiesInStore } = store.state[namespace] _commit.call(this.constructor, `createCopy`, id) if (keepCopiesInStore) { return Object.assign( _getters.call(this.constructor, 'getCopyById', id), data ) } else { // const { copiesById } = this.constructor as typeof BaseModel return Object.assign( (this.constructor as typeof BaseModel).copiesById[id], data ) } } /** * Reset a clone to match the instance in the store. */ public reset(): this { const { idField, tempIdField, _commit } = this .constructor as typeof BaseModel if (this.__isClone) { const id = getId(this, idField) != null ? getId(this, idField) : this[tempIdField] _commit.call(this.constructor, 'resetCopy', id) return this } else { throw new Error('You cannot reset a non-copy') } } /** * Update a store instance to match a clone. */ public commit(): this { const { idField, tempIdField, _commit, _getters } = this .constructor as typeof BaseModel if (this.__isClone) { const id = getId(this, idField) != null ? getId(this, idField) : this[tempIdField] _commit.call(this.constructor, 'commitCopy', id) return _getters.call(this.constructor, 'get', id) } else { throw new Error('You cannot call commit on a non-copy') } } /** * A shortcut to either call create or patch/update * @param params */ public save(params?: Params): Promise { const { idField, preferUpdate } = this.constructor as typeof BaseModel const id = getId(this, idField) if (id != null) { return preferUpdate ? this.update(params) : this.patch(params) } else { return this.create(params) } } /** * Calls service create with the current instance data * @param params */ public create(params?: Params): Promise { const { _dispatch } = this.constructor as typeof BaseModel const data = Object.assign({}, this) if (data[options.idField] === null) { delete data[options.idField] } return _dispatch.call(this.constructor, 'create', [data, params]) } /** * Calls service patch with the current instance data * @param params */ public patch( params?: PatchParams ): Promise { const { idField, _dispatch } = this.constructor as typeof BaseModel const id = getId(this, idField) if (id == null) { const error = new Error( `Missing ${idField} property. You must create the data before you can patch with this data` ) return Promise.reject(error) } return _dispatch.call(this.constructor, 'patch', [id, this, params]) } /** * Calls service update with the current instance data * @param params */ public update(params?: Params): Promise { const { idField, _dispatch } = this.constructor as typeof BaseModel const id = getId(this, idField) if (id !== 0 && !id) { const error = new Error( `Missing ${idField} property. You must create the data before you can update with this data` ) return Promise.reject(error) } return _dispatch.call(this.constructor, 'update', [id, this, params]) } /** * Calls service remove with the current instance id * @param params */ public remove(params?: Params): Promise { const { idField, tempIdField, _dispatch, _commit } = this .constructor as typeof BaseModel const id = getId(this, idField) if (id != null) { if (params && params.eager) { _commit.call(this.constructor, 'removeItem', id) } return _dispatch.call(this.constructor, 'remove', [id, params]) } else { // is temp _commit.call(this.constructor, 'removeTemps', [this[tempIdField]]) _commit.call(this.constructor, 'clearCopy', [this[tempIdField]]) return Promise.resolve(this) } } public toJSON() { return _merge({}, this) } } for (const n in EventEmitter.prototype) { BaseModel[n] = EventEmitter.prototype[n] } addModel(BaseModel) const BaseModelEventEmitter = BaseModel assertIsEventEmitter(BaseModelEventEmitter) return BaseModelEventEmitter as ModelStatic } ================================================ FILE: src/service-module/make-service-module.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import _pick from 'lodash/pick' import _merge from 'lodash/merge' import makeDefaultState from './service-module.state' import makeGetters from './service-module.getters' import makeMutations from './service-module.mutations' import makeActions from './service-module.actions' import { Service } from '@feathersjs/feathers' import { MakeServicePluginOptions } from './types' import { Store } from 'vuex' export default function makeServiceModule( service: Service, options: MakeServicePluginOptions, store: Store ) { const defaults = { namespaced: true, state: makeDefaultState(options), getters: makeGetters(), mutations: makeMutations(), actions: makeActions({service, options}) } const fromOptions = _pick(options, [ 'state', 'getters', 'mutations', 'actions' ]) const merged = _merge({}, defaults, fromOptions) const extended = options.extend({ store, module: merged }) const finalModule = _merge({}, merged, extended) return finalModule } ================================================ FILE: src/service-module/make-service-plugin.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { FeathersVuexOptions, MakeServicePluginOptions, ServicePluginExtendOptions } from './types' import makeServiceModule from './make-service-module' import { globalModels, prepareAddModel } from './global-models' import enableServiceEvents from './service-module.events' import { makeNamespace, getServicePath, assignIfNotPresent } from '../utils' import _get from 'lodash/get' interface ServiceOptionsDefaults { servicePath: string namespace: string extend: ( options: ServicePluginExtendOptions ) => { state: any getters: any mutations: any actions: any } state: {} getters: {} mutations: {} actions: {} instanceDefaults: () => {} setupInstance: (instance: {}) => {} debounceEventsMaxWait: number } const defaults: ServiceOptionsDefaults = { namespace: '', // The namespace for the Vuex module. Will generally be derived from the service.path, service.name, when available. Otherwise, it must be provided here, explicitly. servicePath: '', extend: ({ module }) => module, // for custom plugin (replaces state, getters, mutations, and actions) state: {}, // for custom state getters: {}, // for custom getters mutations: {}, // for custom mutations actions: {}, // for custom actions instanceDefaults: () => ({}), // Default instanceDefaults returns an empty object setupInstance: instance => instance, // Default setupInstance returns the instance debounceEventsMaxWait: 1000 } const events = ['created', 'patched', 'updated', 'removed'] /** * prepare only wraps the makeServicePlugin to provide the globalOptions. * @param globalOptions */ export default function prepareMakeServicePlugin( globalOptions: FeathersVuexOptions ) { const addModel = prepareAddModel(globalOptions) /** * (1) Make a Vuex plugin for the provided service. * (2a) Attach the vuex store to the BaseModel. * (2b) If the Model does not extend the BaseModel, monkey patch it, too * (3) Setup real-time events */ return function makeServicePlugin(config: MakeServicePluginOptions) { if (!config.service) { throw new Error( 'No service was provided. If you passed one in, check that you have configured a transport plugin on the Feathers Client. Make sure you use the client version of the transport.' ) } const options = Object.assign({}, defaults, globalOptions, config) const { Model, service, namespace, nameStyle, instanceDefaults, setupInstance, preferUpdate } = options if (globalOptions.handleEvents && options.handleEvents) { options.handleEvents = Object.assign( {}, globalOptions.handleEvents, options.handleEvents ) } events.forEach(eventName => { if (!options.handleEvents[eventName]) options.handleEvents[eventName] = () => options.enableEvents || true }) // Make sure we get a service path from either the service or the options let { servicePath } = options if (!servicePath) { servicePath = getServicePath(service, Model) } options.servicePath = servicePath service.FeathersVuexModel = Model return store => { // (1^) Create and register the Vuex module options.namespace = makeNamespace(namespace, servicePath, nameStyle) const module = makeServiceModule(service, options, store) // Don't preserve state if reinitialized (prevents state pollution in SSR) store.registerModule(options.namespace, module, { preserveState: false }) // (2a^) Monkey patch the BaseModel in globalModels const BaseModel = _get(globalModels, [options.serverAlias, 'BaseModel']) if (BaseModel && !BaseModel.store) { Object.assign(BaseModel, { store }) } // (2b^) Monkey patch the Model(s) and add to globalModels assignIfNotPresent(Model, { namespace: options.namespace, servicePath, instanceDefaults, setupInstance, preferUpdate }) // As per 1^, don't preserve state on the model either (prevents state pollution in SSR) Object.assign(Model, { store }) if (!Model.modelName || Model.modelName === 'BaseModel') { throw new Error( 'The modelName property is required for Feathers-Vuex Models' ) } addModel(Model) // (3^) Setup real-time events if (options.enableEvents) { enableServiceEvents({ service, Model, store, options }) } } } } ================================================ FILE: src/service-module/service-module.actions.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import fastCopy from 'fast-copy' import { getId } from '../utils' import { Service } from '@feathersjs/feathers' import { MakeServicePluginOptions } from './types' interface serviceAndOptions { service: Service options: MakeServicePluginOptions } export default function makeServiceActions({service, options}: serviceAndOptions) { const serviceActions = { find({ commit, dispatch }, params) { params = params || {} params = fastCopy(params) // For working with client-side services, paginate.default must be truthy. if (params.paginate === true) { params.paginate = { default: true } } commit('setPending', 'find') return service .find(params) .then(response => dispatch('handleFindResponse', { params, response })) .catch(error => dispatch('handleFindError', { params, error })) }, // Two query syntaxes are supported, since actions only receive one argument. // 1. Just pass the id: `get(1)` // 2. Pass arguments as an array: `get([null, params])` get({ state, getters, commit, dispatch }, args) { let id let params let skipRequestIfExists if (Array.isArray(args)) { id = args[0] params = args[1] || {} } else { id = args params = {} } params = fastCopy(params) if ('skipRequestIfExists' in params) { skipRequestIfExists = params.skipRequestIfExists delete params.skipRequestIfExists } else { skipRequestIfExists = state.skipRequestIfExists } function getFromRemote() { commit('setPending', 'get') return service .get(id, params) .then(async function (item) { dispatch('addOrUpdate', item) commit('unsetPending', 'get') return state.keyedById[id] }) .catch(error => { commit('setError', { method: 'get', error }) commit('unsetPending', 'get') return Promise.reject(error) }) } // If the records is already in store, return it const existingItem = getters.get(id, params) if (existingItem && skipRequestIfExists) { return Promise.resolve(existingItem) } return getFromRemote() }, create({ commit, dispatch, state }, dataOrArray) { const { idField, tempIdField } = state let data let params let tempIds if (Array.isArray(dataOrArray)) { data = dataOrArray[0] params = dataOrArray[1] } else { data = dataOrArray } params = fastCopy(params) if (Array.isArray(data)) { tempIds = data.map(i => i[tempIdField]) } else { tempIds = [data[tempIdField]] // Array of tempIds } params = params || {} commit('setPending', 'create') commit('setIdPending', { method: 'create', id: tempIds }) return service .create(data, params) .then(async response => { if (Array.isArray(response)) { dispatch('addOrUpdateList', response) response = response.map(item => { const id = getId(item, idField) return state.keyedById[id] }) } else { const id = getId(response, idField) const tempId = tempIds[0] if (id != null && tempId != null) { commit('updateTemp', { id, tempId }) } response = dispatch('addOrUpdate', response) // response = state.keyedById[id] } commit('removeTemps', tempIds) return response }) .catch(error => { commit('setError', { method: 'create', error }) return Promise.reject(error) }) .finally(() => { commit('unsetPending', 'create') commit('unsetIdPending', { method: 'create', id: tempIds }) }) }, update({ commit, dispatch, state }, [id, data, params]) { commit('setPending', 'update') commit('setIdPending', { method: 'update', id }) params = fastCopy(params) return service .update(id, data, params) .then(async function (item) { dispatch('addOrUpdate', item) return state.keyedById[id] }) .catch(error => { commit('setError', { method: 'update', error }) return Promise.reject(error) }) .finally(() => { commit('unsetPending', 'update') commit('unsetIdPending', { method: 'update', id }) }) }, /** * If params.data is provided, it will be passed as the patch data (instead of the `data` arg). * This provides a simple way to patch with partial data. */ patch({ commit, dispatch, state }, [id, data, params]) { commit('setPending', 'patch') commit('setIdPending', { method: 'patch', id }) params = fastCopy(params) if (options.Model && (!params || !params.data)) { data = options.Model.diffOnPatch(data) } if (params && params.data) { data = params.data } return service .patch(id, data, params) .then(async function (item) { dispatch('addOrUpdate', item) return state.keyedById[id] }) .catch(error => { commit('setError', { method: 'patch', error }) return Promise.reject(error) }) .finally(() => { commit('unsetPending', 'patch') commit('unsetIdPending', { method: 'patch', id }) }) }, remove({ commit }, idOrArray) { let id let params if (Array.isArray(idOrArray)) { id = idOrArray[0] params = idOrArray[1] } else { id = idOrArray } params = params || {} params = fastCopy(params) commit('setPending', 'remove') commit('setIdPending', { method: 'remove', id }) return service .remove(id, params) .then(item => { commit('removeItem', id) return item }) .catch(error => { commit('setError', { method: 'remove', error }) return Promise.reject(error) }) .finally(() => { commit('unsetPending', 'remove') commit('unsetIdPending', { method: 'remove', id }) }) } } const actions = { count({ dispatch }, params) { params = params || {} params = fastCopy(params) if (!params.query) { throw 'params must contain a query-object' } params.query.$limit = 0 // <- limit 0 in feathers is a fast count query return dispatch('find', params) .then(response => { return response.total || response.length }) .catch(error => dispatch('handleFindError', { params, error })) }, /** * Handle the response from the find action. * * @param payload consists of the following two params * @param params - Remember that these params aren't what was sent to the * Feathers client. The client modifies the params object. * @param response */ async handleFindResponse( { state, commit, dispatch }, { params, response } ) { const { qid = 'default', query } = params const { idField } = state dispatch('addOrUpdateList', response) commit('unsetPending', 'find') const mapItemFromState = item => { const id = getId(item, idField) return state.keyedById[id] } // The pagination data will be under `pagination.default` or whatever qid is passed. response.data && commit('updatePaginationForQuery', { qid, response, query }) // Swap out the response records for their Vue-observable store versions const data = response.data || response const mappedFromState = data.map(mapItemFromState) if (mappedFromState[0] !== undefined) { response.data ? (response.data = mappedFromState) : (response = mappedFromState) } response = await dispatch('afterFind', response) return response }, async handleFindError({ commit }, { params, error }) { commit('setError', { method: 'find', params, error }) commit('unsetPending', 'find') return Promise.reject(error) }, async afterFind({}, response) { return response }, addOrUpdateList({ state, commit }, response) { const list = response.data || response const isPaginated = response.hasOwnProperty('total') const toAdd = [] const toUpdate = [] const toRemove = [] const { idField, autoRemove } = state const disableRemove = response.disableRemove || !autoRemove list.forEach(item => { const id = getId(item, idField) const existingItem = state.keyedById[id] if (id !== null && id !== undefined) { existingItem ? toUpdate.push(item) : toAdd.push(item) } }) if (!isPaginated && !disableRemove) { // Find IDs from the state which are not in the list state.ids.forEach(id => { if (!list.some(item => getId(item, idField) === id)) { toRemove.push(state.keyedById[id]) } }) commit('removeItems', toRemove) // commit removal } if (options.Model) { toAdd.forEach((item, index) => { toAdd[index] = new options.Model(item, { commit: false }) }) } commit('addItems', toAdd) commit('updateItems', toUpdate) return response }, /** * Adds or updates an item. If a matching temp record is found in the store, * the temp record will completely replace the existingItem. This is to work * around the common scenario where the realtime `created` event arrives before * the `create` response returns to create the record. The reference to the * original temporary record must be maintained in order to preserve reactivity. */ addOrUpdate({ state, commit }, item) { const { idField } = state const id = getId(item, idField) const isIdOk = id !== null && id !== undefined if ( options.Model && !(item instanceof options.Model) ) { item = new options.Model(item, { commit: false }) } if (isIdOk) { if (state.keyedById[id]) { commit('updateItem', item) } else { commit('addItem', item) } } return item } } /** * Only add a method to the store if the service actually has that same method. */ Object.keys(serviceActions).map(method => { if (service[method] && typeof service[method] === 'function') { actions[method] = serviceActions[method] } }) return actions } ================================================ FILE: src/service-module/service-module.events.ts ================================================ import { getId } from '../utils' import _debounce from 'lodash/debounce' import { globalModels } from './global-models' export interface ServiceEventsDebouncedQueue { addOrUpdateById: {} removeItemById: {} enqueueAddOrUpdate(item: any): void enqueueRemoval(item: any): void flushAddOrUpdateQueue(): void flushRemoveItemQueue(): void } export default function enableServiceEvents({ service, Model, store, options }): ServiceEventsDebouncedQueue { const debouncedQueue: ServiceEventsDebouncedQueue = { addOrUpdateById: {}, removeItemById: {}, enqueueAddOrUpdate(item): void { const id = getId(item, options.idField) this.addOrUpdateById[id] = item if (this.removeItemById.hasOwnProperty(id)) { delete this.removeItemById[id] } this.flushAddOrUpdateQueue() }, enqueueRemoval(item): void { const id = getId(item, options.idField) this.removeItemById[id] = item if (this.addOrUpdateById.hasOwnProperty(id)) { delete this.addOrUpdateById[id] } this.flushRemoveItemQueue() }, flushAddOrUpdateQueue: _debounce( async function () { const values = Object.values(this.addOrUpdateById) if (values.length === 0) return await store.dispatch(`${options.namespace}/addOrUpdateList`, { data: values, disableRemove: true }) this.addOrUpdateById = {} }, options.debounceEventsTime || 20, { maxWait: options.debounceEventsMaxWait } ), flushRemoveItemQueue: _debounce( function () { const values = Object.values(this.removeItemById) if (values.length === 0) return store.commit(`${options.namespace}/removeItems`, values) this.removeItemById = {} }, options.debounceEventsTime || 20, { maxWait: options.debounceEventsMaxWait } ) } const handleEvent = (eventName, item, mutationName): void => { const handler = options.handleEvents[eventName] const confirmOrArray = handler(item, { model: Model, models: globalModels }) const [affectsStore, modified = item] = Array.isArray(confirmOrArray) ? confirmOrArray : [confirmOrArray] if (affectsStore) { if (!options.debounceEventsTime) { eventName === 'removed' ? store.commit(`${options.namespace}/removeItem`, modified) : store.dispatch(`${options.namespace}/${mutationName}`, modified) } else { eventName === 'removed' ? debouncedQueue.enqueueRemoval(item) : debouncedQueue.enqueueAddOrUpdate(item) } } } // Listen to socket events when available. service.on('created', item => { handleEvent('created', item, 'addOrUpdate') Model.emit && Model.emit('created', item) }) service.on('updated', item => { handleEvent('updated', item, 'addOrUpdate') Model.emit && Model.emit('updated', item) }) service.on('patched', item => { handleEvent('patched', item, 'addOrUpdate') Model.emit && Model.emit('patched', item) }) service.on('removed', item => { handleEvent('removed', item, 'removeItem') Model.emit && Model.emit('removed', item) }) return debouncedQueue } ================================================ FILE: src/service-module/service-module.getters.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import sift from 'sift' import { filterQuery, sorter, select } from '@feathersjs/adapter-commons' import { globalModels as models } from './global-models' import _omit from 'lodash/omit' import { unref } from '@vue/composition-api' import { ServiceState } from '..' import { Id } from '@feathersjs/feathers' const FILTERS = ['$sort', '$limit', '$skip', '$select'] const additionalOperators = ['$elemMatch'] const getCopiesById = ({ keepCopiesInStore, servicePath, serverAlias, copiesById }) => { if (keepCopiesInStore) { return copiesById } else { const Model = models[serverAlias].byServicePath[servicePath] return Model.copiesById } } export default function makeServiceGetters() { return { list: state => Object.values(state.keyedById), find: state => _params => { const params = unref(_params) || {} const { paramsForServer, whitelist, keyedById, idField, tempsById } = state const q = _omit(params.query || {}, paramsForServer) const { query, filters } = filterQuery(q, { operators: additionalOperators.concat(whitelist) }) let values = Object.values(keyedById) as any if (params.temps) { values.push(...(Object.values(tempsById) as any)) } values = values.filter(sift(query)) if (params.copies) { const copiesById = getCopiesById(state) // replace keyedById value with existing clone value values = values.map(value => copiesById[value[idField]] || value) } const total = values.length if (filters.$sort !== undefined) { values.sort(sorter(filters.$sort)) } if (filters.$skip !== undefined && filters.$limit !== undefined) { values = values.slice(filters.$skip, filters.$limit + filters.$skip) } else if (filters.$skip !== undefined || filters.$limit !== undefined) { values = values.slice(filters.$skip, filters.$limit) } if (filters.$select) { values = select(params)(values) } return { total, limit: filters.$limit || 0, skip: filters.$skip || 0, data: values } }, count: (state, getters) => _params => { const params = unref(_params) || {} const cleanQuery = _omit(params.query, FILTERS) params.query = cleanQuery return getters.find(params).total }, get: ({ keyedById, tempsById, idField, tempIdField }) => ( _id, _params = {} ) => { const id = unref(_id) const params = unref(_params) const record = keyedById[id] && select(params, idField)(keyedById[id]) if (record) { return record } const tempRecord = tempsById[id] && select(params, tempIdField)(tempsById[id]) return tempRecord || null }, getCopyById: state => id => { const copiesById = getCopiesById(state) return copiesById[id] }, isCreatePendingById: ({ isIdCreatePending }: ServiceState) => (id: Id) => isIdCreatePending.includes(id), isUpdatePendingById: ({ isIdUpdatePending }: ServiceState) => (id: Id) => isIdUpdatePending.includes(id), isPatchPendingById: ({ isIdPatchPending }: ServiceState) => (id: Id) => isIdPatchPending.includes(id), isRemovePendingById: ({ isIdRemovePending }: ServiceState) => (id: Id) => isIdRemovePending.includes(id), isSavePendingById: (state: ServiceState, getters) => (id: Id) => getters.isCreatePendingById(id) || getters.isUpdatePendingById(id) || getters.isPatchPendingById(id), isPendingById: (state: ServiceState, getters) => (id: Id) => getters.isSavePendingById(id) || getters.isRemovePendingById(id) } } export type GetterName = keyof ReturnType ================================================ FILE: src/service-module/service-module.mutations.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0, no-var: 0 */ import Vue from 'vue' import { serializeError } from 'serialize-error' import { updateOriginal, mergeWithAccessors, assignTempId, getId, getQueryInfo } from '../utils' import { globalModels as models } from './global-models' import _omit from 'lodash/omit' import _get from 'lodash/get' import _isObject from 'lodash/isObject' import { Id } from '@feathersjs/feathers' import { ServiceState } from '..' export type PendingServiceMethodName = | 'find' | 'get' | 'create' | 'update' | 'patch' | 'remove' export type PendingIdServiceMethodName = Exclude< PendingServiceMethodName, 'find' | 'get' > export default function makeServiceMutations() { function addItems(state, items) { const { serverAlias, idField, tempIdField, modelName } = state const Model = _get(models, [serverAlias, modelName]) const BaseModel = _get(models, [serverAlias, 'BaseModel']) for (let item of items) { const id = getId(item, idField) const isTemp = id === null || id === undefined // If the response contains a real id, remove isTemp if (id != null) { delete item.__isTemp } if (Model && !(item instanceof BaseModel) && !(item instanceof Model)) { item = new Model(item, { commit: false }) } if (isTemp) { let tempId = item[tempIdField] if (tempId == null) { tempId = assignTempId(state, item) } item.__isTemp = true Vue.set(state.tempsById, tempId, item) } else { // Only add the id if it's not already in the `ids` list. if (!state.ids.includes(id)) { state.ids.push(id) } Vue.set(state.keyedById, id, item) } } } function updateItems(state, items) { const { idField, replaceItems, addOnUpsert, serverAlias, modelName } = state const Model = _get(models, [serverAlias, modelName]) const BaseModel = _get(models, [state.serverAlias, 'BaseModel']) for (let item of items) { const id = getId(item, idField) // If the response contains a real id, remove isTemp if (id != null) { delete item.__isTemp } // Update the record if (id !== null && id !== undefined) { if (state.ids.includes(id)) { // Completely replace the item if (replaceItems) { if (Model && !(item instanceof Model)) { item = new Model(item) } Vue.set(state.keyedById, id, item) // Merge in changes } else { /** * If we have a Model class, calling new Model(incomingData) will call update * the original record with the accessors and setupInstance data. * This means that date objects and relationships will be preserved. * * If there's no Model class, just call updateOriginal on the incoming data. */ if ( Model && !(item instanceof BaseModel) && !(item instanceof Model) ) { item = new Model(item) } else { const original = state.keyedById[id] updateOriginal(original, item) } } // if addOnUpsert then add the record into the state, else discard it. } else if (addOnUpsert) { state.ids.push(id) Vue.set(state.keyedById, id, item) } continue } } } function mergeInstance(state, item) { const { idField } = state const id = getId(item, idField) const existingItem = state.keyedById[id] if (existingItem) { mergeWithAccessors(existingItem, item) } } function merge(state, { dest, source }) { mergeWithAccessors(dest, source) } return { mergeInstance, merge, addItem(state, item) { addItems(state, [item]) }, addItems, updateItem(state, item) { updateItems(state, [item]) }, updateItems(state, items) { if (!Array.isArray(items)) { throw new Error( 'You must provide an array to the `updateItems` mutation.' ) } updateItems(state, items) }, // Promotes temp to "real" item: // - adds _id to temp // - removes __isTemp flag // - migrates temp from tempsById to keyedById updateTemp(state, { id, tempId }) { const temp = state.tempsById[tempId] if (temp) { temp[state.idField] = id Vue.delete(temp, '__isTemp') Vue.delete(state.tempsById, tempId) // If an item already exists in the store from the `created` event firing // it will be replaced here Vue.set(state.keyedById, id, temp) // Only add the id if it's not already in the `ids` list. if (!state.ids.includes(id)) { state.ids.push(id) } } // Add _id to temp's clone as well if it exists const Model = _get(models, [state.serverAlias, state.modelName]) const tempClone = Model && Model.copiesById && Model.copiesById[tempId] if (tempClone) { tempClone[state.idField] = id Model.copiesById[id] = tempClone Vue.delete(tempClone, '__isTemp') } }, removeItem(state, item) { const { idField } = state const idToBeRemoved = _isObject(item) ? getId(item, idField) : item const isIdOk = idToBeRemoved !== null && idToBeRemoved !== undefined const index = state.ids.findIndex(i => i === idToBeRemoved) const Model = _get(models, `[${state.serverAlias}][${state.modelName}]`) const copiesById = state.keepCopiesInStore ? state.copiesById : Model.copiesById if (isIdOk && index !== null && index !== undefined) { Vue.delete(state.ids, index) Vue.delete(state.keyedById, idToBeRemoved) if (copiesById.hasOwnProperty(idToBeRemoved)) { Vue.delete(copiesById, idToBeRemoved) } } }, // Removes temp records removeTemps(state, tempIds) { tempIds.forEach(id => { const temp = state.tempsById[id] if (temp) { if (temp[state.idField]) { // Removes __isTemp if created delete temp.__isTemp Vue.delete(temp, '__isTemp') } } }) state.tempsById = _omit(state.tempsById, tempIds) }, removeItems(state, items) { const { idField } = state if (!Array.isArray(items)) { throw new Error( 'You must provide an array to the `removeItems` mutation.' ) } // Make sure we have an array of ids. Assume all are the same. const containsObjects = items[0] && _isObject(items[0]) const idsToRemove = containsObjects ? items.map(item => getId(item, idField)) : items const mapOfIdsToRemove = idsToRemove.reduce((map, id) => { map[id] = true return map }, {}) const Model = _get(models, [ state.serverAlias, 'byServicePath', state.servicePath ]) const copiesById = state.keepCopiesInStore ? state.copiesById : Model.copiesById idsToRemove.forEach(id => { Vue.delete(state.keyedById, id) if (copiesById.hasOwnProperty(id)) { Vue.delete(copiesById, id) } }) // Get indexes to remove from the ids array. const mapOfIndexesToRemove = state.ids.reduce((map, id, index) => { if (mapOfIdsToRemove[id]) { map[index] = true } return map }, {}) // Remove highest indexes first, so the indexes don't change const indexesInReverseOrder = Object.keys(mapOfIndexesToRemove).sort( (a, b) => { if (a < b) { return 1 } else if (a > b) { return -1 } else { return 0 } } ) indexesInReverseOrder.forEach(indexInIdsArray => { Vue.delete(state.ids, indexInIdsArray) }) }, clearAll(state) { state.ids = [] state.keyedById = {} if (state.keepCopiesInStore) { state.copiesById = {} } else { const Model = _get(models, [ state.serverAlias, 'byServicePath', state.servicePath ]) Object.keys(Model.copiesById).forEach(k => Vue.delete(Model.copiesById, k) ) } }, // Creates a copy of the record with the passed-in id, stores it in copiesById createCopy(state, id) { const { servicePath, keepCopiesInStore, serverAlias } = state const current = state.keyedById[id] || state.tempsById[id] const Model = _get(models, [serverAlias, 'byServicePath', servicePath]) let item if (Model) { item = new Model(current, { clone: true }) } else { const existingClone = state.copiesById[id] item = existingClone ? mergeWithAccessors(existingClone, current) : mergeWithAccessors({}, current) } if (keepCopiesInStore) { state.copiesById[id] = item } else { // Since it won't be added to the store, make it a Vue object if (!item.hasOwnProperty('__ob__')) { item = Vue.observable(item) } if (!Model.hasOwnProperty('copiesById')) { Object.defineProperty(Model, 'copiesById', { value: {} }) } Model.copiesById[id] = item } }, // Resets the copy to match the original record, locally resetCopy(state, id) { const { servicePath, keepCopiesInStore } = state const Model = _get(models, [ state.serverAlias, 'byServicePath', servicePath ]) const copy = keepCopiesInStore ? state.copiesById[id] : Model && _get(Model, ['copiesById', id]) if (copy) { const original = copy[state.idField] != null ? state.keyedById[id] : state.tempsById[id] mergeWithAccessors(copy, original) } }, // Deep assigns copy to original record, locally commitCopy(state, id) { const { servicePath, keepCopiesInStore } = state const Model = _get(models, [ state.serverAlias, 'byServicePath', servicePath ]) const copy = keepCopiesInStore ? state.copiesById[id] : Model && _get(Model, ['copiesById', id]) if (copy) { const original = copy[state.idField] != null ? state.keyedById[id] : state.tempsById[id] mergeWithAccessors(original, copy) } }, // Removes the copy from copiesById clearCopy(state, id) { const { keepCopiesInStore } = state const Model = _get(models, [ state.serverAlias, 'byServicePath', state.servicePath ]) const copiesById = keepCopiesInStore ? state.copiesById : Model.copiesById if (copiesById[id]) { Vue.delete(copiesById, id) } }, /** * Stores pagination data on state.pagination based on the query identifier * (qid) The qid must be manually assigned to `params.qid` */ updatePaginationForQuery(state, { qid, response, query = {} }) { const { data, total } = response const { idField } = state const ids = data.map(i => i[idField]) const queriedAt = new Date().getTime() const { queryId, queryParams, pageId, pageParams } = getQueryInfo( { qid, query }, response ) if (!state.pagination[qid]) { Vue.set(state.pagination, qid, {}) } if (!query.hasOwnProperty('$limit') && response.hasOwnProperty('limit')) { Vue.set(state.pagination, 'defaultLimit', response.limit) } if (!query.hasOwnProperty('$skip') && response.hasOwnProperty('skip')) { Vue.set(state.pagination, 'defaultSkip', response.skip) } const mostRecent = { query, queryId, queryParams, pageId, pageParams, queriedAt, total } const qidData = state.pagination[qid] || {} Object.assign(qidData, { mostRecent }) qidData[queryId] = qidData[queryId] || {} const queryData = { total, queryParams } Object.assign(qidData[queryId], queryData) const pageData = { [pageId]: { pageParams, ids, queriedAt } } Object.assign(qidData[queryId], pageData) const newState = Object.assign({}, state.pagination[qid], qidData) Vue.set(state.pagination, qid, newState) }, setPending(state, method: PendingServiceMethodName): void { const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) state[`is${uppercaseMethod}Pending`] = true }, unsetPending(state, method: PendingServiceMethodName): void { const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) state[`is${uppercaseMethod}Pending`] = false }, setIdPending( state, payload: { method: PendingIdServiceMethodName; id: Id | Id[] } ): void { const { method, id } = payload const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) const isIdMethodPending = state[ `isId${uppercaseMethod}Pending` ] as ServiceState['isIdCreatePending'] // if `id` is an array, ensure it doesn't have duplicates const ids = Array.isArray(id) ? [...new Set(id)] : [id] ids.forEach(id => { if (typeof id === 'number' || typeof id === 'string') { isIdMethodPending.push(id) } }) }, unsetIdPending( state, payload: { method: PendingIdServiceMethodName; id: Id | Id[] } ): void { const { method, id } = payload const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) const isIdMethodPending = state[ `isId${uppercaseMethod}Pending` ] as ServiceState['isIdCreatePending'] // if `id` is an array, ensure it doesn't have duplicates const ids = Array.isArray(id) ? [...new Set(id)] : [id] ids.forEach(id => { const idx = isIdMethodPending.indexOf(id) if (idx >= 0) { Vue.delete(isIdMethodPending, idx) } }) }, setError( state, payload: { method: PendingServiceMethodName; error: Error } ): void { const { method, error } = payload const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) state[`errorOn${uppercaseMethod}`] = Object.assign( {}, serializeError(error) ) }, clearError(state, method: PendingServiceMethodName): void { const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) state[`errorOn${uppercaseMethod}`] = null } } } ================================================ FILE: src/service-module/service-module.state.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import _omit from 'lodash/omit' import { MakeServicePluginOptions, Model } from './types' import { Id } from '@feathersjs/feathers' export interface ServiceStateExclusiveDefaults { ids: string[] errorOnFind: any errorOnGet: any errorOnCreate: any errorOnPatch: any errorOnUpdate: any errorOnRemove: any isFindPending: boolean isGetPending: boolean isCreatePending: boolean isPatchPending: boolean isUpdatePending: boolean isRemovePending: boolean keyedById: {} tempsById: {} copiesById: {} namespace?: string pagination?: { defaultLimit: number defaultSkip: number default?: PaginationState } paramsForServer: string[] modelName?: string debounceEventsTime: number isIdCreatePending: Id[] isIdUpdatePending: Id[] isIdPatchPending: Id[] isIdRemovePending: Id[] } export interface ServiceState { options: {} ids: string[] autoRemove: boolean errorOnFind: any errorOnGet: any errorOnCreate: any errorOnPatch: any errorOnUpdate: any errorOnRemove: any isFindPending: boolean isGetPending: boolean isCreatePending: boolean isPatchPending: boolean isUpdatePending: boolean isRemovePending: boolean idField: string tempIdField: string keyedById: { [k: string]: M [k: number]: M } tempsById: { [k: string]: M [k: number]: M } copiesById: { [k: string]: M } whitelist: string[] paramsForServer: string[] namespace: string nameStyle: string // Should be enum of 'short' or 'path' pagination?: { defaultLimit: number defaultSkip: number default?: PaginationState } modelName?: string debounceEventsTime: number debounceEventsMaxWait: number isIdCreatePending: Id[] isIdUpdatePending: Id[] isIdPatchPending: Id[] isIdRemovePending: Id[] } export interface PaginationState { ids: any limit: number skip: number ip: number total: number mostRecent: any } export default function makeDefaultState(options: MakeServicePluginOptions) { const nonStateProps = [ 'Model', 'service', 'instanceDefaults', 'setupInstance', 'handleEvents', 'extend', 'state', 'getters', 'mutations', 'actions' ] const state: ServiceStateExclusiveDefaults = { ids: [], keyedById: {}, copiesById: {}, tempsById: {}, // Really should be called tempsByTempId pagination: { defaultLimit: null, defaultSkip: null }, paramsForServer: ['$populateParams'], debounceEventsTime: null, isFindPending: false, isGetPending: false, isCreatePending: false, isUpdatePending: false, isPatchPending: false, isRemovePending: false, errorOnFind: null, errorOnGet: null, errorOnCreate: null, errorOnUpdate: null, errorOnPatch: null, errorOnRemove: null, isIdCreatePending: [], isIdUpdatePending: [], isIdPatchPending: [], isIdRemovePending: [] } if (options.Model) { state.modelName = options.Model.modelName } const startingState = _omit(options, nonStateProps) return Object.assign({}, state, startingState) } ================================================ FILE: src/service-module/types.ts ================================================ import { Service, Id } from '@feathersjs/feathers' import { Params, Paginated } from '../utils' import { EventEmitter } from 'events' import { Store } from 'vuex' import { Ref } from '@vue/composition-api' export { Id } from '@feathersjs/feathers' /* eslint @typescript-eslint/no-explicit-any: 0 */ export interface FeathersVuexOptions { serverAlias: string addOnUpsert?: boolean autoRemove?: boolean debug?: boolean enableEvents?: boolean handleEvents?: HandleEvents idField?: string tempIdField?: string keepCopiesInStore?: boolean debounceEventsTime?: number debounceEventsMaxWait?: number nameStyle?: string paramsForServer?: string[] preferUpdate?: boolean replaceItems?: boolean skipRequestIfExists?: boolean whitelist?: string[] } export interface HandleEvents { created?: Function patched?: Function updated?: Function removed?: Function } export interface ServicePluginExtendOptions { store: Store module: any } export interface MakeServicePluginOptions { Model: any service: Service idField?: string tempIdField?: string addOnUpsert?: boolean autoRemove?: boolean debug?: boolean enableEvents?: boolean preferUpdate?: boolean replaceItems?: boolean skipRequestIfExists?: boolean nameStyle?: string keepCopiesInStore?: boolean debounceEventsTime?: number debounceEventsMaxWait?: number servicePath?: string namespace?: string whitelist?: string[] paramsForServer?: string[] instanceDefaults?: () => {} setupInstance?: (data: any, { models, store }) => {} handleEvents?: HandleEvents extend?: ( options: ServicePluginExtendOptions ) => { state?: any getters?: any mutations?: any actions?: any } state?: {} getters?: {} mutations?: {} actions?: {} } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface FeathersVuexStoreState { /** Allow clients to augment store state */ } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface FeathersVuexGlobalModels { /** Allow clients to augment Global models */ } // Alias and default to any if user doesn't augment interfaces export type StoreState = keyof FeathersVuexStoreState extends never ? any : FeathersVuexStoreState export type GlobalModels = keyof FeathersVuexGlobalModels extends never ? any : FeathersVuexGlobalModels export interface PatchParams extends Params { data?: Partial } export interface ModelSetupContext { /** * The global Vuex store */ store: StoreState /** * The global `models` object */ models: GlobalModels } export interface ModelInstanceOptions { /** * Creating clone? * * Default: `false` */ clone?: boolean /** * Add to store * * Default: `true` */ commit?: boolean /** * Merge with existing? * * Default: `true` */ merge?: boolean } export type AnyData = { [key: string]: any } /** Static Model interface */ export interface ModelStatic extends EventEmitter { /** * The path passed to `FeathersClient.service()` to create the service */ servicePath: string /** * Holds the value that was used to register the module with Vuex. * This will match the servicePath unless you've provided a custom * namespace in the Service Module plugin options. */ namespace: string /** * The global Vuex store */ readonly store: Store /** * The field in each record that will contain the ID */ idField: string /** * The field in each temporary record that contains the temporary ID */ tempIdField: string /** * If `true`, calling `model.save()` will do an `update` instead of a `patch`. */ preferUpdate: boolean /** * Server alias in the global `models` object */ serverAlias: string /** * Model name used to circumvent Babel transpilation errors */ modelName: string /** * The global `models` object */ readonly models: GlobalModels /** * All model copies created using `model.clone()` */ readonly copiesById: { [key: string]: Model | undefined [key: number]: Model | undefined } /** * The BaseModel constructor calls mergeWithAccessors(this, newData). * This utility function correctly copies data between both regular * objects and Vue.observable instances. If you create a class where * you need to do your own merging, you probably don't want * mergeWithAccessors to run twice. In this case, you can use the * `merge: false` BaseModel instance option to prevent the internal * merge. You can then access the mergeWithAccessors method by calling * this method like MyModel.merge(this, newData). * @param dest destination object * @param source source object * @param blacklist keys to ignore when merging * @example * class Todo extends BaseModel { * public constructor(data, options?) { * options.merge = false // Prevent the internal merge * super(data, options) * // ... your custom constructor logic happens here. * // Call the static merge method to do your own merging. * Todo.merge(this, data) * } * } */ merge(dest: unknown, source: unknown, blacklist?: string[]): void /** * Create new Model * @param data partial model data * @param options model instance options */ new (data?: AnyData, options?: ModelInstanceOptions): Model prototype: Model /** * The instanceDefaults API was created in version 1.7 to prevent * requiring to specify data for new instances created throughout * the app. Depending on the complexity of the service's "business * logic", it can save a lot of boilerplate. Notice that it is * similar to the setupInstance method added in 2.0. The instanceDefaults * method should ONLY be used to return default values for a new * instance. Use setupInstance to handle other transformations on * the data. * @param data the instance data * @param ctx setup context */ instanceDefaults(data: AnyData, ctx: ModelSetupContext): AnyData /** * A new setupInstance class method is now available in version 2.0. * This method allows you to transform the data and setup the final * instance based on incoming data. For example, you can access the * models object to reference other service Model classes and create * data associations. * @param data the instance data * @param ctx setup context */ setupInstance(data: AnyData, ctx: ModelSetupContext): AnyData /** * Gets called just before sending the data to the API server. It gets * called with the data and must return the diffed data. * * Default: `data => data` * @param data the instance data */ diffOnPatch(data: AnyData): AnyData /** * A proxy for the `find` action * @param params Find params */ find(params?: Params): Promise> /** * A proxy for the `find` getter * @param params Find params */ findInStore( params?: Params | Ref ): Paginated /** * A proxy for the `count` action * @param params Find params */ count(params?: Params): Promise /** * A proxy for the `count` getter * @param params Find params */ countInStore(params?: Params | Ref): number /** * A proxy for the `get` action * @param id ID of record to retrieve * @param params Get params */ get(id: Id, params?: Params): Promise /** * A proxy for the `get` getter * @param id ID of record to retrieve * @param params Get params */ getFromStore( id: Id | Ref, params?: Params | Ref ): M | undefined } /** Model instance interface */ export interface Model { [key: string]: any /** * model's temporary ID */ readonly __id?: string /** * model is temporary? */ readonly __isTemp?: boolean /** * model is a clone? */ readonly __isClone?: boolean /** * `Create` is currently pending on this model */ readonly isCreatePending: boolean /** * `Update` is currently pending on this model */ readonly isUpdatePending: boolean /** * `Patch` is currently pending on this model */ readonly isPatchPending: boolean /** * `Remove` is currently pending on this model */ readonly isRemovePending: boolean /** * Any of `create`, `update` or `patch` is currently pending on this model */ readonly isSavePending: boolean /** * Any method is currently pending on this model */ readonly isPending: boolean /** * Creates a deep copy of the record and stores it on * `Model.copiesById`. This allows you to make changes * to the clone and not update visible data until you * commit or save the data. * @param data Properties to modify on the cloned instance */ clone(data?: AnyData): this /** * The create method calls the create action (service method) * using the instance data. * @param params Params passed to the Feathers client request */ create(params?: Params): Promise /** * The patch method calls the patch action (service method) * using the instance data. The instance's id field is used * for the patch id. * * You can provide an object as `params.data`, and Feathers-Vuex * will use `params.data` as the patch data. This allows patching * with partial data. * @param params Params passed to the Feathers client request */ patch(params?: PatchParams): Promise /** * The remove method calls the remove action (service method) * using the instance data. The instance's id field is used * for the remove id. * @param params Params passed to the Feathers client request */ remove(params?: Params): Promise /** * The update method calls the update action (service method) * using the instance data. The instance's id field is used for * the update id. * @param params Params passed to the Feathers client request */ update(params?: Params): Promise /** * The save method is a convenience wrapper for the create/patch * methods, by default. If the records has no _id, the * instance.create() method will be used. * @param params Params passed to the Feathers client request */ save(params?: Params): Promise /** * Commit changes from clone to original */ commit(): this /** * Discards changes made on this clone and syncs with the original */ reset(): this } ================================================ FILE: src/useFind.ts ================================================ /* eslint @typescript-eslint/no-explicit-any: 0 */ import { computed, isRef, reactive, Ref, toRefs, watch } from '@vue/composition-api' import debounce from 'lodash/debounce' import { getItemsFromQueryInfo, getQueryInfo, Params, Paginated } from './utils' import { ModelStatic, Model } from './service-module/types' interface UseFindOptions { model: ModelStatic params: Params | Ref fetchParams?: Params | Ref queryWhen?: Ref qid?: string local?: boolean immediate?: boolean } interface UseFindState { debounceTime: null | number qid: string isPending: boolean haveBeenRequested: boolean haveLoaded: boolean error: null | Error latestQuery: null | object isLocal: boolean } interface UseFindData { items: Ref> servicePath: Ref isPending: Ref haveBeenRequested: Ref haveLoaded: Ref isLocal: Ref qid: Ref debounceTime: Ref latestQuery: Ref paginationData: Ref error: Ref find(params?: Params | Ref): Promise> } const unwrapParams = (params: Params | Ref): Params => isRef(params) ? params.value : params export default function find(options: UseFindOptions): UseFindData { const defaults: UseFindOptions = { model: null, params: null, qid: 'default', queryWhen: computed((): boolean => true), local: false, immediate: true } const { model, params, queryWhen, qid, local, immediate } = Object.assign( {}, defaults, options ) if (!model) { throw new Error( `No model provided for useFind(). Did you define and register it with FeathersVuex?` ) } const getFetchParams = (providedParams?: Params | Ref): Params => { const provided = unwrapParams(providedParams) if (provided) { return provided } const fetchParams = unwrapParams(options.fetchParams) // Returning null fetchParams allows the query to be skipped. if (fetchParams || fetchParams === null) { return fetchParams } const params = unwrapParams(options.params) return params } const state = reactive({ qid, isPending: false, haveBeenRequested: false, haveLoaded: local, error: null, debounceTime: null, latestQuery: null, isLocal: local }) const computes = { // The find getter items: computed(() => { const getterParams = unwrapParams(params) if (getterParams) { if (getterParams.paginate) { const serviceState = model.store.state[model.servicePath] const { defaultSkip, defaultLimit } = serviceState.pagination const skip = getterParams.query.$skip || defaultSkip const limit = getterParams.query.$limit || defaultLimit const pagination = computes.paginationData.value[getterParams.qid || state.qid] || {} const response = skip != null && limit != null ? { limit, skip } : {} const queryInfo = getQueryInfo(getterParams, response) const items = getItemsFromQueryInfo( pagination, queryInfo, serviceState.keyedById ) return items } else { return model.findInStore(getterParams).data } } else { return [] } }), paginationData: computed(() => { return model.store.state[model.servicePath].pagination }), servicePath: computed(() => model.servicePath) } function find(params?: Params | Ref): Promise> { params = unwrapParams(params) if (queryWhen.value && !state.isLocal) { state.isPending = true state.haveBeenRequested = true return model.find(params).then(response => { // To prevent thrashing, only clear error on response, not on initial request. state.error = null state.haveLoaded = true if(!Array.isArray(response)) { const queryInfo = getQueryInfo(params, response) queryInfo.response = response queryInfo.isOutdated = false state.latestQuery = queryInfo } state.isPending = false return response }) } } const methods = { findDebounced(params?: Params) { return find(params) } } function findProxy(params?: Params | Ref) { const paramsToUse = getFetchParams(params) if (paramsToUse && paramsToUse.debounce) { if (paramsToUse.debounce !== state.debounceTime) { methods.findDebounced = debounce(find, paramsToUse.debounce) state.debounceTime = paramsToUse.debounce } return methods.findDebounced(paramsToUse) } else if (paramsToUse) { return find(paramsToUse) } else { // Set error } } watch( () => getFetchParams(), () => { findProxy() }, { immediate } ) return { ...computes, ...toRefs(state), find } } ================================================ FILE: src/useGet.ts ================================================ /* eslint @typescript-eslint/no-explicit-any: 0 */ import { reactive, computed, toRefs, isRef, watch, Ref } from '@vue/composition-api' import { Params } from './utils' import { ModelStatic, Model, Id } from './service-module/types' interface UseGetOptions { model: ModelStatic id: null | string | number | Ref | Ref | Ref params?: Params | Ref queryWhen?: Ref local?: boolean immediate?: boolean } interface UseGetState { isPending: boolean hasBeenRequested: boolean hasLoaded: boolean error: null | Error isLocal: boolean } interface UseGetData { item: Ref> servicePath: Ref isPending: Ref hasBeenRequested: Ref hasLoaded: Ref isLocal: Ref error: Ref get(id: Id, params?: Params): Promise } export default function get(options: UseGetOptions): UseGetData { const defaults: UseGetOptions = { model: null, id: null, params: null, queryWhen: computed((): boolean => true), local: false, immediate: true } const { model, id, params, queryWhen, local, immediate } = Object.assign( {}, defaults, options ) if (!model) { throw new Error( `No model provided for useGet(). Did you define and register it with FeathersVuex?` ) } function getId(): null | string | number { return isRef(id) ? id.value : id || null } function getParams(): Params { return isRef(params) ? params.value : params } const state = reactive({ isPending: false, hasBeenRequested: false, hasLoaded: false, error: null, isLocal: local }) const computes = { item: computed(() => { const getterId = isRef(id) ? id.value : id const getterParams = isRef(params) ? Object.assign({}, params.value) : params == null ? params : { ...params } if (getterParams != null) { return model.getFromStore(getterId, getterParams) || null } else { return model.getFromStore(getterId) || null } }), servicePath: computed(() => model.servicePath) } function get(id: Id, params?: Params): Promise { const idToUse = isRef(id) ? id.value : id const paramsToUse = isRef(params) ? params.value : params if (idToUse != null && queryWhen.value && !state.isLocal) { state.isPending = true state.hasBeenRequested = true const promise = paramsToUse != null ? model.get(idToUse, paramsToUse) : model.get(idToUse) return promise .then(response => { state.isPending = false state.hasLoaded = true return response }) .catch(error => { state.isPending = false state.error = error return error }) } else { return Promise.resolve(undefined) } } watch( [() => getId(), () => getParams()], ([id, params]) => { get(id as string | number, params as Params) }, { immediate } ) return { ...toRefs(state), ...computes, get } } ================================================ FILE: src/utils.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import decode from 'jwt-decode' import inflection from 'inflection' import Vue from 'vue' import fastCopy from 'fast-copy' import _isObject from 'lodash/isObject' import _trim from 'lodash/trim' import _omit from 'lodash/omit' import ObjectID from 'bson-objectid' import { globalModels as models } from './service-module/global-models' import stringify from 'fast-json-stable-stringify' import { Service } from '@feathersjs/feathers' interface Query { [key: string]: any } interface PaginationOptions { default: number max: number } interface Params { query?: Query paginate?: false | Pick provider?: string route?: { [key: string]: string } headers?: { [key: string]: any } temps?: boolean copies?: boolean [key: string]: any // (JL) not sure if we want this } interface Paginated { total: number limit: number skip: number data: T[] } export { Query, PaginationOptions, Params, Paginated } export function stripSlashes(location: string) { return _trim(location, '/') } export function setByDot(obj, path, value, ifDelete?) { if (ifDelete) { // eslint-disable-next-line no-console console.log( 'DEPRECATED. Use deleteByDot instead of setByDot(obj,path,value,true). (setByDot)' ) } if (path.indexOf('.') === -1) { obj[path] = value if (value === undefined && ifDelete) { delete obj[path] } return } const parts = path.split('.') const lastIndex = parts.length - 1 return parts.reduce((obj1, part, i) => { if (i !== lastIndex) { if (!obj1.hasOwnProperty(part) || typeof obj1[part] !== 'object') { obj1[part] = {} } return obj1[part] } obj1[part] = value if (value === undefined && ifDelete) { delete obj1[part] } return obj1 }, obj) } export function upperCaseFirst(string) { return string.charAt(0).toUpperCase() + string.slice(1) } export function getShortName(service) { let namespace = stripSlashes(service) if (Array.isArray(namespace)) { namespace = namespace.slice(-1) } else if (namespace.includes('/')) { namespace = namespace.slice(namespace.lastIndexOf('/') + 1) } return namespace } export function getNameFromPath(service) { return stripSlashes(service) } // Reads and returns the contents of a cookie with the provided name. export function readCookie(cookies, name) { if (!cookies) { return undefined } const nameEQ = name + '=' const ca = cookies.split(';') for (let i = 0; i < ca.length; i++) { let c = ca[i] while (c.charAt(0) === ' ') { c = c.substring(1, c.length) } if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length, c.length) } } return null } // Pass a decoded payload and it will return a boolean based on if it hasn't expired. export function payloadIsValid(payload) { return payload && payload.exp * 1000 > new Date().getTime() } // from https://github.com/iliakan/detect-node export const isNode = Object.prototype.toString.call( typeof process !== 'undefined' ? process : 0 ) === '[object process]' export const isBrowser = !isNode const authDefaults = { commit: undefined, req: undefined, moduleName: 'auth', cookieName: 'feathers-jwt' } export function getValidPayloadFromToken(token) { if (token) { try { const payload = decode(token) return payloadIsValid(payload) ? payload : undefined } catch (error) { return undefined } } return undefined } export const initAuth = function initAuth(options) { const { commit, req, moduleName, cookieName, feathersClient } = Object.assign( {}, authDefaults, options ) if (typeof commit !== 'function') { throw new Error( 'You must pass the `commit` function in the `initAuth` function options.' ) } let cookies if (req) { cookies = req.headers.cookie } else if (document && document.cookie) { cookies = document.cookie } else { throw new Error( 'You must pass the `req` object in the `initAuth` function options.' ) } const accessToken = readCookie(cookies, cookieName) const payload = getValidPayloadFromToken(accessToken) if (payload) { commit(`${moduleName}/setAccessToken`, accessToken) commit(`${moduleName}/setPayload`, payload) if (feathersClient) { return feathersClient.authentication .setAccessToken(accessToken) .then(() => payload) } } return Promise.resolve(payload) } /** * run de BaseModel hydration on client for each api */ export const hydrateApi = function hydrateApi({ api }) { Object.keys(api).forEach(modelName => { if (!['byServicePath', 'BaseModel'].includes(modelName)) { const Model = api[modelName] Model.hydrateAll() } }) } /** * Generate a new tempId and mark the record as a temp * @param state * @param item */ export function assignTempId(state, item) { const { debug, tempIdField } = state if (debug) { // eslint-disable-next-line no-console console.info('assigning temporary id to item', item) } const newId = new ObjectID().toHexString() item[tempIdField] = newId return newId } function stringifyIfObject(val): string | any { if (typeof val === 'object' && val != null) { return val.toString() } return val } /** * Get the id from a record in this order: * 1. the `idField` * 2. id * 3. _id * @param item * @param idField */ export function getId(item, idField?) { if (!item) { return } if (item[idField] != null || item.hasOwnProperty(idField)) { return stringifyIfObject(item[idField]) } if (item.id != null || item.hasOwnProperty('id')) { return stringifyIfObject(item.id) } if (item._id != null || item.hasOwnProperty('_id')) { return stringifyIfObject(item._id) } } // Creates a Model class name from the last part of the servicePath export function getModelName(Model) { // If the Model.name has been customized, use it. if (Model.modelName) { return Model.modelName } // Otherwise, use an inflection of the last bit of the servicePath const parts = Model.servicePath.split('/') let name = parts[parts.length - 1] name = inflection.titleize(name) name = name.split('-').join('') name = inflection.singularize(name) return name } export function registerModel(Model, globalModels, apiPrefix, servicePath) { const modelName = getModelName(Model) const path = apiPrefix ? `${apiPrefix}.${modelName}` : modelName setByDot(globalModels, path, Model) globalModels.byServicePath[servicePath] = Model return { path, name: modelName } } export function getServicePrefix(servicePath) { const parts = servicePath.split('/') let name = parts[parts.length - 1] // name = inflection.underscore(name) name = name.replace(/-/g, '_') name = inflection.camelize(name, true) return name } export function getServiceCapitalization(servicePath) { const parts = servicePath.split('/') let name = parts[parts.length - 1] // name = inflection.underscore(name) name = name.replace(/-/g, '_') name = inflection.camelize(name) return name } export function updateOriginal(original, newData) { Object.keys(newData).forEach(key => { const newProp = newData[key] const oldProp = original[key] let shouldCopyProp = false if (newProp === oldProp) { return } // If the old item doesn't already have this property, update it if (!original.hasOwnProperty(key)) { shouldCopyProp = true // If the old prop is null or undefined, and the new prop is neither } else if ( (oldProp === null || oldProp === undefined) && newProp !== null && newProp !== undefined ) { shouldCopyProp = true // If both old and new are arrays } else if (Array.isArray(oldProp) && Array.isArray(newProp)) { shouldCopyProp = true } else if (_isObject(oldProp)) { shouldCopyProp = true } else if ( oldProp !== newProp && !Array.isArray(oldProp) && !Array.isArray(newProp) ) { shouldCopyProp = true } if (shouldCopyProp) { if (original.hasOwnProperty(key)) { original[key] = newProp } else { Vue.set(original, key, newProp) } } }) } export function getQueryInfo( params: Params = {}, response: Partial, 'limit' | 'skip'>> = {} ) { const query = params.query || {} const qid: string = params.qid || 'default' const $limit = response.limit !== null && response.limit !== undefined ? response.limit : query.$limit const $skip = response.skip !== null && response.skip !== undefined ? response.skip : query.$skip const queryParams = _omit(query, ['$limit', '$skip']) const queryId = stringify(queryParams) const pageParams = $limit !== undefined ? { $limit, $skip } : undefined const pageId = pageParams ? stringify(pageParams) : undefined return { qid, query, queryId, queryParams, pageParams, pageId, response: undefined, isOutdated: undefined as boolean | undefined } } export function getItemsFromQueryInfo(pagination, queryInfo, keyedById) { const { queryId, pageId } = queryInfo const queryLevel = pagination[queryId] const pageLevel = queryLevel && queryLevel[pageId] const ids = pageLevel && pageLevel.ids if (ids && ids.length) { return ids.map(id => keyedById[id]) } else { return [] } } export function makeNamespace(namespace, servicePath, nameStyle) { const nameStyles = { short: getShortName, path: getNameFromPath } return namespace || nameStyles[nameStyle](servicePath) } /** * Gets the service path or name from the service. The modelname is provided * to allow easier discovery if there's a problem. * @param service * @param modelName */ export function getServicePath(service: Service, Model: any) { if (!service.name && !service.path && !Model.servicePath) { throw new Error( `Service for model named ${Model.name} is missing a path or name property. The feathers adapter needs to be updated with a PR to expose this property. You can work around this by adding a static servicePath = passing a 'servicePath' attribute in the options: makeServicePlugin({servicePath: '/path/to/my/service'})` ) } return service.path || service.name || Model.servicePath } export function randomString(length) { let text = '' const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' for (let i = 0; i < length; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)) } return text } export function createRelatedInstance({ item, Model, idField, store }) { // Create store instances (if data contains an idField) const model = new Model(item) const id = getId(model, idField) const storedModel = store.state[model.constructor.namespace].keyedById[id] return { model, storedModel } } export function isBaseModelInstance(item) { const baseModels = Object.keys(models).map(alias => models[alias].BaseModel) return !!baseModels.find(BaseModel => { return item instanceof BaseModel }) } export function mergeWithAccessors( dest, source, blacklist = ['__isClone', '__ob__'] ) { const sourceProps = Object.getOwnPropertyNames(source) const destProps = Object.getOwnPropertyNames(dest) const sourceIsVueObservable = sourceProps.includes('__ob__') const destIsVueObservable = destProps.includes('__ob__') sourceProps.forEach(key => { const sourceDesc = Object.getOwnPropertyDescriptor(source, key) const destDesc = Object.getOwnPropertyDescriptor(dest, key) // if (Array.isArray(source[key]) && source[key].find(i => i.__ob__)) { // sourceIsVueObservable = true // } // if (Array.isArray(dest[key]) && dest[key].find(i => i.__ob__)) { // destIsVueObservable = true // } // This might have to be uncommented, but we'll try it this way, for now. // if (!sourceDesc.enumerable) { // return // } // If the destination is not writable, return. Also ignore blacklisted keys. // Must explicitly check if writable is false if ((destDesc && destDesc.writable === false) || blacklist.includes(key)) { return } // Handle Vue observable objects if (destIsVueObservable || sourceIsVueObservable) { const isObject = _isObject(source[key]) const isFeathersVuexInstance = isObject && !!( source[key].constructor.modelName || source[key].constructor.namespace ) // Do not use fastCopy directly on a feathers-vuex BaseModel instance to keep from breaking reactivity. if (isObject && !isFeathersVuexInstance) { try { dest[key] = fastCopy(source[key]) } catch (err) { if (!err.message.includes('getter')) { throw err } } } else { try { dest[key] = source[key] } catch (err) { if (!err.message.includes('getter')) { throw err } } } return } // Handle defining accessors if ( typeof sourceDesc.get === 'function' || typeof sourceDesc.set === 'function' ) { Object.defineProperty(dest, key, sourceDesc) return } // Do not attempt to overwrite a getter in the dest object if (destDesc && typeof destDesc.get === 'function') { return } // Assign values // Do not allow sharing of deeply-nested objects between instances // Potentially breaks accessors on nested data. Needs recursion if this is an issue let value if (_isObject(sourceDesc.value) && !isBaseModelInstance(sourceDesc.value)) { value = fastCopy(sourceDesc.value) } dest[key] = value || sourceDesc.value }) return dest } export function checkNamespace(namespace, item, debug) { if (!namespace && debug) { // eslint-disable-next-line no-console console.error( 'A `namespace` was not available on the Model for this item:', item, 'this can be caused by not passing the Model into the makeServicePlugin function' ) } return namespace !== null && namespace !== undefined } export function assignIfNotPresent(Model, props): void { for (const key in props) { if (!Model.hasOwnProperty(key)) { Model[key] = props[key] } } } ================================================ FILE: src/vue-plugin/vue-plugin.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import FeathersVuexFind from '../FeathersVuexFind' import FeathersVuexGet from '../FeathersVuexGet' import FeathersVuexFormWrapper from '../FeathersVuexFormWrapper' import FeathersVuexInputWrapper from '../FeathersVuexInputWrapper' import FeathersVuexPagination from '../FeathersVuexPagination' import FeathersVuexCount from '../FeathersVuexCount' import { globalModels } from '../service-module/global-models' import { GlobalModels } from '../service-module/types' // Augment global models onto VueConstructor and instance declare module 'vue/types/vue' { interface VueConstructor { $FeathersVuex: GlobalModels } interface Vue { $FeathersVuex: GlobalModels } } export const FeathersVuex = { install(Vue, options = { components: true }) { const shouldSetupComponents = options.components !== false Vue.$FeathersVuex = globalModels Vue.prototype.$FeathersVuex = globalModels if (shouldSetupComponents) { Vue.component('FeathersVuexFind', FeathersVuexFind) Vue.component('FeathersVuexGet', FeathersVuexGet) Vue.component('FeathersVuexFormWrapper', FeathersVuexFormWrapper) Vue.component('FeathersVuexInputWrapper', FeathersVuexInputWrapper) Vue.component('FeathersVuexPagination', FeathersVuexPagination) Vue.component('FeathersVuexCount', FeathersVuexCount) } } } ================================================ FILE: stories/.npmignore ================================================ *.stories.js ================================================ FILE: stories/FeathersVuexFormWrapper.stories.js ================================================ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import '../../assets/styles/tailwind.postcss' import FeathersVuexFormWrapper from '../src/FeathersVuexFormWrapper' import Readme from './README.md' import store from '../../store/store.dev' import { models } from 'feathers-vuex' export default { title: 'FeathersVuexFormWrapper', parameters: { component: FeathersVuexFormWrapper, readme: { sidebar: Readme } } } export const Basic = () => ({ components: { FeathersVuexFormWrapper }, data: () => ({ date: null, UserModel: models.api.User }), store, template: `
` }) ================================================ FILE: stories/FeathersVuexInputWrapper.stories.js ================================================ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import FeathersVuexInputWrapper from '../src/FeathersVuexInputWrapper.vue' import { makeModel } from '@rovit/test-model' const User = makeModel() const user = new User({ _id: 1, email: 'marshall@rovit.com', carColor: '#FFF' }) export default { title: 'FeathersVuexInputWrapper', component: FeathersVuexInputWrapper } export const basic = () => ({ components: { FeathersVuexInputWrapper }, data: () => ({ user }), methods: { save({ clone, data }) { const user = clone.commit() user.patch(data) } }, template: `
{{user}}
` }) export const handlerAsPromise = () => ({ components: { FeathersVuexInputWrapper }, data: () => ({ user }), methods: { async save({ clone, data }) { const user = clone.commit() return user.patch(data) } }, template: `
{{user}}
` }) export const multipleOnDistinctProperties = () => ({ components: { FeathersVuexInputWrapper }, data: () => ({ user }), methods: { async save({ event, clone, prop, data }) { const user = clone.commit() return user.patch(data) } }, template: `
{{user}}
` }) export const noInputInSlot = () => ({ components: { FeathersVuexInputWrapper }, data: () => ({ user }), methods: { async save({ clone, data }) { const user = clone.commit() user.patch(data) } }, template: `
` }) ================================================ FILE: test/auth-module/actions.test.js ================================================ import assert from 'chai/chai' import setupVuexAuth from '~/src/auth-module/auth-module' import setupVuexService from '~/src/service-module/service-module' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import Vuex, { mapActions } from 'vuex' import memory from 'feathers-memory' const options = {} const globalModels = {} const auth = setupVuexAuth(feathersClient, options, globalModels) const service = setupVuexService(feathersClient, options, globalModels) const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjAsImV4cCI6OTk5OTk5OTk5OTk5OX0.zmvEm8w142xGI7CbUsnvVGZk_hrVE1KEjzDt80LSW50' describe('Auth Module - Actions', () => { it('Authenticate', done => { const store = new Vuex.Store({ plugins: [auth()] }) feathersClient.use('authentication', { create(data) { return Promise.resolve({ accessToken }) } }) const authState = store.state.auth const actions = mapActions('auth', ['authenticate']) assert(authState.accessToken === null) assert(authState.errorOnAuthenticate === null) assert(authState.errorOnLogout === null) assert(authState.isAuthenticatePending === false) assert(authState.isLogoutPending === false) assert(authState.payload === null) const request = { strategy: 'local', email: 'test', password: 'test' } actions.authenticate.call({ $store: store }, request).then(response => { assert(authState.accessToken === response.accessToken) assert(authState.errorOnAuthenticate === null) assert(authState.errorOnLogout === null) assert(authState.isAuthenticatePending === false) assert(authState.isLogoutPending === false) const expectedPayload = { userId: 0, exp: 9999999999999 } assert.deepEqual(authState.payload, expectedPayload) done() }) // Make sure proper state changes occurred before response assert(authState.accessToken === null) assert(authState.errorOnAuthenticate === null) assert(authState.errorOnLogout === null) assert(authState.isAuthenticatePending === true) assert(authState.isLogoutPending === false) assert(authState.payload === null) }) it('Logout', done => { const store = new Vuex.Store({ plugins: [auth()] }) feathersClient.use('authentication', { create(data) { return Promise.resolve({ accessToken }) } }) const authState = store.state.auth const actions = mapActions('auth', ['authenticate', 'logout']) const request = { strategy: 'local', email: 'test', password: 'test' } actions.authenticate.call({ $store: store }, request).then(authResponse => { actions.logout.call({ $store: store }).then(response => { assert(authState.accessToken === null) assert(authState.errorOnAuthenticate === null) assert(authState.errorOnLogout === null) assert(authState.isAuthenticatePending === false) assert(authState.isLogoutPending === false) assert(authState.payload === null) done() }) }) }) it('Authenticate with userService config option', done => { feathersClient.use('authentication', { create(data) { return Promise.resolve({ accessToken }) } }) feathersClient.use( 'users', memory({ store: { 0: { id: 0, email: 'test@test.com' } } }) ) const store = new Vuex.Store({ plugins: [auth({ userService: 'users' }), service('users')] }) const authState = store.state.auth const actions = mapActions('auth', ['authenticate']) assert(authState.user === null) const request = { strategy: 'local', email: 'test', password: 'test' } actions.authenticate .call({ $store: store }, request) .then(response => { const expectedUser = { id: 0, email: 'test@test.com' } assert.deepEqual(authState.user, expectedUser) done() }) .catch(error => { assert(!error, error) done() }) }) }) ================================================ FILE: test/auth-module/auth-module.test.ts ================================================ /* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { assert } from 'chai' import feathersVuex from '../../src/index' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import Vuex from 'vuex' import { isEmpty } from 'lodash' const { makeAuthPlugin, makeServicePlugin, BaseModel } = feathersVuex( feathersClient, { serverAlias: 'api' } ) interface CustomStore { state: any auth: any authentication?: any users?: any } function makeContext() { class User extends BaseModel { constructor(data, options) { super(data, options) } static modelName = 'User' static instanceDefaults() { return { email: '', password: '' } } } const servicePath = 'users' const usersPlugin = makeServicePlugin({ Model: User, service: feathersClient.service(servicePath), servicePath }) const authPlugin = makeAuthPlugin({ userService: 'users' }) const store = new Vuex.Store({ plugins: [authPlugin, usersPlugin] }) return { User, usersPlugin, authPlugin, BaseModel, store } } describe('Auth Module', () => { describe('Configuration', () => { it('has default auth namespace', () => { const { store } = makeContext() const authState = Object.assign({}, store.state.auth) const expectedCustomStore = { accessToken: null, entityIdField: 'userId', errorOnAuthenticate: null, errorOnLogout: null, isAuthenticatePending: false, isLogoutPending: false, payload: null, responseEntityField: 'user', serverAlias: 'api', user: null, userService: 'users' } assert.deepEqual(authState, expectedCustomStore, 'has the default state') }) it('can customize the namespace', function() { const store = new Vuex.Store({ plugins: [makeAuthPlugin({ namespace: 'authentication' })] }) assert(store.state.authentication, 'the custom namespace was used') }) }) describe('Customizing Auth Store', function() { it('allows adding custom state', function() { const customState = { test: true, test2: { test: true } } const store = new Vuex.Store({ plugins: [makeAuthPlugin({ state: customState })] }) assert(store.state.auth.test === true, 'added custom state') assert(store.state.auth.test2.test === true, 'added custom state') }) it('allows custom mutations', function() { const state = { test: true } const customMutations = { setTestToFalse(state) { state.test = false } } const store = new Vuex.Store({ plugins: [makeAuthPlugin({ state, mutations: customMutations })] }) store.commit('auth/setTestToFalse') assert( store.state.auth.test === false, 'the custom state was modified by the custom mutation' ) }) it('has a user && isAuthenticated getter when there is a userService attribute', function() { const store = new Vuex.Store({ state: { state: {}, auth: {}, users: { idField: 'id', keyedById: { 1: { id: 1, name: 'Marshall' } } } }, plugins: [ makeAuthPlugin({ state: { user: { id: 1 } }, userService: 'users' }) ] }) const user = store.getters['auth/user'] const isAuthenticated = store.getters['auth/isAuthenticated'] assert(user.name === 'Marshall', 'Got the user from the users store.') assert(isAuthenticated, 'isAuthenticated') }) it('getters show not authenticated when there is no user', function() { const store = new Vuex.Store({ state: { state: {}, auth: {}, users: { idField: 'id', keyedById: {} } }, plugins: [ makeAuthPlugin({ state: {}, userService: 'users' }) ] }) const user = store.getters['auth/user'] const isAuthenticated = store.getters['auth/isAuthenticated'] assert(user === null, 'user getter returned null as expected') assert(!isAuthenticated, 'not authenticated') }) it('allows custom getters', function() { const customGetters = { oneTwoThree() { return 123 } } const store = new Vuex.Store({ plugins: [makeAuthPlugin({ getters: customGetters })] }) assert( store.getters['auth/oneTwoThree'] === 123, 'the custom getter was available' ) }) it('allows adding custom actions', function() { const config = { state: { isTrue: false }, mutations: { setToTrue(state) { state.isTrue = true } }, actions: { trigger(context) { context.commit('setToTrue') } } } const store = new Vuex.Store({ plugins: [makeAuthPlugin(config)] }) store.dispatch('auth/trigger') assert(store.state.auth.isTrue === true, 'the custom action was run') }) }) it('Calls auth service without params', async function() { let receivedData = null let receivedParams = null feathersClient.use('authentication', { create(data, params) { receivedData = data receivedParams = params return Promise.resolve({ accessToken: 'jg54jh2gj6fgh734j5h4j25jbh' }) } }) const { store } = makeContext() const request = { strategy: 'local', email: 'test', password: 'test' } await store.dispatch('auth/authenticate', request) assert(receivedData, 'got data') assert(receivedData.strategy === 'local', 'got strategy') assert(receivedData.email === 'test', 'got email') assert(receivedData.password === 'test', 'got password') assert(receivedParams && isEmpty(receivedParams), 'empty params') }) it('Calls auth service with params', async function() { let receivedParams = null feathersClient.use('authentication', { create(data, params) { receivedParams = params return Promise.resolve({ accessToken: 'jg54jh2gj6fgh734j5h4j25jbh' }) } }) const { store } = makeContext() const request = { strategy: 'local', email: 'test', password: 'test' } const customParams = { theAnswer: 42 } await store.dispatch('auth/authenticate', [request, customParams]) assert(receivedParams && receivedParams.theAnswer === 42, 'got params') }) }) ================================================ FILE: test/auth.test.js ================================================ import { assert } from 'chai' import feathersVuexAuth, { reducer } from '../src/auth' import * as actionTypes from '../src/action-types' import './server' import { makeFeathersRestClient } from './feathers-client' describe('feathers-vuex:auth', () => { it('is CommonJS compatible', () => { assert(typeof require('../lib/auth').default === 'function') }) it('basic functionality', () => { assert(typeof feathersVuexAuth === 'function', 'It worked') }) it('throws an error if the auth plugin is missing', () => { const app = {} const store = {} const plugin = feathersVuexAuth(store).bind(app) assert.throws( plugin, 'You must first register the @feathersjs/authentication-client plugin' ) }) it('returns the app, is chainable', () => { const app = { authenticate() {} } const store = {} const returnValue = feathersVuexAuth(store).bind(app)() assert(returnValue === app) }) it('replaces the original authenticate function', () => { const feathersClient = makeFeathersRestClient() const oldAuthenticate = feathersClient.authenticate const store = {} feathersClient.configure(feathersVuexAuth(store)) assert(oldAuthenticate !== feathersClient.authenticate) }) it('dispatches actions to the store.', done => { const feathersClient = makeFeathersRestClient() const fakeStore = { dispatch(action) { switch (action.type) { case actionTypes.FEATHERS_AUTH_REQUEST: assert(action.payload.test || action.payload.accessToken) break case actionTypes.FEATHERS_AUTH_SUCCESS: assert(action.data) break case actionTypes.FEATHERS_AUTH_FAILURE: assert(action.error) done() break case actionTypes.FEATHERS_AUTH_LOGOUT: assert(action) break } } } feathersClient.configure(feathersVuexAuth(fakeStore)) try { feathersClient .authenticate({ test: true }) .then(response => { feathersClient.logout() return response }) .catch(error => { assert(error.className === 'not-authenticated') }) } catch (err) {} try { feathersClient.authenticate({ strategy: 'jwt', accessToken: 'q34twershtdyfhgmj' }) } catch (err) { // eslint-disable-next-line no-console console.log(err) } }) }) describe('feathers-vuex:auth - Reducer', () => { it('Has defaults', () => { const state = undefined const defaultState = { isPending: false, isError: false, isSignedIn: false, accessToken: null, error: undefined } const newState = reducer(state, {}) assert.deepEqual(newState, defaultState) }) it(`Responds to ${actionTypes.FEATHERS_AUTH_REQUEST}`, () => { const state = undefined const action = { type: actionTypes.FEATHERS_AUTH_REQUEST, payload: { strategy: 'jwt', accessToken: 'evh8vq2pj' } } const expectedState = { isPending: true, isError: false, isSignedIn: false, accessToken: null, error: undefined } const newState = reducer(state, action) assert.deepEqual(newState, expectedState) }) it(`Responds to ${actionTypes.FEATHERS_AUTH_SUCCESS}`, () => { const state = undefined const accessToken = 'evh8vq2pj' const action = { type: actionTypes.FEATHERS_AUTH_SUCCESS, data: { accessToken } } const expectedState = { isPending: false, isError: false, isSignedIn: true, accessToken: accessToken, error: undefined } const newState = reducer(state, action) assert.deepEqual(newState, expectedState) }) it(`Responds to ${actionTypes.FEATHERS_AUTH_FAILURE}`, () => { const state = undefined const error = 'Unauthorized' const action = { type: actionTypes.FEATHERS_AUTH_FAILURE, error } const expectedState = { isPending: false, isError: true, isSignedIn: false, accessToken: null, error } const newState = reducer(state, action) assert.deepEqual(newState, expectedState) }) it(`Responds to ${actionTypes.FEATHERS_AUTH_LOGOUT}`, () => { const state = undefined const action = { type: actionTypes.FEATHERS_AUTH_LOGOUT } const expectedState = { isPending: false, isError: false, isSignedIn: false, accessToken: null, error: undefined } const newState = reducer(state, action) assert.deepEqual(newState, expectedState) }) }) ================================================ FILE: test/fixtures/fake-data.js ================================================ export default { users: [ { email: 'Richie_Cartwright97@hotmail.com', password: '$2a$13$7BBrmdiWTtm3GbD/KHdBOOjTqricfqPI06j/Wg/rsDqpnEza00bHG', _id: '5c6cafbf1babb758d2975407' }, { email: 'Kristofer25@hotmail.com', password: '$2a$13$d5f0aRKwHwK9NFbJfYG4Ke4gZU39dOa5jGqzbFtuWpBll4mLs/Ewu', _id: '5c6cafbf1babb758d2975408' }, { email: 'Margarett.Kozey@hotmail.com', password: '$2a$13$t6cmWh4zjpfVoTqGUtyB1O/zLZe99uYhNNEQQi7cMEpTtyzABeIt.', _id: '5c6cafbf1babb758d2975409' }, { email: 'Pinkie_Braun98@yahoo.com', password: '$2a$13$.HrrqdURnxWoBljdifveveC4eF6RXbP4qb2WEK40TIJ5D8FxSNe6O', _id: '5c6cafbf1babb758d297540a' }, { email: 'Isabelle26@yahoo.com', password: '$2a$13$J0x0WhGjGQVyOs15zGmOluN5oj681xze4ARaKD8I05hVEwvO8KUfi', _id: '5c6cafbf1babb758d297540b' }, { email: 'Kay92@yahoo.com', password: '$2a$13$jmPxZxZKW0E3XgEyUEqyEeY5S.0PlvHnbTPR/T8WKbm192macrpSq', _id: '5c6cafbf1babb758d297540c' } ], transactions: [ { _id: '5c6cafbf1babb758d297540d', name: 'Hahn, Dare and Turner', type: 'deposit', amount: 589.57, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b3' }, { _id: '5c6cafbf1babb758d297540e', name: 'Schulist, Abbott and McClure', type: 'invoice', amount: 206, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754c4' }, { _id: '5c6cafbf1babb758d297540f', name: 'Simonis, Waters and Turcotte', type: 'deposit', amount: 384.59, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754ba' }, { _id: '5c6cafbf1babb758d2975410', name: 'Buckridge - Steuber', type: 'invoice', amount: 445.16, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754ba' }, { _id: '5c6cafbf1babb758d2975411', name: 'Reilly, Hahn and Murray', type: 'invoice', amount: 104.58, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754bf' }, { _id: '5c6cafbf1babb758d2975412', name: 'Heaney LLC', type: 'invoice', amount: 551.98, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b8' }, { _id: '5c6cafbf1babb758d2975413', name: 'Harris LLC', type: 'payment', amount: 797.64, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754c1' }, { _id: '5c6cafbf1babb758d2975414', name: 'Ruecker and Sons', type: 'deposit', amount: 104.17, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754b1' }, { _id: '5c6cafbf1babb758d2975415', name: 'Walter, DuBuque and Sipes', type: 'withdrawal', amount: 724.42, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754c2' }, { _id: '5c6cafbf1babb758d2975416', name: 'Tremblay LLC', type: 'payment', amount: 721.65, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754c5' }, { _id: '5c6cafbf1babb758d2975417', name: 'Quitzon, Hoppe and Bayer', type: 'withdrawal', amount: 929.01, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754ae' }, { _id: '5c6cafbf1babb758d2975418', name: 'Okuneva - McClure', type: 'withdrawal', amount: 178.02, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754c2' }, { _id: '5c6cafbf1babb758d2975419', name: 'Berge LLC', type: 'payment', amount: 365.29, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d297541a', name: 'Bode - McLaughlin', type: 'deposit', amount: 682.29, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754b8' }, { _id: '5c6cafbf1babb758d297541b', name: 'Labadie - Kub', type: 'deposit', amount: 557.18, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b5' }, { _id: '5c6cafbf1babb758d297541c', name: 'Torphy LLC', type: 'withdrawal', amount: 899.76, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754be' }, { _id: '5c6cafbf1babb758d297541d', name: 'Torp LLC', type: 'deposit', amount: 326.11, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b5' }, { _id: '5c6cafbf1babb758d297541e', name: 'Jenkins, Moen and Jast', type: 'invoice', amount: 143.48, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754c4' }, { _id: '5c6cafbf1babb758d297541f', name: 'Towne, Conn and Swaniawski', type: 'deposit', amount: 404.22, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b2' }, { _id: '5c6cafbf1babb758d2975420', name: 'Weissnat Group', type: 'payment', amount: 226.77, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b2' }, { _id: '5c6cafbf1babb758d2975421', name: 'Boehm, Stehr and Rolfson', type: 'withdrawal', amount: 862.48, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754c3' }, { _id: '5c6cafbf1babb758d2975422', name: 'Schumm, Bruen and Upton', type: 'withdrawal', amount: 191.31, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754ad' }, { _id: '5c6cafbf1babb758d2975423', name: 'Hilpert - Bogan', type: 'invoice', amount: 415.73, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754bf' }, { _id: '5c6cafbf1babb758d2975424', name: 'Ondricka, Koch and Adams', type: 'deposit', amount: 131.94, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754bb' }, { _id: '5c6cafbf1babb758d2975425', name: 'Lockman LLC', type: 'deposit', amount: 965.39, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754c5' }, { _id: '5c6cafbf1babb758d2975426', name: 'Tremblay LLC', type: 'deposit', amount: 43.32, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b4' }, { _id: '5c6cafbf1babb758d2975427', name: 'Block Group', type: 'withdrawal', amount: 935.48, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754c3' }, { _id: '5c6cafbf1babb758d2975428', name: 'Marquardt Inc', type: 'deposit', amount: 279.73, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754c5' }, { _id: '5c6cafbf1babb758d2975429', name: 'Littel LLC', type: 'payment', amount: 366.58, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754c2' }, { _id: '5c6cafbf1babb758d297542a', name: 'Kuvalis LLC', type: 'deposit', amount: 641.81, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b6' }, { _id: '5c6cafbf1babb758d297542b', name: 'Howell - Jakubowski', type: 'payment', amount: 239.4, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b3' }, { _id: '5c6cafbf1babb758d297542c', name: 'Quigley - Mann', type: 'invoice', amount: 948.68, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754c3' }, { _id: '5c6cafbf1babb758d297542d', name: "O'Reilly - O'Hara", type: 'payment', amount: 42.22, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754b1' }, { _id: '5c6cafbf1babb758d297542e', name: 'Schuster, Heller and Jenkins', type: 'invoice', amount: 660.17, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754b9' }, { _id: '5c6cafbf1babb758d297542f', name: 'Skiles - Wolff', type: 'payment', amount: 492.12, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b4' }, { _id: '5c6cafbf1babb758d2975430', name: 'Bauch - Leffler', type: 'deposit', amount: 851.64, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754bc' }, { _id: '5c6cafbf1babb758d2975431', name: 'Pacocha - Morissette', type: 'deposit', amount: 817.33, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754ae' }, { _id: '5c6cafbf1babb758d2975432', name: 'Rutherford Group', type: 'payment', amount: 536.97, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b0' }, { _id: '5c6cafbf1babb758d2975433', name: 'Labadie, Schaefer and Dietrich', type: 'deposit', amount: 470.89, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754af' }, { _id: '5c6cafbf1babb758d2975434', name: 'Weber - Schimmel', type: 'withdrawal', amount: 598.67, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754c3' }, { _id: '5c6cafbf1babb758d2975435', name: 'Ritchie, Dach and Oberbrunner', type: 'payment', amount: 193.49, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b6' }, { _id: '5c6cafbf1babb758d2975436', name: 'Strosin Group', type: 'invoice', amount: 957.26, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d2975437', name: 'Stark - Rogahn', type: 'payment', amount: 634.26, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b9' }, { _id: '5c6cafbf1babb758d2975438', name: 'Emard and Sons', type: 'payment', amount: 985.95, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754b6' }, { _id: '5c6cafbf1babb758d2975439', name: 'Nienow - Kuphal', type: 'withdrawal', amount: 994.74, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754be' }, { _id: '5c6cafbf1babb758d297543a', name: 'Hartmann and Sons', type: 'payment', amount: 719.73, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754be' }, { _id: '5c6cafbf1babb758d297543b', name: 'Weimann - Gleichner', type: 'payment', amount: 209.88, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754be' }, { _id: '5c6cafbf1babb758d297543c', name: 'Hirthe Inc', type: 'deposit', amount: 371.33, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b8' }, { _id: '5c6cafbf1babb758d297543d', name: 'Schaden - Wisoky', type: 'payment', amount: 48.39, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754c3' }, { _id: '5c6cafbf1babb758d297543e', name: 'Stroman Inc', type: 'invoice', amount: 306.6, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754ae' }, { _id: '5c6cafbf1babb758d297543f', name: 'Robel Group', type: 'invoice', amount: 815.09, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754c0' }, { _id: '5c6cafbf1babb758d2975440', name: 'Dach Inc', type: 'payment', amount: 644.27, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b9' }, { _id: '5c6cafbf1babb758d2975441', name: 'VonRueden and Sons', type: 'payment', amount: 917.19, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b9' }, { _id: '5c6cafbf1babb758d2975442', name: 'Nikolaus, Abernathy and Jakubowski', type: 'withdrawal', amount: 299.81, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754bb' }, { _id: '5c6cafbf1babb758d2975443', name: 'Turcotte and Sons', type: 'payment', amount: 574.29, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754bf' }, { _id: '5c6cafbf1babb758d2975444', name: 'Okuneva - Walker', type: 'withdrawal', amount: 590.38, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754bc' }, { _id: '5c6cafbf1babb758d2975445', name: 'Abernathy, Wilkinson and Watsica', type: 'payment', amount: 866.38, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d2975446', name: 'Williamson, Lemke and Blanda', type: 'invoice', amount: 584.63, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754bf' }, { _id: '5c6cafbf1babb758d2975447', name: 'Metz Inc', type: 'deposit', amount: 54.24, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754bd' }, { _id: '5c6cafbf1babb758d2975448', name: 'Block LLC', type: 'payment', amount: 491.09, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754c0' }, { _id: '5c6cafbf1babb758d2975449', name: 'Grady, Schultz and Padberg', type: 'deposit', amount: 551.38, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754c4' }, { _id: '5c6cafbf1babb758d297544a', name: 'Bradtke - Botsford', type: 'payment', amount: 674.32, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b5' }, { _id: '5c6cafbf1babb758d297544b', name: 'Schultz, Graham and Herzog', type: 'payment', amount: 79.16, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754c1' }, { _id: '5c6cafbf1babb758d297544c', name: 'Beer, Boyer and Bergstrom', type: 'payment', amount: 34.12, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754c2' }, { _id: '5c6cafbf1babb758d297544d', name: 'Monahan - Crona', type: 'deposit', amount: 470.31, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754af' }, { _id: '5c6cafbf1babb758d297544e', name: 'Morar and Sons', type: 'deposit', amount: 518.19, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b4' }, { _id: '5c6cafbf1babb758d297544f', name: 'Stamm, Steuber and Doyle', type: 'payment', amount: 790.31, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b0' }, { _id: '5c6cafbf1babb758d2975450', name: 'Gottlieb, Harber and Wehner', type: 'invoice', amount: 153.32, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b1' }, { _id: '5c6cafbf1babb758d2975451', name: 'Kuhic - Paucek', type: 'payment', amount: 587.14, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754b6' }, { _id: '5c6cafbf1babb758d2975452', name: "O'Keefe, Rice and Crooks", type: 'deposit', amount: 549.83, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754c2' }, { _id: '5c6cafbf1babb758d2975453', name: 'Reilly LLC', type: 'payment', amount: 10.12, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b5' }, { _id: '5c6cafbf1babb758d2975454', name: 'Walsh Group', type: 'payment', amount: 118.07, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b3' }, { _id: '5c6cafbf1babb758d2975455', name: 'Thompson Group', type: 'payment', amount: 452.88, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b6' }, { _id: '5c6cafbf1babb758d2975456', name: 'Abbott - Conn', type: 'withdrawal', amount: 930.52, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b0' }, { _id: '5c6cafbf1babb758d2975457', name: 'Walker, Boehm and Pouros', type: 'withdrawal', amount: 325.91, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b0' }, { _id: '5c6cafbf1babb758d2975458', name: 'Collier - Schamberger', type: 'withdrawal', amount: 579.84, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754b8' }, { _id: '5c6cafbf1babb758d2975459', name: 'Tremblay - Auer', type: 'invoice', amount: 245.28, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754bd' }, { _id: '5c6cafbf1babb758d297545a', name: 'Luettgen, Moore and Schroeder', type: 'invoice', amount: 22.18, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b4' }, { _id: '5c6cafbf1babb758d297545b', name: 'Herzog Inc', type: 'withdrawal', amount: 294.14, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754c5' }, { _id: '5c6cafbf1babb758d297545c', name: 'Kutch, Reynolds and Ankunding', type: 'invoice', amount: 858.23, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754c0' }, { _id: '5c6cafbf1babb758d297545d', name: 'Hagenes - Thiel', type: 'invoice', amount: 213.87, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754bf' }, { _id: '5c6cafbf1babb758d297545e', name: 'Hoppe - Raynor', type: 'invoice', amount: 733.43, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754c2' }, { _id: '5c6cafbf1babb758d297545f', name: 'Lesch, Little and Nicolas', type: 'deposit', amount: 154.18, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754bd' }, { _id: '5c6cafbf1babb758d2975460', name: 'Hintz LLC', type: 'withdrawal', amount: 173.48, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754c5' }, { _id: '5c6cafbf1babb758d2975461', name: 'Stamm and Sons', type: 'withdrawal', amount: 343.16, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b1' }, { _id: '5c6cafbf1babb758d2975462', name: 'Labadie - Weimann', type: 'withdrawal', amount: 812.54, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754c0' }, { _id: '5c6cafbf1babb758d2975463', name: 'Hegmann Inc', type: 'withdrawal', amount: 843.29, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b2' }, { _id: '5c6cafbf1babb758d2975464', name: 'Walker - Bruen', type: 'invoice', amount: 122.99, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754bf' }, { _id: '5c6cafbf1babb758d2975465', name: 'Erdman - Streich', type: 'withdrawal', amount: 810.5, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754b8' }, { _id: '5c6cafbf1babb758d2975466', name: 'Swift - Wisozk', type: 'withdrawal', amount: 356.07, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754bc' }, { _id: '5c6cafbf1babb758d2975467', name: 'Schroeder, Abernathy and Miller', type: 'payment', amount: 588.23, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754bb' }, { _id: '5c6cafbf1babb758d2975468', name: 'Hansen Inc', type: 'payment', amount: 978.32, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754c2' }, { _id: '5c6cafbf1babb758d2975469', name: 'Reilly, Kautzer and Bode', type: 'invoice', amount: 874.7, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d297546a', name: 'Pagac, Kohler and Johnston', type: 'invoice', amount: 579.61, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754be' }, { _id: '5c6cafbf1babb758d297546b', name: 'Stehr Group', type: 'invoice', amount: 673.81, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754c1' }, { _id: '5c6cafbf1babb758d297546c', name: 'McCullough - Harvey', type: 'invoice', amount: 506.83, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754b3' }, { _id: '5c6cafbf1babb758d297546d', name: 'Brekke, Davis and Russel', type: 'invoice', amount: 363.19, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754c1' }, { _id: '5c6cafbf1babb758d297546e', name: 'Kutch Group', type: 'payment', amount: 266.73, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b4' }, { _id: '5c6cafbf1babb758d297546f', name: 'Funk, McLaughlin and Hartmann', type: 'invoice', amount: 610.16, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754b9' }, { _id: '5c6cafbf1babb758d2975470', name: 'Pouros - Lind', type: 'payment', amount: 673.94, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b2' }, { _id: '5c6cafbf1babb758d2975471', name: 'Towne - Schmitt', type: 'withdrawal', amount: 285.58, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754bc' }, { _id: '5c6cafbf1babb758d2975472', name: 'Prohaska - Schroeder', type: 'withdrawal', amount: 331.94, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754ae' }, { _id: '5c6cafbf1babb758d2975473', name: 'Harber and Sons', type: 'payment', amount: 526.65, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b5' }, { _id: '5c6cafbf1babb758d2975474', name: 'Doyle - Huel', type: 'payment', amount: 582.6, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754c5' }, { _id: '5c6cafbf1babb758d2975475', name: 'Goyette, Collins and Greenholt', type: 'payment', amount: 961.47, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754b2' }, { _id: '5c6cafbf1babb758d2975476', name: 'Purdy - Corkery', type: 'withdrawal', amount: 742.07, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754bb' }, { _id: '5c6cafbf1babb758d2975477', name: 'Baumbach, Gutkowski and Hauck', type: 'payment', amount: 67.98, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754b1' }, { _id: '5c6cafbf1babb758d2975478', name: 'Schuster - Jast', type: 'invoice', amount: 952.12, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754b0' }, { _id: '5c6cafbf1babb758d2975479', name: 'Cassin LLC', type: 'withdrawal', amount: 491.26, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754c1' }, { _id: '5c6cafbf1babb758d297547a', name: 'Koelpin - Rice', type: 'deposit', amount: 148.37, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754ae' }, { _id: '5c6cafbf1babb758d297547b', name: 'Moen LLC', type: 'deposit', amount: 666.99, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754be' }, { _id: '5c6cafbf1babb758d297547c', name: 'Wiza LLC', type: 'deposit', amount: 492.08, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754ba' }, { _id: '5c6cafbf1babb758d297547d', name: 'Sanford, Auer and Lueilwitz', type: 'invoice', amount: 677.3, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b0' }, { _id: '5c6cafbf1babb758d297547e', name: 'Schroeder LLC', type: 'withdrawal', amount: 842.7, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d297547f', name: 'Carter - Schoen', type: 'invoice', amount: 987.15, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754bb' }, { _id: '5c6cafbf1babb758d2975480', name: 'Hills Inc', type: 'invoice', amount: 70.08, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754b5' }, { _id: '5c6cafbf1babb758d2975481', name: 'Carter - Morar', type: 'invoice', amount: 837.2, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754bd' }, { _id: '5c6cafbf1babb758d2975482', name: 'Lubowitz, Powlowski and Leffler', type: 'invoice', amount: 323.43, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b6' }, { _id: '5c6cafbf1babb758d2975483', name: 'Haag, Larkin and Corwin', type: 'deposit', amount: 830.43, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754c1' }, { _id: '5c6cafbf1babb758d2975484', name: 'Dibbert, Gerlach and Schneider', type: 'invoice', amount: 366.91, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754af' }, { _id: '5c6cafbf1babb758d2975485', name: 'Purdy LLC', type: 'invoice', amount: 872.18, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754c1' }, { _id: '5c6cafbf1babb758d2975486', name: 'Pagac, Wyman and Stanton', type: 'withdrawal', amount: 545.68, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754ae' }, { _id: '5c6cafbf1babb758d2975487', name: 'Quigley - Littel', type: 'deposit', amount: 256.81, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754c0' }, { _id: '5c6cafbf1babb758d2975488', name: 'Gleason - Goldner', type: 'deposit', amount: 906.86, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754c4' }, { _id: '5c6cafbf1babb758d2975489', name: 'Daugherty Inc', type: 'invoice', amount: 78.18, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b0' }, { _id: '5c6cafbf1babb758d297548a', name: 'Ratke - Thiel', type: 'invoice', amount: 735.66, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754c1' }, { _id: '5c6cafbf1babb758d297548b', name: 'Fisher and Sons', type: 'deposit', amount: 260.56, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754c5' }, { _id: '5c6cafbf1babb758d297548c', name: 'Aufderhar - Ernser', type: 'payment', amount: 325.62, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754b2' }, { _id: '5c6cafbf1babb758d297548d', name: 'Bayer - Jacobs', type: 'withdrawal', amount: 364.59, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b5' }, { _id: '5c6cafbf1babb758d297548e', name: 'Streich - Tremblay', type: 'withdrawal', amount: 929.34, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754bf' }, { _id: '5c6cafbf1babb758d297548f', name: 'Satterfield - Kuhlman', type: 'deposit', amount: 476.89, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d2975490', name: 'Schneider Group', type: 'invoice', amount: 555.79, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b8' }, { _id: '5c6cafbf1babb758d2975491', name: 'Hane LLC', type: 'invoice', amount: 193.13, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754c1' }, { _id: '5c6cafbf1babb758d2975492', name: 'Leffler - Herzog', type: 'invoice', amount: 590.05, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754c5' }, { _id: '5c6cafbf1babb758d2975493', name: 'Luettgen LLC', type: 'invoice', amount: 258.9, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d2975494', name: 'Mosciski, Welch and Pfeffer', type: 'invoice', amount: 407.29, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b3' }, { _id: '5c6cafbf1babb758d2975495', name: 'DuBuque and Sons', type: 'invoice', amount: 444.08, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d2975496', name: 'Tromp, Harber and Reichel', type: 'deposit', amount: 591.37, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754bb' }, { _id: '5c6cafbf1babb758d2975497', name: 'Mayer and Sons', type: 'payment', amount: 2.32, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754b8' }, { _id: '5c6cafbf1babb758d2975498', name: 'Larson, Schmeler and Oberbrunner', type: 'payment', amount: 896.93, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754ae' }, { _id: '5c6cafbf1babb758d2975499', name: 'Schaden - Becker', type: 'deposit', amount: 510.06, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754ae' }, { _id: '5c6cafbf1babb758d297549a', name: 'Bailey, Kshlerin and Powlowski', type: 'withdrawal', amount: 97.7, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b6' }, { _id: '5c6cafbf1babb758d297549b', name: 'Wisoky LLC', type: 'deposit', amount: 931.21, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754bc' }, { _id: '5c6cafbf1babb758d297549c', name: 'Mayert - Morissette', type: 'withdrawal', amount: 610.77, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754bc' }, { _id: '5c6cafbf1babb758d297549d', name: 'Hammes Inc', type: 'withdrawal', amount: 74.11, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b4' }, { _id: '5c6cafbf1babb758d297549e', name: 'Gleichner and Sons', type: 'withdrawal', amount: 983.35, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d297549f', name: "Champlin - O'Hara", type: 'withdrawal', amount: 247.82, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754bd' }, { _id: '5c6cafbf1babb758d29754a0', name: 'Legros - Schuster', type: 'deposit', amount: 821.05, userId: '5c6cafbf1babb758d297540b', accountId: '5c6cafbf1babb758d29754be' }, { _id: '5c6cafbf1babb758d29754a1', name: 'Ritchie - Treutel', type: 'invoice', amount: 157.82, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754bd' }, { _id: '5c6cafbf1babb758d29754a2', name: 'Hyatt, Kuphal and Stanton', type: 'deposit', amount: 711.48, userId: '5c6cafbf1babb758d297540c', accountId: '5c6cafbf1babb758d29754c3' }, { _id: '5c6cafbf1babb758d29754a3', name: 'Stamm - Leffler', type: 'invoice', amount: 89.05, userId: '5c6cafbf1babb758d2975407', accountId: '5c6cafbf1babb758d29754b9' }, { _id: '5c6cafbf1babb758d29754a4', name: 'Bailey, VonRueden and Lesch', type: 'withdrawal', amount: 378.25, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754ba' }, { _id: '5c6cafbf1babb758d29754a5', name: 'Osinski, Gleason and Predovic', type: 'withdrawal', amount: 582.79, userId: '5c6cafbf1babb758d297540a', accountId: '5c6cafbf1babb758d29754b2' }, { _id: '5c6cafbf1babb758d29754a6', name: 'Purdy LLC', type: 'invoice', amount: 633.06, userId: '5c6cafbf1babb758d2975408', accountId: '5c6cafbf1babb758d29754b7' }, { _id: '5c6cafbf1babb758d29754a7', name: 'Lowe and Sons', type: 'withdrawal', amount: 632.33, userId: '5c6cafbf1babb758d2975409', accountId: '5c6cafbf1babb758d29754b9' } ], config: [ { label: 'ipsum ea elit', description: 'exercitation ut incididunt irure', tags: [ 'veniam consequat commodo', 'dolor in officia Duis sint', 'eu consectetur non cupidatat', 'ut deserunt' ], name: 'commodo consectetur ad sunt', value: 'nulla', _id: '5c6cafbf1babb758d29754a8' }, { label: 'labore velit Ut adipisic', description: 'aute non', tags: [ 'est voluptate deserunt', 'minim nisi fugiat in elit', 'dolor in', 'nostrud' ], name: 'Excepteur veniam velit enim Ut', value: 'exercitation', _id: '5c6cafbf1babb758d29754a9' }, { label: 'labore officia', description: 'quis aliqua', tags: ['mollit labore magna ad', 'minim sit magna'], name: 'oc', value: 'dolor', _id: '5c6cafbf1babb758d29754aa' }, { label: 'Duis fug', description: 'Duis eiusmod non deserunt', tags: [ 'nostrud Lorem', 'laborum ut labore esse', 'voluptate', 'occaecat tempor', 'laboris irure culpa' ], name: 'nulla ut dolor in', value: 'amet in enim consequat', _id: '5c6cafbf1babb758d29754ab' }, { label: 'culpa', description: 'velit enim incididunt', tags: ['tempor nisi fugiat proident', 'Ut pariatur'], name: 'irure incididunt dolore ipsum', value: 'consectetur elit ea Ut', _id: '5c6cafbf1babb758d29754ac' } ], accounts: [ { _id: '5c6cafbf1babb758d29754ad', name: 'Credit Card Account', userId: '5c6cafbf1babb758d297540c', currencyCode: 'XFU', currencySymbol: 'B/.' }, { _id: '5c6cafbf1babb758d29754ae', name: 'Money Market Account', userId: '5c6cafbf1babb758d297540c', currencyCode: 'BZD', currencySymbol: '₮' }, { _id: '5c6cafbf1babb758d29754af', name: 'Auto Loan Account', userId: '5c6cafbf1babb758d2975408', currencyCode: 'PKR', currencySymbol: '$' }, { _id: '5c6cafbf1babb758d29754b0', name: 'Personal Loan Account', userId: '5c6cafbf1babb758d297540c', currencyCode: 'PKR', currencySymbol: '$' }, { _id: '5c6cafbf1babb758d29754b1', name: 'Home Loan Account', userId: '5c6cafbf1babb758d2975408', currencyCode: 'XCD', currencySymbol: '₨' }, { _id: '5c6cafbf1babb758d29754b2', name: 'Credit Card Account', userId: '5c6cafbf1babb758d2975409', currencyCode: 'BWP', currencySymbol: 'kr' }, { _id: '5c6cafbf1babb758d29754b3', name: 'Personal Loan Account', userId: '5c6cafbf1babb758d2975409', currencyCode: 'NIO', currencySymbol: '$' }, { _id: '5c6cafbf1babb758d29754b4', name: 'Auto Loan Account', userId: '5c6cafbf1babb758d297540c', currencyCode: 'SDG', currencySymbol: '$' }, { _id: '5c6cafbf1babb758d29754b5', name: 'Money Market Account', userId: '5c6cafbf1babb758d2975409', currencyCode: 'MNT', currencySymbol: '₨' }, { _id: '5c6cafbf1babb758d29754b6', name: 'Auto Loan Account', userId: '5c6cafbf1babb758d297540b', currencyCode: 'BOB BOV', currencySymbol: '$' }, { _id: '5c6cafbf1babb758d29754b7', name: 'Personal Loan Account', userId: '5c6cafbf1babb758d297540a', currencyCode: 'ANG', currencySymbol: 'Lt' }, { _id: '5c6cafbf1babb758d29754b8', name: 'Personal Loan Account', userId: '5c6cafbf1babb758d297540c', currencyCode: 'HTG USD', currencySymbol: '﷼' }, { _id: '5c6cafbf1babb758d29754b9', name: 'Investment Account', userId: '5c6cafbf1babb758d297540a', currencyCode: 'SDG', currencySymbol: 'L' }, { _id: '5c6cafbf1babb758d29754ba', name: 'Credit Card Account', userId: '5c6cafbf1babb758d297540a', currencyCode: 'BYR', currencySymbol: 'лв' }, { _id: '5c6cafbf1babb758d29754bb', name: 'Checking Account', userId: '5c6cafbf1babb758d2975409', currencyCode: 'XDR', currencySymbol: 'Ls' }, { _id: '5c6cafbf1babb758d29754bc', name: 'Savings Account', userId: '5c6cafbf1babb758d2975409', currencyCode: 'PHP', currencySymbol: '£' }, { _id: '5c6cafbf1babb758d29754bd', name: 'Investment Account', userId: '5c6cafbf1babb758d2975407', currencyCode: 'ZWL', currencySymbol: '$' }, { _id: '5c6cafbf1babb758d29754be', name: 'Credit Card Account', userId: '5c6cafbf1babb758d297540c', currencyCode: 'DKK', currencySymbol: 'TT$' }, { _id: '5c6cafbf1babb758d29754bf', name: 'Auto Loan Account', userId: '5c6cafbf1babb758d2975409', currencyCode: 'RSD', currencySymbol: 'L' }, { _id: '5c6cafbf1babb758d29754c0', name: 'Checking Account', userId: '5c6cafbf1babb758d297540b', currencyCode: 'CHF', currencySymbol: '$' }, { _id: '5c6cafbf1babb758d29754c1', name: 'Credit Card Account', userId: '5c6cafbf1babb758d2975407', currencyCode: 'ILS', currencySymbol: '﷼' }, { _id: '5c6cafbf1babb758d29754c2', name: 'Credit Card Account', userId: '5c6cafbf1babb758d297540c', currencyCode: 'BMD', currencySymbol: '﷼' }, { _id: '5c6cafbf1babb758d29754c3', name: 'Auto Loan Account', userId: '5c6cafbf1babb758d2975409', currencyCode: 'DOP', currencySymbol: '$' }, { _id: '5c6cafbf1babb758d29754c4', name: 'Savings Account', userId: '5c6cafbf1babb758d297540c', currencyCode: 'LVL', currencySymbol: 'Php' }, { _id: '5c6cafbf1babb758d29754c5', name: 'Home Loan Account', userId: '5c6cafbf1babb758d297540b', currencyCode: 'XOF', currencySymbol: '₨' } ] } ================================================ FILE: test/fixtures/feathers-client.js ================================================ import feathers from '@feathersjs/feathers' import socketio from '@feathersjs/socketio-client' import rest from '@feathersjs/rest-client' import axios from 'axios' import auth from '@feathersjs/authentication-client' import io from 'socket.io-client/dist/socket.io' import fixtureSocket from 'can-fixture-socket' const mockServer = new fixtureSocket.Server(io) const baseUrl = 'http://localhost:3030' // These are fixtures used in the service-modulet.test.js under socket events. let id = 0 mockServer.on('things::create', function (data, params, cb) { data.id = id id++ mockServer.emit('things created', data) cb(null, data) }) mockServer.on('things::patch', function (id, data, params, cb) { Object.assign(data, { id, test: true }) mockServer.emit('things patched', data) cb(null, data) }) mockServer.on('things::update', function (id, data, params, cb) { Object.assign(data, { id, test: true }) mockServer.emit('things updated', data) cb(null, data) }) mockServer.on('things::remove', function (id, obj, cb) { const response = { id, test: true } mockServer.emit('things removed', response) cb(null, response) }) let idDebounce = 0 mockServer.on('things-debounced::create', function (data, obj, cb) { data.id = idDebounce idDebounce++ mockServer.emit('things-debounced created', data) cb(null, data) }) mockServer.on('things-debounced::patch', function (id, data, params, cb) { Object.assign(data, { id, test: true }) mockServer.emit('things-debounced patched', data) cb(null, data) }) mockServer.on('things-debounced::update', function (id, data, params, cb) { Object.assign(data, { id, test: true }) mockServer.emit('things-debounced updated', data) cb(null, data) }) mockServer.on('things-debounced::remove', function (id, params, cb) { const response = { id, test: true } mockServer.emit('things-debounced removed', response) cb(null, response) }) // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function makeFeathersSocketClient(baseUrl) { const socket = io(baseUrl) return feathers().configure(socketio(socket)).configure(auth()) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function makeFeathersRestClient(baseUrl) { return feathers().configure(rest(baseUrl).axios(axios)).configure(auth()) } const sock = io(baseUrl) export const feathersSocketioClient = feathers() .configure(socketio(sock)) .configure(auth()) export const feathersRestClient = feathers() .configure(rest(baseUrl).axios(axios)) .configure(auth()) ================================================ FILE: test/fixtures/server.js ================================================ import feathers from '@feathersjs/feathers' import rest from '@feathersjs/express/rest' import socketio from '@feathersjs/socketio' import bodyParser from 'body-parser' import auth from '@feathersjs/authentication' import jwt from '@feathersjs/authentication-jwt' import memory from 'feathers-memory' const app = feathers() .use(bodyParser.json()) .use(bodyParser.urlencoded({ extended: true })) .configure(rest()) .configure(socketio()) .use('/users', memory()) .use('/todos', memory()) .use('/errors', memory()) .configure( auth({ secret: 'test', service: '/users' }) ) .configure(jwt()) app.service('/errors').hooks({ before: { all: [ hook => { throw new Error(`${hook.method} Denied!`) } ] } }) const port = 3030 const server = app.listen(port) process.on('unhandledRejection', (reason, p) => console.log('Unhandled Rejection at: Promise ', p, reason) ) server.on('listening', () => { console.log(`Feathers application started on localhost:${port}`) setTimeout(function() { server.close() }, 50000) }) ================================================ FILE: test/fixtures/store.js ================================================ import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default function makeStore() { return new Vuex.Store({ state: { count: 0 }, mutations: { increment(state) { state.count++ } } }) } ================================================ FILE: test/fixtures/todos.js ================================================ export function makeTodos() { return { 1: { _id: 1, description: 'Dishes', isComplete: true }, 2: { _id: 2, description: 'Laundry', isComplete: true }, 3: { _id: 3, description: 'Groceries', isComplete: true } } } ================================================ FILE: test/index.test.ts ================================================ import { assert } from 'chai' import * as feathersVuex from '../src/index' import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) describe('feathers-vuex', () => { it('has correct exports', () => { assert(typeof feathersVuex.default === 'function') assert( typeof feathersVuex.FeathersVuex.install === 'function', 'has Vue Plugin' ) assert(feathersVuex.FeathersVuexFind) assert(feathersVuex.FeathersVuexGet) assert(feathersVuex.initAuth) assert(feathersVuex.makeFindMixin) assert(feathersVuex.makeGetMixin) assert(feathersVuex.models) }) it('requires a Feathers Client instance', () => { try { feathersVuex.default( {}, { serverAlias: 'index-test' } ) } catch (error) { assert( error.message === 'The first argument to feathersVuex must be a feathers client.' ) } }) }) ================================================ FILE: test/make-find-mixin.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' import jsdom from 'jsdom-global' import Vue from 'vue/dist/vue' import Vuex from 'vuex' import feathersVuex, { FeathersVuex } from '../src/index' import makeFindMixin from '../src/make-find-mixin' import { feathersRestClient as feathersClient } from './fixtures/feathers-client' jsdom() require('events').EventEmitter.prototype._maxListeners = 100 function makeContext() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'make-find-mixin' }) class FindModel extends BaseModel { public static modelName = 'FindModel' public static test = true } return { FindModel, BaseModel, makeServicePlugin } } Vue.use(Vuex) Vue.use(FeathersVuex) describe('Find Mixin', function () { const { makeServicePlugin, FindModel } = makeContext() const serviceName = 'todos' const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: FindModel, service: feathersClient.service(serviceName) }) ] }) it('correctly forms mixin data', function () { const todosMixin = makeFindMixin({ service: 'todos' }) interface TodosComponent { todos: [] todosServiceName: string isFindTodosPending: boolean haveTodosBeenRequestedOnce: boolean haveTodosLoadedOnce: boolean findTodos: Function todosLocal: boolean todosQid: string todosQueryWhen: Function todosParams: any todosFetchParams: any } const vm = new Vue({ name: 'todos-component', mixins: [todosMixin], store, template: `
` }).$mount() assert.deepEqual(vm.todos, [], 'todos prop was empty array') assert( vm.hasOwnProperty('todosPaginationData'), 'pagination data prop was present, even if undefined' ) assert(vm.todosServiceName === 'todos', 'service name was correct') assert(vm.isFindTodosPending === false, 'loading boolean is in place') assert( vm.haveTodosBeenRequestedOnce === false, 'requested once boolean is in place' ) assert(vm.haveTodosLoadedOnce === false, 'loaded once boolean is in place') assert(typeof vm.findTodos === 'function', 'the find action is in place') assert(vm.todosLocal === false, 'local boolean is false by default') assert( typeof vm.$options.created[0] === 'function', 'created lifecycle hook function is in place given that local is false' ) assert( vm.todosQid === 'default', 'the default query identifier is in place' ) assert(vm.todosQueryWhen === true, 'the default queryWhen is true') // assert(vm.todosWatch.length === 0, 'the default watch is an empty array') assert( vm.todosParams === undefined, 'no params are in place by default, must be specified by the user' ) assert( vm.todosFetchParams === undefined, 'no fetch params are in place by default, must be specified by the user' ) }) it('correctly forms mixin data for dynamic service', function () { const tasksMixin = makeFindMixin({ service() { return this.serviceName }, local: true }) interface TasksComponent { tasks: [] serviceServiceName: string isFindTasksPending: boolean findTasks: Function tasksLocal: boolean tasksQid: string tasksQueryWhen: Function tasksParams: any tasksFetchParams: any } const vm = new Vue({ name: 'tasks-component', data: () => ({ serviceName: 'tasks' }), mixins: [tasksMixin], store, template: `
` }).$mount() assert.deepEqual(vm.items, [], 'items prop was empty array') assert( vm.hasOwnProperty('servicePaginationData'), 'pagination data prop was present, even if undefined' ) assert(vm.serviceServiceName === 'tasks', 'service name was correct') assert(vm.isFindServicePending === false, 'loading boolean is in place') assert(typeof vm.findService === 'function', 'the find action is in place') assert(vm.serviceLocal === true, 'local boolean is set to true') assert( typeof vm.$options.created === 'undefined', 'created lifecycle hook function is NOT in place given that local is true' ) assert( vm.serviceQid === 'default', 'the default query identifier is in place' ) assert(vm.serviceQueryWhen === true, 'the default queryWhen is true') // assert(vm.tasksWatch.length === 0, 'the default watch is an empty array') assert( vm.serviceParams === undefined, 'no params are in place by default, must be specified by the user' ) assert( vm.serviceFetchParams === undefined, 'no fetch params are in place by default, must be specified by the user' ) }) }) ================================================ FILE: test/service-module/make-service-plugin.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' import Vue from 'vue' import Vuex from 'vuex' import { ServiceState } from './types' import { clearModels } from '../../src/service-module/global-models' import { clients } from '../../src/service-module/global-clients' import { feathersRestClient as feathers } from '../../test/fixtures/feathers-client' import feathersVuex from '../../src/index' import _pick from 'lodash/pick' import _omit from 'lodash/omit' Vue.use(Vuex) describe('makeServicePlugin', function () { beforeEach(() => { clearModels() }) it('adds Feathers client to the global clients', () => { feathersVuex(feathers, { serverAlias: 'this is a test' }) assert(clients.byAlias['this is a test'], 'got a reference to the client.') }) it('registers the vuex module with options', function () { interface RootState { todos: {} } const serverAlias = 'make-service-plugin' const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { serverAlias }) const servicePath = 'make-service-plugin-todos' class Todo extends BaseModel { public static modelName = 'Todo' public static servicePath = servicePath } const todosPlugin = makeServicePlugin({ servicePath, Model: Todo, service: feathers.service(servicePath), namespace: 'make-service-plugin-todos' }) const store = new Vuex.Store({ plugins: [todosPlugin] }) const keys = Object.keys(store.state['make-service-plugin-todos']) const received = _pick(store.state['make-service-plugin-todos'], keys) const expected = { addOnUpsert: false, autoRemove: false, debug: false, copiesById: {}, enableEvents: true, errorOnCreate: null, errorOnFind: null, errorOnGet: null, errorOnPatch: null, errorOnRemove: null, errorOnUpdate: null, idField: 'id', tempIdField: '__id', ids: [], isCreatePending: false, isFindPending: false, isGetPending: false, isPatchPending: false, isRemovePending: false, isUpdatePending: false, keepCopiesInStore: false, debounceEventsTime: null, debounceEventsMaxWait: 1000, keyedById: {}, modelName: 'Todo', nameStyle: 'short', namespace: 'make-service-plugin-todos', pagination: { defaultLimit: null, defaultSkip: null }, paramsForServer: ['$populateParams'], preferUpdate: false, replaceItems: false, serverAlias: 'make-service-plugin', servicePath: 'make-service-plugin-todos', skipRequestIfExists: false, tempsById: {}, whitelist: [], isIdCreatePending: [], isIdUpdatePending: [], isIdPatchPending: [], isIdRemovePending: [] } assert.deepEqual(_omit(received), _omit(expected), 'defaults in place.') }) it('sets up Model.store && service.FeathersVuexModel', function () { const serverAlias = 'make-service-plugin' const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { serverAlias }) const servicePath = 'make-service-plugin-todos' const service = feathers.service(servicePath) class Todo extends BaseModel { public static modelName = 'Todo' public static servicePath = servicePath } const todosPlugin = makeServicePlugin({ servicePath, Model: Todo, service }) const store = new Vuex.Store({ plugins: [todosPlugin] }) assert(Todo.store === store, 'the store is on the Model!') // @ts-ignore assert.equal(service.FeathersVuexModel, Todo, 'Model accessible on service') }) it('allows accessing other models', function () { const serverAlias = 'make-service-plugin' const { makeServicePlugin, BaseModel, models } = feathersVuex(feathers, { idField: '_id', serverAlias }) const servicePath = 'make-service-plugin-todos' class Todo extends BaseModel { public static modelName = 'Todo' public static servicePath = servicePath } const todosPlugin = makeServicePlugin({ servicePath, Model: Todo, service: feathers.service(servicePath) }) const store = new Vuex.Store({ plugins: [todosPlugin] }) assert(models[serverAlias][Todo.name] === Todo) assert(Todo.store === store) }) it('allows service specific handleEvents', async function () { // feathers.use('todos', new TodosService()) const serverAlias = 'make-service-plugin' const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { idField: '_id', serverAlias }) const servicePath = 'make-service-plugin-todos' class Todo extends BaseModel { public static modelName = 'Todo' public static servicePath = servicePath public static namespace = 'make-service-plugin-todos' } let createdCalled = false let updatedCalled = false let patchedCalled = false let removedCalled = false const todosPlugin = makeServicePlugin({ servicePath, Model: Todo, service: feathers.service(servicePath), handleEvents: { created() { createdCalled = true return true }, updated() { updatedCalled = true return true }, patched() { patchedCalled = true return true }, removed() { removedCalled = true return true } } }) const store = new Vuex.Store({ plugins: [todosPlugin] }) const todo = new Todo() // Fake server call feathers.service('make-service-plugin-todos').hooks({ before: { create: [ context => { delete context.data.__id delete context.data.__isTemp }, context => { context.result = { _id: 24, ...context.data } return context } ], update: [ context => { context.result = { ...context.data } return context } ], patch: [ context => { context.result = { ...context.data } return context } ], remove: [ context => { context.result = { ...todo } return context } ] } }) await todo.create() assert(createdCalled, 'created handler called') await todo.update() assert(updatedCalled, 'updated handler called') await todo.patch() assert(patchedCalled, 'patched handler called') await todo.remove() assert(removedCalled, 'removed handler called') }) it('fall back to globalOptions handleEvents if service specific handleEvents handler is missing', async function () { // feathers.use('todos', new TodosService()) const serverAlias = 'make-service-plugin' let globalCreatedCalled = false let globalUpdatedCalled = false let globalPatchedCalled = false let globalRemovedCalled = false const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { idField: '_id', serverAlias, handleEvents: { created() { globalCreatedCalled = true return true }, updated() { globalUpdatedCalled = true return true }, patched() { globalPatchedCalled = true return true }, removed() { globalRemovedCalled = true return true } } }) const servicePath = 'make-service-plugin-todos' class Todo extends BaseModel { public static modelName = 'Todo' public static servicePath = servicePath public static namespace = 'make-service-plugin-todos' } let specificUpdatedCalled = false const todosPlugin = makeServicePlugin({ servicePath, Model: Todo, service: feathers.service(servicePath), namespace: 'make-service-plugin-todos', handleEvents: { updated() { specificUpdatedCalled = true return true } } }) const store = new Vuex.Store({ plugins: [todosPlugin] }) const todo = new Todo() // Fake server call feathers.service('make-service-plugin-todos').hooks({ before: { create: [ context => { delete context.data.__id delete context.data.__isTemp }, context => { context.result = { _id: 24, ...context.data } return context } ], update: [ context => { context.result = { ...context.data } return context } ], patch: [ context => { context.result = { ...context.data } return context } ], remove: [ context => { context.result = { ...todo } return context } ] } }) await todo.create() assert(globalCreatedCalled, 'global created handler called') await todo.update() assert(specificUpdatedCalled, 'specific updated handler called') assert(!globalUpdatedCalled, 'global updated handler NOT called') await todo.patch() assert(globalPatchedCalled, 'global patched handler called') await todo.remove() assert(globalRemovedCalled, 'global removed handler called') }) it('allow handleEvents handlers to return extracted event data', async function () { const serverAlias = 'make-service-plugin' const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { idField: '_id', serverAlias, handleEvents: { created(e) { return [true, e.myCreatedPropWithActualData] }, updated(e) { return [true, e.myUpdatedPropWithActualData] }, patched(e) { return [true, e.myPatchedPropWithActualData] }, removed(e) { return [true, e.myRemovedPropWithActualData] } } }) const servicePath = 'make-service-plugin-todos' class Todo extends BaseModel { public static modelName = 'Todo' public static servicePath = servicePath } const todosService = feathers.service(servicePath) const todosPlugin = makeServicePlugin({ servicePath, Model: Todo, service: todosService, namespace: 'make-service-plugin-todos' }) const store = new Vuex.Store<{ todos: ServiceState }>({ plugins: [todosPlugin] }) const { keyedById } = store.state['make-service-plugin-todos'] let createdData = null let updatedData = null let patchedData = null let removedData = null Todo.on('created', e => (createdData = e)) Todo.on('updated', e => (updatedData = e)) Todo.on('patched', e => (patchedData = e)) Todo.on('removed', e => (removedData = e)) assert(Object.keys(keyedById).length === 0, 'no todos in store') todosService.emit('created', { context: 'foo', myCreatedPropWithActualData: { _id: 42, text: '' } }) assert(keyedById[42], 'todo added to store') assert(keyedById[42].text === '', 'todo string is empty') assert(createdData, "Model's created event fired") assert( createdData.context === 'foo' && createdData.myCreatedPropWithActualData, "Model's created event got all event data" ) todosService.emit('updated', { context: 'bar', myUpdatedPropWithActualData: { _id: 42, text: 'updated' } }) assert(keyedById[42].text === 'updated', 'todo was updated') assert(updatedData, "Model's updated event fired") assert( updatedData.context === 'bar' && updatedData.myUpdatedPropWithActualData, "Model's updated event got all event data" ) todosService.emit('patched', { context: 'baz', myPatchedPropWithActualData: { _id: 42, text: 'patched' } }) assert(keyedById[42].text === 'patched', 'todo was patched') assert(patchedData, "Model's patched event fired") assert( patchedData.context === 'baz' && patchedData.myPatchedPropWithActualData, "Model's patched event got all event data" ) todosService.emit('removed', { context: 'spam', myRemovedPropWithActualData: { _id: 42 } }) assert(Object.keys(keyedById).length === 0, 'todo removed from store') assert(removedData, "Model's removed event fired") assert( removedData.context === 'spam' && removedData.myRemovedPropWithActualData, "Model's removed event got all event data" ) }) }) ================================================ FILE: test/service-module/misconfigured-client.test.ts ================================================ /* eslint @typescript-eslint/ban-ts-ignore:0 */ import { assert } from 'chai' import feathersVuex from '../../src/index' import feathers from '@feathersjs/client' import auth from '@feathersjs/authentication-client' // @ts-ignore const feathersClient = feathers().configure(auth()) describe('Service Module - Bad Client Setup', () => { it('throws an error when no client transport plugin is registered', () => { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'misconfigured' }) class MisconfiguredTask extends BaseModel { public static modelName = 'MisconfiguredTask' public static test = true } try { makeServicePlugin({ Model: MisconfiguredTask, service: feathersClient.service('misconfigured-todos') }) } catch (error) { assert( error.message.includes( 'No service was provided. If you passed one in, check that you have configured a transport plugin on the Feathers Client. Make sure you use the client version of the transport.' ), 'got an error with a misconfigured client' ) } }) }) ================================================ FILE: test/service-module/model-base.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' import Vue from 'vue' import Vuex from 'vuex' import { clearModels } from '../../src/service-module/global-models' import { feathersRestClient as feathers, makeFeathersRestClient } from '../fixtures/feathers-client' import feathersVuex from '../../src/index' Vue.use(Vuex) process.setMaxListeners(100) describe.skip('Model - Standalone', function () { it.skip('allows using a model without a service', function () {}) it.skip('rename serverAlias to just `alias` or maybe `groupName`', function () {}) }) describe('makeModel / BaseModel', function () { before(() => { clearModels() }) it('properly sets up the BaseModel', function () { const alias = 'model-base' const { BaseModel } = feathersVuex(feathers, { serverAlias: alias }) const { name, store, namespace, idField, preferUpdate, serverAlias, models, copiesById } = BaseModel assert(name === 'BaseModel', 'name in place') // Monkey patched onto the Model class in `makeServicePlugin()` assert(!store, 'no store by default') assert(!namespace, 'no namespace by default') assert(idField === 'id', 'default idField is id') assert(!preferUpdate, 'prefer fetch by default') // Readonly props assert(serverAlias === 'model-base', 'serverAlias') assert(models, 'models are available') assert.equal(Object.keys(copiesById).length, 0, 'copiesById is empty') // Static Methods const staticMethods = [ 'getId', 'find', 'findInStore', 'count', 'countInStore', 'get', 'getFromStore' ] staticMethods.forEach(method => { assert(typeof BaseModel[method] === 'function', `has ${method} method`) }) // Prototype Methods const prototypeMethods = [ 'clone', 'reset', 'commit', 'save', 'create', 'patch', 'update', 'remove' ] prototypeMethods.forEach(method => { assert( typeof BaseModel.prototype[method] === 'function', `has ${method} method` ) }) // Utility Methods const utilityMethods = ['hydrateAll'] utilityMethods.forEach(method => { assert(typeof BaseModel[method] === 'function', `has ${method} method`) }) const eventMethods = [ 'on', 'off', 'once', 'emit', 'addListener', 'removeListener', 'removeAllListeners' ] eventMethods.forEach(method => { assert(typeof BaseModel[method] === 'function', `has ${method} method`) }) const getterMethods = [ 'isCreatePending', 'isUpdatePending', 'isPatchPending', 'isRemovePending', 'isSavePending', 'isPending' ] const m = new BaseModel() getterMethods.forEach(method => { assert( typeof Object.getOwnPropertyDescriptor(Object.getPrototypeOf(m), method).get === 'function', `has ${method} getter` ) }) }) it('allows customization through the FeathersVuexOptions', function () { const { BaseModel } = feathersVuex(feathers, { serverAlias: 'myApi', idField: '_id', preferUpdate: true }) const { idField, preferUpdate, serverAlias } = BaseModel assert(idField === '_id', 'idField was set') assert(preferUpdate, 'turned on preferUpdate') assert(serverAlias === 'myApi', 'serverAlias was set') }) it('receives store & other props after Vuex plugin is registered', function () { const { BaseModel, makeServicePlugin } = feathersVuex(feathers, { serverAlias: 'myApi' }) BaseModel.modelName = 'TestModel' const plugin = makeServicePlugin({ servicePath: 'todos', service: feathers.service('todos'), Model: BaseModel }) new Vuex.Store({ plugins: [plugin] }) const { store, namespace, servicePath } = BaseModel assert(store, 'store is in place') assert.equal(namespace, 'todos', 'namespace is in place') assert.equal(servicePath, 'todos', 'servicePath is in place') }) it('allows access to other models after Vuex plugins are registered', function () { const serverAlias = 'model-base' const { makeServicePlugin, BaseModel, models } = feathersVuex(feathers, { idField: '_id', serverAlias }) // Create a Todo Model & Plugin class Todo extends BaseModel { public static modelName = 'Todo' public test = true } const todosPlugin = makeServicePlugin({ servicePath: 'todos', Model: Todo, service: feathers.service('todos') }) // Create a Task Model & Plugin class Task extends BaseModel { public static modelName = 'Task' public test = true } const tasksPlugin = makeServicePlugin({ servicePath: 'tasks', Model: Task, service: feathers.service('tasks') }) // Register the plugins new Vuex.Store({ plugins: [todosPlugin, tasksPlugin] }) assert(models[serverAlias][Todo.name] === Todo) assert.equal(Todo.models, models, 'models available at Model.models') assert.equal(Task.models, models, 'models available at Model.models') }) it('works with multiple, independent Feathers servers', function () { // Create a Todo Model & Plugin on myApi const feathersMyApi = makeFeathersRestClient('https://api.my-api.com') const myApi = feathersVuex(feathersMyApi, { idField: '_id', serverAlias: 'myApi' }) class Todo extends myApi.BaseModel { public static modelName = 'Todo' public test = true } const todosPlugin = myApi.makeServicePlugin({ Model: Todo, service: feathersMyApi.service('todos') }) // Create a Task Model & Plugin on theirApi const feathersTheirApi = makeFeathersRestClient('https://api.their-api.com') const theirApi = feathersVuex(feathersTheirApi, { serverAlias: 'theirApi' }) class Task extends theirApi.BaseModel { public static modelName = 'Task' public test = true } const tasksPlugin = theirApi.makeServicePlugin({ Model: Task, service: feathersTheirApi.service('tasks') }) // Register the plugins new Vuex.Store({ plugins: [todosPlugin, tasksPlugin] }) const { models } = myApi assert(models.myApi.Todo === Todo) assert(!models.theirApi.Todo, `Todo stayed out of the 'theirApi' namespace`) assert(models.theirApi.Task === Task) assert(!models.myApi.Task, `Task stayed out of the 'myApi' namespace`) assert.equal( models.myApi.byServicePath[Todo.servicePath], Todo, 'also registered in models.byServicePath' ) assert.equal( models.theirApi.byServicePath[Task.servicePath], Task, 'also registered in models.byServicePath' ) }) }) ================================================ FILE: test/service-module/model-instance-defaults.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { ServiceState, Location } from './types' import { assert } from 'chai' import feathersVuex, { models } from '../../src/index' import { mergeWithAccessors } from '../../src/utils' import { clearModels } from '../../src/service-module/global-models' import { makeFeathersRestClient, feathersRestClient as feathersClient, feathersSocketioClient } from '../fixtures/feathers-client' import Vuex from 'vuex' import { makeContext as makeLetterContext } from './model-methods.test' interface TodoState extends ServiceState { test: any test2: { test: boolean } isTrue: boolean } interface RootState { todos: TodoState tasks: ServiceState tests: ServiceState blah: ServiceState things: ServiceState } function makeContext() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'service-module' }) class Todo extends BaseModel { public constructor(data = {}, options?) { super(data, options) } public static modelName = 'Todo' public description: string } class Person extends BaseModel { public constructor(data = {}, options?) { super(data, options) } public static modelName = 'Person' public static test = true } class Item extends BaseModel { public static modelName = 'Item' public static test = true } class Task extends BaseModel { public static modelName = 'Task' public static test = true } class Car extends BaseModel { public static modelName = 'Car' public static test = true } class Group extends BaseModel { public static modelName = 'Group' public static test = true } class Test extends BaseModel { public static modelName = 'Test' public static test = true } class Thing extends BaseModel { public static modelName = 'Thing' public static test = true } const todosPlugin = makeServicePlugin({ Model: Todo, service: feathersClient.service('service-todos') }) const store = new Vuex.Store({ plugins: [ todosPlugin, makeServicePlugin({ Model: Person, service: feathersClient.service('people') }), makeServicePlugin({ Model: Car, service: feathersClient.service('cars') }), makeServicePlugin({ Model: Group, service: feathersClient.service('groups') }) ] }) return { makeServicePlugin, BaseModel, Todo, Person, Item, Task, Car, Group, Test, Thing, store } } describe('Models - Default Values', function() { beforeEach(() => { clearModels() }) it('models default to an empty object when there is no BaseModel.store', function() { const { BaseModel } = makeContext() // Since we're not using this NakedTodo model in a service plugin, it doesn't get // monkey patched with the store. class NakedTodo extends BaseModel { public static modelName = 'NakedTodo' public static test = true } const todo = new NakedTodo() assert.deepEqual(todo.toJSON(), {}, 'default model is an empty object') }) it('models have tempIds when there is a store', function() { const { Todo } = makeContext() const todo = new Todo() const expectedProps = ['__id', '__isTemp'] assert.deepEqual( Object.keys(todo), expectedProps, 'default model is a temp' ) }) it('adds new instances containing an id to the store', function() { const { Todo } = makeContext() const todo = new Todo({ id: 1, description: 'test', isComplete: true }) const todoInStore = Todo.store.state['service-todos'].keyedById[1] assert.deepEqual(todoInStore, todo, 'task was added to the store') }) it('stores clones in Model.copiesById by default', function() { const { Todo } = makeContext() const todo = new Todo({ id: 1, description: 'This is the original' }) assert.deepEqual( Todo.copiesById, {}, 'Model.copiesById should start out empty' ) const todoClone = todo.clone() assert(Todo.copiesById[1], 'should have a copy stored on Model.copiesById') todoClone.description = `I'm a clone, now!` todoClone.commit() assert.equal( todo.description, `I'm a clone, now!`, 'the original should have been updated' ) }) it('each model has its own Model.copiesById', function() { const { Todo, Person } = makeContext() const todo = new Todo({ id: 1, description: 'This is the original' }) const person = new Person({ id: 2, name: 'Xavier' }) todo.clone() assert(Todo.copiesById[1], 'should have a copy stored on Todo.copiesById') assert( !Person.copiesById[1], 'should not have a copy stored on Person.copiesById' ) person.clone() assert( Person.copiesById[2], 'should have a copy stored on Person.copiesById' ) assert( !Todo.copiesById[2], 'should not have a copy stored on Todo.copiesById' ) }) it('allows instance defaults, including getters and setters', function() { const { BaseModel } = feathersVuex(feathersClient, { serverAlias: 'instance-defaults' }) class Car extends BaseModel { public id? public year = 1905 public make = 'Tesla' public model = 'Roadster' public get combined(): string { return `${this.year} ${this.make} ${this.model}` } public set yearBeforeCurrent(year) { if (year < this.year) { this.year = year } } public constructor(data?, options?) { super(data, options) } } const car = new Car() assert.equal(car.year, 1905, 'default year set') assert.equal(car.make, 'Tesla', 'default make set') assert.equal(car.model, 'Roadster', 'default model set') assert.equal(car.combined, '1905 Tesla Roadster', 'getters work, too!') car.yearBeforeCurrent = 1900 assert.equal(car.combined, '1900 Tesla Roadster', 'setters work, too!') }) it('allows overriding default values in the constructor', function() { const { BaseModel } = feathersVuex(feathersClient, { serverAlias: 'instance-defaults' }) class Car extends BaseModel { public id? public year = 1905 public make = 'Tesla' public model = 'Roadster' public constructor(data?, options?) { super(data, options) if (this.make === 'Tesla') { this.make = 'Porsche' } } } const car = new Car() assert.equal(car.make, 'Porsche', 'default make set') }) it(`uses the class defaults if you don't override them in the constructor`, function() { const { BaseModel } = feathersVuex(feathersClient, { serverAlias: 'instance-defaults' }) class Person extends BaseModel { public id? public firstName = 'Harry' public location: Location = { coordinates: [0, 0] } public constructor(data?, options?) { // Calling super calls the BaseModel constructor, which merges the data // onto `this`. super(data, options) // Once the BaseModel constructor has finished, the props in the class // definition are applied to `this` before running any additional code in the // extending class's constructor. This means that at this point, all // new instances have `location.coordinates = [0, 0]` // Since we're not re-applying the `data` to `this`, the class defaults have // overwritten whatever we passed in. return this } } const location: Location = { coordinates: [1, 1] } const person1 = new Person({ firstName: 'Marshall', location }) const person2 = new Person({ firstName: 'Austin', location }) const areSame = person1.location === person2.location assert(!areSame, 'the locations are different objects') assert(person1.firstName === 'Harry', 'the defaults replaced our args') assert(person2.firstName === 'Harry', 'the defaults replaced our args') // See, even the location we passed in was overwritten by the defaults. assert.deepEqual(person1.location.coordinates, [0, 0], 'defaults won') }) it('does not share nested objects between instances when you override class defaults in the constructor', function() { const { BaseModel } = feathersVuex(feathersClient, { serverAlias: 'instance-defaults' }) class Person extends BaseModel { public id? public firstName: string public location: Location = { coordinates: [0, 0] } public constructor(data?, options?) { // Pass { merge: false } in the third arg to prevent BaseModel from // doing its own merge super(data, { merge: false }) // Calling merge here overwrites the Class's default location. // You could also write `this.location = data.location` Person.merge(this, data) } } const location: Location = { coordinates: [1, 1] } // Look, I'm passing in location with coordinates [1, 1] const person1 = new Person({ firstName: 'Marshall', location }) const person2 = new Person({ firstName: 'Austin', location }) const areSame = person1.location === person2.location // But the objects are distinct because they've been merged in the constructor assert(!areSame, 'the locations are different objects') }) it('allows passing instanceDefaults in the service plugin options', function() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'instance-defaults' }) class Person extends BaseModel { public static modelName = 'Person' public constructor(data?, options?) { super(data, options) } } const location: Location = { coordinates: [1, 1] } new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Person, service: feathersClient.service('people'), instanceDefaults: () => ({ firstName: 'Harry', lastName: 'Potter', location, get fullName() { return `${this.firstName} ${this.lastName}` }, set fullName(val) { const [firstName, lastName] = val.split(' ') Object.assign(this, { firstName, lastName }) } }) }) ] }) const person1 = new Person({ firstName: 'Marshall', lastName: 'Thompson' }) const person2 = new Person({ firstName: 'Kai', location: { coordinates: [0, 0] }, fullName: 'Jerry Seinfeld' }) const areSame = person1.location === person2.location assert(!areSame, 'nested objects are unique') assert.equal(person1.lastName, 'Thompson', 'person1 has correct lastName') assert.equal(person2.lastName, 'Potter', 'person2 got default lastName') assert.deepEqual( person1.location.coordinates, [1, 1], 'person1 got default location' ) assert.deepEqual( person2.location.coordinates, [0, 0], 'person2 got provided location' ) assert.equal(person1.fullName, 'Marshall Thompson', 'getter is in place') assert.equal(person2.fullName, 'Kai Potter', 'getter is still in place') person1.fullName = 'Marshall Me' person2.fullName = 'Kai Me' assert.equal(person1.firstName, 'Marshall', 'firstName was set') assert.equal(person1.lastName, 'Me', 'lastName was set') assert.equal(person2.firstName, 'Kai', 'firstName was set') assert.equal(person2.lastName, 'Me', 'lastName was set') }) it('instanceDefault accessors stay intact with clone and commit', function() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'instance-defaults' }) class Person extends BaseModel { public static modelName = 'Person' public constructor(data?, options?) { super(data, options) } } const location: Location = { coordinates: [1, 1] } new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Person, service: feathersClient.service('people'), instanceDefaults: () => ({ firstName: 'Harry', lastName: 'Potter', location, get fullName() { return `${this.firstName} ${this.lastName}` }, set fullName(val) { const [firstName, lastName] = val.split(' ') Object.assign(this, { firstName, lastName }) } }) }) ] }) const person = new Person({ firstName: 'Marshall', lastName: 'Thompson' }) // Clone the person const clone = person.clone() // Check the getter clone.firstName = 'FeathersJS' clone.lastName = 'Developer' assert.equal(clone.fullName, 'FeathersJS Developer', 'getter is in place') // Check the setter clone.fullName = 'Marshall Me' assert.equal( `${clone.firstName} ${clone.lastName}`, 'Marshall Me', 'Setter is in place' ) // Commit the clone clone.commit() //Check the getter person.firstName = 'FeathersJS' person.lastName = 'Developer' assert.equal(person.fullName, 'FeathersJS Developer', 'getter is in place') // Check the setter person.fullName = 'Scooby Doo' assert.equal( `${person.firstName} ${person.lastName}`, 'Scooby Doo', 'Setter is in place' ) }) it('instanceDefaults in place after patch', async function() { const { Letter, store, lettersService } = makeLetterContext() let letter = new Letter({ name: 'Garmadon', age: 1025 }) letter = await letter.save() assert.equal(typeof letter.to, 'string', 'default to field still in place') assert.equal(typeof letter.status, 'string', 'accessor prop still in place') letter = await letter.save() assert.equal(typeof letter.to, 'string', 'default to field still in place') assert.equal(typeof letter.status, 'string', 'accessor prop still in place') }) }) ================================================ FILE: test/service-module/model-methods.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { ServiceState } from './types' import { assert } from 'chai' import feathersVuex from '../../src/index' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import Vuex from 'vuex' import { clearModels } from '../../src/service-module/global-models' import memory from 'feathers-memory' import { makeStore } from '../test-utils' import { isDate } from 'date-fns' require('events').EventEmitter.prototype._maxListeners = 100 interface TodoState extends ServiceState { test: any test2: { test: boolean } isTrue: boolean } interface RootState { ['model-methods-persons']: ServiceState ['model-methods-todos']: TodoState ['model-methods-tasks']: ServiceState tests: ServiceState blah: ServiceState things: ServiceState } function makeContext() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'model-methods' }) const serialize = context => { context.data = JSON.parse(JSON.stringify(context.data)) } const deserialize = context => { context.result = JSON.parse(JSON.stringify(context.result)) } feathersClient.use('model-methods-letters', memory()) const lettersService = feathersClient.service('model-methods-letters') // Setup hooks on letters service to simulate toJSON serialization that occurs // with a remote API request. lettersService.hooks({ before: { create: [serialize], update: [serialize], patch: [serialize] }, after: { create: [deserialize], patch: [deserialize], update: [deserialize] } }) class Task extends BaseModel { public static modelName = 'Task' public static servicePath: 'model-methods-tasks' public constructor(data?, options?) { super(data, options) } } class Todo extends BaseModel { public static modelName = 'Todo' public static servicePath: 'model-methods-todos' public constructor(data?, options?) { super(data, options) } } class Letter extends BaseModel { public constructor(data?, options?) { super(data, options) } public static modelName = 'Letter' public static servicePath = 'model-methods-letters' public static instanceDefaults(data, { models, store }) { return { to: '', from: '' } } public static setupInstance(data, { models }) { if (typeof data.createdAt === 'string') { data.createdAt = new Date(data.createdAt) // just assuming the date is formatted correctly ;) } return data } public get status() { return 'pending' } } class Person extends BaseModel { public static modelName = 'Person' public static servicePath = 'model-methods-persons' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ strict: true, plugins: [ makeServicePlugin({ Model: Task, servicePath: 'model-methods-tasks', service: feathersClient.service('model-methods-tasks'), preferUpdate: true, namespace: 'model-methods-tasks' }), makeServicePlugin({ Model: Todo, servicePath: 'model-methods-todos', service: feathersClient.service('model-methods-todos'), namespace: 'model-methods-todos' }), makeServicePlugin({ Model: Letter, servicePath: 'model-methods-letters', service: feathersClient.service('model-methods-letters'), namespace: 'model-methods-letters' }), makeServicePlugin({ Model: Person, servicePath: 'model-methods-persons', service: feathersClient.service('model-methods-persons'), keepCopiesInStore: true, namespace: 'model-methods-persons' }) ] }) // Fake server call feathersClient.service('model-methods-tasks').hooks({ before: { create: [ context => { delete context.data.__id delete context.data.__isTemp }, context => { context.result = { _id: 24, ...context.data } return context } ], update: [ context => { context.result = { ...context.data } return context } ], patch: [ context => { context.result = { ...context.data } return context } ], remove: [ context => { context.result = {} return context } ] } }) // Fake server call feathersClient.service('model-methods-persons').hooks({ before: { create: [ context => { delete context.data.__id delete context.data.__isTemp }, context => { context.result = { _id: 24, ...context.data } return context } ], update: [ context => { context.result = { ...context.data } return context } ], patch: [ context => { context.result = { ...context.data } return context } ], remove: [ context => { context.result = {} return context } ] } }) return { BaseModel, Task, Todo, Letter, Person, lettersService, store } } export { makeContext } describe('Models - Methods', function () { beforeEach(() => { clearModels() }) it('Model.find is a function', function () { const { Task } = makeContext() assert(typeof Task.find === 'function') }) it('Model.find returns a Promise', function () { const { Task } = makeContext() const result = Task.find() assert(typeof result.then !== 'undefined') result.catch(err => { /* noop -- prevents UnhandledPromiseRejectionWarning */ }) }) it('Model.findInStore', function () { const { Task } = makeContext() assert(typeof Task.findInStore === 'function') }) it('Model.count is a function', function () { const { Task } = makeContext() assert(typeof Task.count === 'function') }) it('Model.count returns a Promise', function () { const { Task } = makeContext() const result = Task.count({ query: {} }) assert(typeof result.then !== 'undefined') result.catch(err => { /* noop -- prevents UnhandledPromiseRejectionWarning */ }) }) it('Model.countInStore', function () { const { Task } = makeContext() assert(typeof Task.countInStore === 'function') }) it('Model.get', function () { const { Task } = makeContext() assert(typeof Task.get === 'function') }) it('Model.getFromStore', function () { const { Task } = makeContext() assert(typeof Task.getFromStore === 'function') }) it('allows listening to Feathers events on Model', function (done) { const { Letter } = makeContext() Letter.on('created', data => { assert(data.to === 'Santa', 'received event with data') done() }) // This should trigger an event from the bottom of make-service-plugin.ts const letter = new Letter({ from: 'Me', to: 'Santa' }).save() }) it('instance.save calls create with correct arguments', function () { const { Task } = makeContext() const task = new Task({ test: true }) Object.defineProperty(task, 'create', { value(params) { assert(arguments.length === 1, 'should have only called with params') assert( params === undefined, 'no params should have been passed this time' ) } }) task.save() }) it('instance.save passes params to create', function () { const { Task } = makeContext() const task = new Task({ test: true }) let called = false Object.defineProperty(task, 'create', { value(params) { assert(arguments.length === 1, 'should have only called with params') assert(params.test, 'should have received params') called = true } }) task.save({ test: true }) assert(called, 'create should have been called') }) it('instance.save passes params to patch', function () { const { Todo } = makeContext() const todo = new Todo({ id: 1, test: true }) let called = false Object.defineProperty(todo, 'patch', { value(params) { assert(arguments.length === 1, 'should have only called with params') assert(params.test, 'should have received params') called = true } }) todo.save({ test: true }) assert(called, 'patch should have been called') }) it('instance.save passes params to update', function () { const { Task } = makeContext() Task.preferUpdate = true const task = new Task({ id: 1, test: true }) let called = false Object.defineProperty(task, 'update', { value(params) { assert(arguments.length === 1, 'should have only called with params') assert(params.test, 'should have received params') called = true } }) task.save({ test: true }) assert(called, 'update should have been called') }) it('instance.remove works with temp records', function () { const { Task, store } = makeContext() const task = new Task({ test: true }) const tempId = task.__id task.remove() assert( !store.state['model-methods-tasks'].tempsById[tempId], 'temp was removed' ) }) it('instance.remove removes cloned record from the store', async function () { const { Person, store } = makeContext() const person = new Person({ _id: 1, test: true }) const id = person._id // @ts-ignore const { copiesById } = store.state['model-methods-persons'] person.clone() assert(copiesById[id], 'clone exists') await person.remove() assert(!copiesById[id], 'clone was removed') }) it('instance.remove removes cloned record from Model.copiesById', async function () { const { Task } = makeContext() const task = new Task({ _id: 2, test: true }) const id = task._id task.clone() assert(Task.copiesById[id], 'clone exists') await task.remove() assert(!Task.copiesById[id], 'clone was removed') }) it('instance.remove for temp record removes cloned record from the store', function () { const { Person, store } = makeContext() const person = new Person({ test: true }) const tempId = person.__id // @ts-ignore const { copiesById } = store.state['model-methods-persons'] person.clone() assert(copiesById[tempId], 'clone exists') person.remove() assert(!copiesById[tempId], 'clone was removed') }) it('instance.remove for temp record removes cloned record from the Model.copiesById', function () { const { Task } = makeContext() const task = new Task({ test: true }) const tempId = task.__id task.clone() assert(Task.copiesById[tempId], 'clone exists') task.remove() assert(!Task.copiesById[tempId], 'clone was removed') }) it('removes clone and original upon calling clone.remove()', async function () { const { Person, store } = makeContext() const person = new Person({ _id: 1, test: true }) const id = person._id // @ts-ignore const { copiesById, keyedById } = store.state['model-methods-persons'] person.clone() assert(copiesById[id], 'clone exists') assert(keyedById[id], 'original exists') const clone = copiesById[id] await clone.remove() assert(!copiesById[id], 'clone was removed') assert(!keyedById[id], 'original was removed') }) it('instance methods still available in store data after updateItem mutation (or socket event)', async function () { const { Letter, store, lettersService } = makeContext() let letter = new Letter({ name: 'Garmadon', age: 1025 }) letter = await letter.save() assert.equal( typeof letter.save, 'function', 'saved instance has a save method' ) store.commit('model-methods-letters/updateItem', { id: letter.id, name: 'Garmadon / Dad', age: 1026 }) const letter2 = new Letter({ id: letter.id, name: 'Just Garmadon', age: 1027 }) assert.equal( typeof letter2.save, 'function', 'new instance has a save method' ) }) it('Dates remain as dates after changes', async function () { const { Letter, store, lettersService } = makeContext() let letter = new Letter({ name: 'Garmadon', age: 1025, createdAt: new Date().toString() }) assert(isDate(letter.createdAt), 'createdAt should be a date') letter = await letter.save() assert(isDate(letter.createdAt), 'createdAt should be a date') letter = await letter.save() assert(isDate(letter.createdAt), 'createdAt should be a date') }) it('instance.toJSON', function () { const { Task } = makeContext() const task = new Task({ id: 1, test: true }) Object.defineProperty(task, 'getter', { get() { return `got'er` } }) assert.equal(task.getter, `got'er`) const json = task.toJSON() assert(json, 'got json') }) it('Model pending status sets/clears for create/update/patch/remove', async function() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'model-methods' }) class PendingThing extends BaseModel { public static modelName = 'PendingThing' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: PendingThing, service: feathersClient.service('methods-pending-things') }) ] }) // Create instance const thing = new PendingThing({ description: 'pending test' }) const clone = thing.clone() assert(!!thing.__id, "thing has a tempId") assert(clone.__id === thing.__id, "clone has thing's tempId") // Manually set the result in a hook to simulate the server request. feathersClient.service('methods-pending-things').hooks({ before: { create: [ context => { context.result = { _id: 42, ...context.data } // Check pending status assert(thing.isCreatePending === true, 'isCreatePending set') assert(thing.isSavePending === true, 'isSavePending set') assert(thing.isPending === true, 'isPending set') // Check clone's pending status assert(clone.isCreatePending === true, 'isCreatePending set on clone') assert(clone.isSavePending === true, 'isSavePending set on clone') assert(clone.isPending === true, 'isPending set on clone') return context } ], update: [ context => { context.result = { ...context.data } // Check pending status assert(thing.isUpdatePending === true, 'isUpdatePending set') assert(thing.isSavePending === true, 'isSavePending set') assert(thing.isPending === true, 'isPending set') // Check clone's pending status assert(clone.isUpdatePending === true, 'isUpdatePending set on clone') assert(clone.isSavePending === true, 'isSavePending set on clone') assert(clone.isPending === true, 'isPending set on clone') return context } ], patch: [ context => { context.result = { ...context.data } // Check pending status assert(thing.isPatchPending === true, 'isPatchPending set') assert(thing.isSavePending === true, 'isSavePending set') assert(thing.isPending === true, 'isPending set') // Check clone's pending status assert(clone.isPatchPending === true, 'isPatchPending set on clone') assert(clone.isSavePending === true, 'isSavePending set on clone') assert(clone.isPending === true, 'isPending set on clone') return context } ], remove: [ context => { context.result = { ...context.data } // Check pending status assert(thing.isRemovePending === true, 'isRemovePending set') assert(thing.isSavePending === false, 'isSavePending clear on remove') assert(thing.isPending === true, 'isPending set') // Check clone's pending status assert(clone.isRemovePending === true, 'isRemovePending set on clone') assert(clone.isSavePending === false, 'isSavePending clear on remove on clone') assert(clone.isPending === true, 'isPending set on clone') return context } ] } }) // Create and verify status await thing.create() assert(thing.isCreatePending === false, 'isCreatePending cleared') assert(thing.isSavePending === false, 'isSavePending cleared') assert(thing.isPending === false, 'isPending cleared') assert(clone.isCreatePending === false, 'isCreatePending cleared on clone') assert(clone.isSavePending === false, 'isSavePending cleared on clone') assert(clone.isPending === false, 'isPending cleared on clone') // Update and verify status await thing.update() assert(thing.isUpdatePending === false, 'isUpdatePending cleared') assert(thing.isSavePending === false, 'isSavePending cleared') assert(thing.isPending === false, 'isPending cleared') assert(clone.isUpdatePending === false, 'isUpdatePending cleared on clone') assert(clone.isSavePending === false, 'isSavePending cleared on clone') assert(clone.isPending === false, 'isPending cleared on clone') // Patch and verify status await thing.patch() assert(thing.isPatchPending === false, 'isPatchPending cleared') assert(thing.isSavePending === false, 'isSavePending cleared') assert(thing.isPending === false, 'isPending cleared') assert(clone.isPatchPending === false, 'isPatchPending cleared on clone') assert(clone.isSavePending === false, 'isSavePending cleared on clone') assert(clone.isPending === false, 'isPending cleared on clone') // Remove and verify status await thing.remove() assert(thing.isRemovePending === false, 'isRemovePending cleared') assert(thing.isSavePending === false, 'isSavePending cleared') assert(thing.isPending === false, 'isPending cleared') assert(clone.isRemovePending === false, 'isRemovePending cleared on clone') assert(clone.isSavePending === false, 'isSavePending cleared on clone') assert(clone.isPending === false, 'isPending cleared on clone') }) }) ================================================ FILE: test/service-module/model-relationships.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' import feathersVuex, { models } from '../../src/index' import { clearModels } from '../../src/service-module/global-models' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import Vuex from 'vuex' describe('Models - `setupInstance` & Relatioships', function () { beforeEach(function () { clearModels() }) it('initializes instance with return value from setupInstance', function () { let calledSetupInstance = false const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'myApi' }) class Todo extends BaseModel { public static modelName = 'Todo' public id? public description: string public constructor(data, options?) { super(data, options) } } function setupInstance(instance, { models, store }): Todo { calledSetupInstance = true return Object.assign(instance, { extraProp: true }) } const store = new Vuex.Store({ strict: true, plugins: [ makeServicePlugin({ Model: Todo, service: feathersClient.service('service-todos'), setupInstance }) ] }) const createdAt = '2018-05-01T04:42:24.136Z' const todo = new Todo({ description: 'Go on a date.', isComplete: true, createdAt }) assert(calledSetupInstance, 'setupInstance was called') assert(todo.extraProp, 'got the extraProp') }) it('allows setting up relationships between models and other constructors', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'myApi' }) class Todo extends BaseModel { public static modelName = 'Todo' public id? public description: string public user: User public constructor(data, options?) { super(data, options) } } class User extends BaseModel { public static modelName = 'User' public _id: string public firstName: string public email: string } function setupInstance(instance, { models, store }): Todo { const { User } = models.myApi return Object.assign(instance, { // If instance.user exists, convert it to a User instance ...(instance.user && { user: new User(instance.user) }), // If instance.createdAt exists, convert it to an actual date ...(instance.createdAt && { createdAt: new Date(instance.createdAt) }) }) } const store = new Vuex.Store({ strict: true, plugins: [ makeServicePlugin({ Model: Todo, service: feathersClient.service('service-todos'), setupInstance }), makeServicePlugin({ Model: User, service: feathersClient.service('users'), idField: '_id' }) ] }) const todo = new Todo({ description: `Show Master Splinter what's up.`, isComplete: true, createdAt: '2018-05-01T04:42:24.136Z', user: { _id: 1, firstName: 'Michaelangelo', email: 'mike@tmnt.com' } }) // Check the date assert( typeof todo.createdAt === 'object', 'module.createdAt is an instance of object' ) assert( todo.createdAt.constructor.name === 'Date', 'module.createdAt is an instance of date' ) // Check the user assert(todo.user instanceof User, 'the user is an instance of User') const user = User.getFromStore(1) assert.equal(todo.user, user, 'user was added to the user store.') }) }) function makeContext() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'myApi' }) class Task extends BaseModel { public static modelName = 'Task' public static instanceDefaults() { return { id: null, description: '', isComplete: false } } public constructor(data, options?) { super(data, options) } } /** * This Model demonstrates how to use a dynamic set of instanceDefaults based on incoming data. */ class Todo extends BaseModel { public static modelName = 'Todo' public static instanceDefaults(data) { const priority = data.priority || 'normal' const defaultsByPriority = { normal: { description: '', isComplete: false, priority: '' }, high: { isHighPriority: true, priority: '' } } return defaultsByPriority[priority] } public static setupInstance(data, { models, store }) { const { Task, Item } = models.myApi return Object.assign(data, { ...(data.task && { task: new Task(data.task) }), ...(data.item && { item: new Item(data.item) }), ...(data.items && { items: data.items.map(item => new Item(item)) }) }) } public constructor(data, options?) { super(data, options) } } class Item extends BaseModel { public static modelName = 'Item' public get todos() { return BaseModel.models.Todo.findInStore({ query: {} }).data } public static instanceDefaults() { return { test: false, todo: 'Todo' } } public static setupInstance(data, { models, store }) { const { Todo } = models.myApi return Object.assign(data, { ...(data.todo && { todo: new Todo(data.todo) }) }) } public constructor(data, options?) { super(data, options) } } const store = new Vuex.Store({ strict: true, plugins: [ makeServicePlugin({ Model: Task, service: feathersClient.service('tasks') }), makeServicePlugin({ Model: Todo, service: feathersClient.service('service-todos') }), makeServicePlugin({ Model: Item, service: feathersClient.service('items'), mutations: { toggleTestBoolean(state, item) { item.test = !item.test } } }) ] }) return { makeServicePlugin, BaseModel, store, Todo, Task, Item } } describe('Models - Relationships', function () { beforeEach(function () { clearModels() }) it('can have different instanceDefaults based on new instance data', function () { const { Todo } = makeContext() const normalTodo = new Todo({ description: 'Normal' }) const highPriorityTodo = new Todo({ description: 'High Priority', priority: 'high' }) assert( !normalTodo.hasOwnProperty('isHighPriority'), 'Normal todos do not have an isHighPriority default attribute' ) assert( highPriorityTodo.isHighPriority, 'High priority todos have a unique attribute' ) }) it('adds model instances containing an id to the store', function () { const { Todo, Task } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true } }) assert.deepEqual( Task.getFromStore(1), todo.task, 'task was added to the store' ) }) it('works with multiple keys that match Model names', function () { const { Todo, Task, Item } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true }, item: { id: 2, test: true } }) assert.deepEqual( Task.getFromStore(1), todo.task, 'task was added to the store' ) assert.deepEqual( Item.getFromStore(2), todo.item, 'item was added to the store' ) }) it('handles nested relationships', function () { const { Todo } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true }, item: { id: 2, test: true, todo: { description: 'nested todo under item' } } }) assert( todo.item.todo.constructor.name === 'Todo', 'the nested todo is an instance of Todo' ) }) it('handles circular nested relationships', function () { const { Todo, Item } = makeContext() const todo = new Todo({ id: 1, description: 'todo description', item: { id: 2, test: true, todo: { id: 1, description: 'todo description' } } }) assert.deepEqual(Todo.getFromStore(1), todo, 'todo was added to the store') assert.deepEqual( Item.getFromStore(2), todo.item, 'item was added to the store' ) assert(todo.item, 'todo still has an item') assert(todo.item.todo, 'todo still nested in itself') }) it('updates related data', function () { const { Todo, Item, store } = makeContext() const module = new Todo({ id: 'todo-1', description: 'todo description', item: { id: 'item-2', test: true, todo: { id: 'todo-1', description: 'todo description' } } }) const storedTodo = Todo.getFromStore('todo-1') const storedItem = Item.getFromStore('item-2') store.commit('items/toggleTestBoolean', storedItem) // module.item.test = false assert.equal( module.item.test, false, 'the nested module.item.test should be false' ) assert.equal( storedTodo.item.test, false, 'the nested item.test should be false' ) assert.equal(storedItem.test, false, 'item.test should be false') }) it(`allows creating more than once relational instance`, function () { const { Todo, Item } = makeContext() const todo1 = new Todo({ id: 'todo-1', description: 'todo description', item: { id: 'item-2', test: true } }) const todo2 = new Todo({ id: 'todo-2', description: 'todo description', item: { id: 'item-3', test: true } }) const storedTodo = Todo.getFromStore('todo-1') const storedItem = Item.getFromStore('item-2') assert.equal( todo1.item.test, true, 'the nested module.item.test should be true' ) assert.equal( todo2.item.test, true, 'the nested module.item.test should be true' ) assert.equal( storedTodo.item.test, true, 'the nested item.test should be true' ) assert.equal(storedItem.test, true, 'item.test should be true') }) it(`handles arrays of related data`, function () { const { Todo, Item } = makeContext() const todo1 = new Todo({ id: 'todo-1', description: 'todo description', items: [ { id: 'item-1', test: true }, { id: 'item-2', test: true } ] }) const todo2 = new Todo({ id: 'todo-2', description: 'todo description', items: [ { id: 'item-3', test: true }, { id: 'item-4', test: true } ] }) assert(todo1, 'todo1 is an instance') assert(todo2, 'todo2 is an instance') const storedTodo1 = Todo.getFromStore('todo-1') const storedTodo2 = Todo.getFromStore('todo-2') const storedItem1 = Item.getFromStore('item-1') const storedItem2 = Item.getFromStore('item-2') const storedItem3 = Item.getFromStore('item-3') const storedItem4 = Item.getFromStore('item-4') assert(storedTodo1, 'should have todo 1') assert(storedTodo2, 'should have todo 2') assert(storedItem1, 'should have item 1') assert(storedItem2, 'should have item 2') assert(storedItem3, 'should have item 3') assert(storedItem4, 'should have item 4') }) it('preserves relationships on clone', function () { const { Todo, Task } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true } }) const clone = todo.clone() assert(clone.task instanceof Task, 'nested task is a Task') }) it('preserves relationships on commit', function () { const { Todo, Task } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true } }) const clone = todo.clone() const original = clone.commit() assert(original.task instanceof Task, 'nested task is a Task') }) it('preserves relationship with nested data clone and commit', function () { const { Todo } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true } }) // Create a clone of the nested task, modify and commit. const taskClone = todo.task.clone() taskClone.isComplete = false taskClone.commit() assert.equal(todo.task.isComplete, false, 'preserved after clone') }) it('returns the same object when an instance is cloned twice', function () { const { Todo } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true } }) const clone1 = todo.clone() const clone2 = todo.clone() assert(clone1 === clone2, 'there should only ever be one clone in memory for an instance with the same id') }) it('on clone, nested instances do not get cloned', function () { const { Todo } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true } }) const todoClone = todo.clone() assert(todoClone.task.__isClone === undefined, 'todo.task should still be the original item and not the clone') }) it('on nested commit in instance, original nested instances get updated', function () { const { Todo } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true } }) const taskClone = todo.task.clone() taskClone.description = 'changed' taskClone.commit() assert(todo.task.description === 'changed', 'the nested task should have been updated') }) it('nested instances get updated in clones and original records', function () { const { Todo } = makeContext() const todo = new Todo({ task: { id: 1, description: 'test', isComplete: true } }) const todoClone = todo.clone() assert(todo.task === todoClone.task, 'the same task instance should be in both the original and clone') }) }) ================================================ FILE: test/service-module/model-serialize.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' import feathersVuex from '../../src/index' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import { clearModels } from '../../src/service-module/global-models' import _omit from 'lodash/omit' import Vuex from 'vuex' describe('Models - Serialize', function () { beforeEach(() => { clearModels() }) it('allows customizing toJSON', function () { const { BaseModel, makeServicePlugin } = feathersVuex(feathersClient, { serverAlias: 'myApi' }) class Task extends BaseModel { public static modelName = 'Task' public static instanceDefaults() { return { id: null, description: '', isComplete: false } } public toJSON() { return _omit(this, ['isComplete']) } public constructor(data, options?) { super(data, options) } } const servicePath = 'thingies' const plugin = makeServicePlugin({ servicePath: 'thingies', Model: Task, service: feathersClient.service(servicePath) }) new Vuex.Store({ plugins: [plugin] }) const task = new Task({ description: 'Hello, World!', isComplete: true }) assert(!task.toJSON().hasOwnProperty('isComplete'), 'custom toJSON worked') }) }) ================================================ FILE: test/service-module/model-temp-ids.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { ServiceState } from './types' import { assert } from 'chai' import feathersVuex from '../../src/index' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import { clearModels } from '../../src/service-module/global-models' import { Service as MemoryService } from 'feathers-memory' import Vue from 'vue/dist/vue' import Vuex from 'vuex' import { makeStore } from '../test-utils' import ObjectID from 'bson-objectid' import fastCopy from 'fast-copy' interface RootState { transactions: ServiceState things: ServiceState } class ComicService extends MemoryService { public create(data, params) { return super.create(data, params).then(response => { delete response.__id delete response.__isTemp return response }) } // @ts-ignore public update(id, data, params) { data.createdAt = new Date() // this._super(data, params, callback) } } function makeContext() { feathersClient.use( 'comics', // @ts-ignore new ComicService({ store: makeStore() }) ) const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'model-temp-ids' }) class Comic extends BaseModel { public static modelName = 'Comic' public static test = true public constructor(data, options?) { super(data, options) } } const store = new Vuex.Store({ strict: true, plugins: [ makeServicePlugin({ Model: Comic, service: feathersClient.service('comics'), servicePath: 'comics' }) ] }) return { makeServicePlugin, BaseModel, Comic, store } } describe('Models - Temp Ids', function () { beforeEach(() => { clearModels() }) it('adds tempIds for items without an [idField]', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class Transaction extends BaseModel { public static modelName = 'Transaction' public constructor(data?, options?) { super(data, options) } } new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Transaction, service: feathersClient.service('transactions') }) ] }) const txn = new Transaction({ description: 'Green Pasture - No More Dentists!', website: 'https://www.greenpasture.org', amount: 1.99 }) // Make sure we got an id. assert(txn.__id, 'the record got an __id') assert(txn.__isTemp, 'item is a temp') // It should be non-enumerable and non-writable const desc = Object.getOwnPropertyDescriptor(txn, '__id') assert(desc.enumerable, 'it is enumerable') }) it('allows specifying the value for the tempId', function () { const context = makeContext() const Comic = context.Comic const oid = new ObjectID().toHexString() const comic = new Comic({ __id: oid }) assert(comic.__isTemp, 'item is a temp') assert.equal(comic.__id, oid, 'the objectid was used') }) it('adds to state.tempsById', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class Transaction extends BaseModel { public static modelName = 'Transaction' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Transaction, service: feathersClient.service('transactions') }) ] }) const txn = new Transaction({ description: 'Amazon - Cure Teeth Book', website: 'https://www.amazon.com/Cure-Tooth-Decay-Cavities-Nutrition-ebook/dp/B004GB0JIM', amount: 1.99 }) // Make sure we got an id. assert(store.state.transactions.tempsById[txn.__id], 'it is in the store') }) it('maintains reference to temp item after save', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class Thing extends BaseModel { public static modelName = 'Thing' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Thing, service: feathersClient.service('things') }) ] }) // Manually set the result in a hook to simulate the server request. feathersClient.service('things').hooks({ before: { create: [ // Testing removing the __id and __isTemp so they're not sent to the server. context => { delete context.data.__id delete context.data.__isTemp }, context => { assert(!context.data.__id, '__id was not sent to API server') assert( !context.data.__isTemp, '__isTemp was not sent to API server' ) context.result = { _id: 1, description: 'Robb Wolf - the Paleo Solution', website: 'https://robbwolf.com/shop-old/products/the-paleo-solution-the-original-human-diet/', amount: 1.99 } return context } ] } }) const thing = new Thing({ description: 'Robb Wolf - the Paleo Solution', website: 'https://robbwolf.com/shop-old/products/the-paleo-solution-the-original-human-diet/', amount: 1.99 }) assert(store.state.things.tempsById[thing.__id], 'item is in the tempsById') return thing.save().then(response => { assert(response._id === 1) assert(response.__id, 'the temp id is still intact') assert(!store.state.things.tempsById[response.__id]) assert(response === thing, 'maintained the reference') }) }) it('removes uncreated temp', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class Thing extends BaseModel { public static modelName = 'Thing' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Thing, service: feathersClient.service('things') }) ] }) const thing = new Thing({ description: 'Robb Wolf - the Paleo Solution', website: 'https://robbwolf.com/shop-old/products/the-paleo-solution-the-original-human-diet/', amount: 1.99 }) assert(store.state.things.tempsById[thing.__id], 'item is in the tempsById') store.commit('things/removeTemps', [thing.__id]) assert(!store.state.things.tempsById[thing.__id], 'temp item was removed') }) it('clones into Model.copiesById', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class Transaction extends BaseModel { public static modelName = 'Transaction' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Transaction, service: feathersClient.service('transactions') }) ] }) const txn = new Transaction({ description: 'Robb Wolf - the Paleo Solution', website: 'https://robbwolf.com/shop-old/products/the-paleo-solution-the-original-human-diet/', amount: 1.99 }) txn.clone() assert(Transaction.copiesById[txn.__id], 'it is in the copiesById') }) it('commits into store.tempsById', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class Transaction extends BaseModel { public static modelName = 'Transaction' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Transaction, service: feathersClient.service('transactions') }) ] }) const txn = new Transaction({ description: 'Rovit Monthly Subscription', website: 'https://rovit.com', amount: 1.99 }) // Clone it, change it and commit it. const clone = txn.clone() clone.amount = 11.99 clone.commit() const originalTemp = store.state.transactions.tempsById[txn.__id] assert.equal(originalTemp.amount, 11.99, 'original was updated') }) it('can reset a temp clone', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'temp-ids' }) class Transaction extends BaseModel { public static modelName = 'Transaction' public constructor(data?, options?) { super(data, options) } } new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Transaction, service: feathersClient.service('transactions') }) ] }) const txn = new Transaction({ description: 'Rovit Monthly Subscription', website: 'https://rovit.com', amount: 1.99 }) // Clone it, change it and commit it. const clone = txn.clone() clone.amount = 11.99 clone.reset() assert.equal(clone.amount, 1.99, 'clone was reset') }) it('returns the keyedById record after create, not the tempsById record', function (done) { const { Comic, store } = makeContext() const comic = new Comic({ name: 'The Uncanny X-Men', year: 1969 }) // Create a temp and make sure it's in the tempsById const tempId = comic.__id // @ts-ignore assert(store.state.comics.tempsById[tempId]) assert(comic.__isTemp) comic .save() .then(response => { assert(!response.hasOwnProperty('__isTemp')) // The comic record is no longer in tempsById // @ts-ignore assert(!store.state.comics.tempsById[tempId], 'temp is gone') // The comic record moved to keyedById // @ts-ignore assert(store.state.comics.keyedById[response.id], 'now a real record') done() }) .catch(done) }) it('removes __isTemp from temp and clone', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class Thing extends BaseModel { public static modelName = 'Thing' } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Thing, service: feathersClient.service('things') }) ] }) const thing = new Thing() assert(thing.__isTemp, 'thing has __isTemp') const clone = thing.clone() assert(clone.__isTemp, 'Clone also has __isTemp') store.commit('things/updateTemp', { id: 42, tempId: thing.__id }) assert(!thing.hasOwnProperty('__isTemp'), '__isTemp was removed from thing') assert(!clone.hasOwnProperty('__isTemp'), '__isTemp was removed from clone') }) it('updateTemp assigns ID to temp and migrates it from tempsById to keyedById', function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class Thing extends BaseModel { public static modelName = 'Thing' } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Thing, service: feathersClient.service('things') }) ] }) const thing = new Thing() assert(thing.__id, 'thing has tempId') assert(!thing._id, 'thing does not have _id') assert(store.state.things.tempsById[thing.__id], 'thing is in tempsById') store.commit('things/updateTemp', { id: 42, tempId: thing.__id }) assert(thing._id === 42, 'thing got _id') assert(store.state.things.keyedById[42] === thing, 'thing is in keyedById') assert(store.state.things.ids.includes(42), "thing's _id is in ids") assert( !store.state.things.tempsById[thing.__id], 'thing is no longer in tempsById' ) }) it('Clone gets _id after save (create only called once)', async function () { // Test ensures subsequent calls to clone.save() do not call create const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class Thing extends BaseModel { public static modelName = 'Thing' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Thing, service: feathersClient.service('things') }) ] }) // Manually set the result in a hook to simulate the server request. let createCalled = false feathersClient.service('things').hooks({ before: { create: [ context => { assert(createCalled === false, 'Create is only called once') createCalled = true context.result = { _id: 42, ...context.data } return context } ], patch: [ context => { assert(context.data.__isClone, 'Patch called on clone') assert(context.id === 42, 'context has correct ID') assert(context.data._id === 42, 'patch called with correct _id') assert( context.data.description === 'Thing 3', 'patch called with correct description' ) context.result = { ...context.data } return context } ] } }) // Create instance and clone const thing = new Thing({ description: 'Thing 1' }) const clone = thing.clone() assert(thing.__id === clone.__id, "clone has thing's tempId") assert(clone.description === 'Thing 1', "clone got thing's description") assert(!thing.hasOwnProperty('_id'), 'thing has no _id') assert(!clone.hasOwnProperty('_id'), 'clone has no _id') // Modify clone and save clone.description = 'Thing 2' const response = await clone.save() assert(response === thing, 'response from clone.save() is thing') assert(thing._id === 42, 'thing got _id') assert(thing.description === 'Thing 2', "thing got clone's changes") assert(clone._id === response._id, 'clone got _id') // Modify clone again and save again clone.description = 'Thing 3' const response2 = await clone.save() assert(response2 === thing, 'response2 is still thing') assert(thing.description === 'Thing 3', "thing got clone's new changes") }) it('find() getter does not return duplicates with temps: true', async function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class FooModel extends BaseModel { public static modelName = 'FooModel' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: FooModel, service: feathersClient.service('foos'), servicePath: 'foos' }) ] }) // Fake server call feathersClient.service('foos').hooks({ before: { create: [ context => { delete context.data.__id delete context.data.__isTemp }, context => { context.result = { _id: 24, ...context.data } return context } ] } }) // Create component with find() computed prop const watchEvents = [] new Vue({ template: `
`, computed: { things() { return store.getters['foos/find']({ query: { test: true }, temps: true }).data } }, watch: { things(items) { watchEvents.push(fastCopy(items)) } } }).$mount() const item = new FooModel({ test: true }) await item.save() assert(watchEvents.length > 0, 'watch fired at least once') watchEvents.forEach(items => { if (items.length === 2) { assert(items[0]._id !== items[1]._id, 'no duplicate id') assert(items[0].__id !== items[1].__id, 'no duplicate tempId') } }) }) it('Model pending status updated for tempIds and clones', async function() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { idField: '_id', serverAlias: 'temp-ids' }) class PendingThing extends BaseModel { public static modelName = 'PendingThing' public constructor(data?, options?) { super(data, options) } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: PendingThing, service: feathersClient.service('pending-things') }) ] }) // Create instance const thing = new PendingThing({ description: 'PendingThing 1' }) const clone = thing.clone() assert(!!thing.__id, 'thing has a tempId') assert(clone.__id === thing.__id, "clone has thing's tempId") // Manually set the result in a hook to simulate the server request. feathersClient.service('pending-things').hooks({ before: { create: [ context => { context.result = { _id: 42, ...context.data } // Check pending status assert(thing.isCreatePending === true, 'isCreatePending set') assert(thing.isSavePending === true, 'isSavePending set') assert(thing.isPending === true, 'isPending set') // Check clone's pending status assert(clone.isCreatePending === true, 'isCreatePending set') assert(clone.isSavePending === true, 'isSavePending set on clone') assert(clone.isPending === true, 'isPending set') return context } ] } }) // Save and verify status await thing.save() assert(thing.isCreatePending === false, 'isCreatePending cleared') assert(thing.isSavePending === false, 'isSavePending cleared') assert(thing.isPending === false, 'isPending cleared') assert(clone.isCreatePending === false, 'isCreatePending cleared') assert(clone.isSavePending === false, 'isSavePending cleared on clone') assert(clone.isPending === false, 'isPending cleared') }) }) ================================================ FILE: test/service-module/model-tests.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' interface ModelOptions { servicePath: string } describe('TypeScript Class Inheritance', () => { it('Can access static instanceDefaults from BaseModel', () => { abstract class BaseModel { public static instanceDefaults public constructor(data, options?) { const { instanceDefaults } = this.constructor as typeof BaseModel const defaults = instanceDefaults(data, options) assert( defaults.description === 'default description', 'We get defaults in the BaseModel constructor' ) Object.assign(this, defaults, data) } } class Todo extends BaseModel { public static modelName = 'Todo' public description: string public static instanceDefaults = (data, options) => ({ description: 'default description' }) public constructor(data, options?) { super(data, options) const { instanceDefaults } = this.constructor as typeof BaseModel const defaults = instanceDefaults(data, options) assert( defaults.description === 'default description', 'We get defaults in the Todo constructor, too' ) } } const todo = new Todo({ test: true }) assert( todo.description === 'default description', 'got default description' ) }) it('Can access static instanceDefaults from two levels of inheritance', () => { abstract class BaseModel { public static instanceDefaults public constructor(data, options?) { const { instanceDefaults } = this.constructor as typeof BaseModel const defaults = instanceDefaults(data, options) assert( defaults.description === 'default description', 'We get defaults in the BaseModel constructor' ) Object.assign(this, defaults, data) } } function makeServiceModel(options) { const { servicePath } = options class ServiceModel extends BaseModel { public static modelName = 'ServiceModel' public constructor(data, options: ModelOptions = { servicePath: '' }) { options.servicePath = servicePath super(data, options) } } return ServiceModel } class Todo extends makeServiceModel({ servicePath: 'todos' }) { public static modelName = 'Todo' public description: string public static instanceDefaults = (data, options) => ({ description: 'default description' }) } const todo = new Todo({ test: true }) assert( todo.description === 'default description', 'got default description' ) }) it('Can access static servicePath from Todo in BaseModel', () => { abstract class BaseModel { public static instanceDefaults public static servicePath public static namespace public constructor(data, options?) { const { instanceDefaults, servicePath, namespace } = this .constructor as typeof BaseModel const defaults = instanceDefaults(data, options) assert( defaults.description === 'default description', 'We get defaults in the BaseModel constructor' ) Object.assign(this, defaults, data, { _options: { namespace, servicePath } }) } } class Todo extends BaseModel { public static modelName = 'Todo' public static namespace: string = 'todos' public static servicePath: string = 'v1/todos' public description: string public _options public static instanceDefaults = (data, models) => ({ description: 'default description' }) } const todo = new Todo({ test: true }) assert(todo._options.servicePath === 'v1/todos', 'got static servicePath') }) it('cannot serialize instance methods', () => { class BaseModel { public clone() { return this } public constructor(data) { Object.assign(this, data) } } class Todo extends BaseModel { public static modelName = 'Todo' public serialize() { return Object.assign({}, this, { serialized: true }) } } const todo = new Todo({ name: 'test' }) const json = JSON.parse(JSON.stringify(todo)) assert(!json.clone) assert(!json.serialize) }) }) ================================================ FILE: test/service-module/service-module.actions.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { ServiceState } from './types' import { assert } from 'chai' import feathersVuex from '../../src/index' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import Vuex, { mapActions } from 'vuex' import memory from 'feathers-memory' import { clearModels } from '../../src/service-module/global-models' import { makeStore, makeStoreWithAtypicalIds } from '../test-utils' interface RootState { 'my-todos': ServiceState 'my-tasks': ServiceState broken: ServiceState } interface NumberedList { 0?: {} 1?: {} } function makeContext() { feathersClient.use( 'my-todos', memory({ store: makeStore() }) ) feathersClient.use( 'my-tasks', memory({ store: makeStore(), paginate: { default: 10, max: 50 } }) ) const todoService = feathersClient.service('my-todos') const taskService = feathersClient.service('my-tasks') const noIdService = feathersClient.use( 'no-ids', memory({ store: makeStoreWithAtypicalIds(), paginate: { default: 10, max: 50 } }) ) const brokenService = feathersClient.use('broken', { find() { return Promise.reject(new Error('find error')) }, get() { return Promise.reject(new Error('get error')) }, create() { return Promise.reject(new Error('create error')) }, update() { return Promise.reject(new Error('update error')) }, patch() { return Promise.reject(new Error('patch error')) }, remove() { return Promise.reject(new Error('remove error')) }, // eslint-disable-next-line @typescript-eslint/no-empty-function setup() {} }) const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'service-module-actions' }) class Todo extends BaseModel { public static modelName = 'Todo' public static test = true } class Task extends BaseModel { public static modelName = 'Task' public static test = true } class NoId extends BaseModel { public static modelName = 'NoId' public static test = true } class Broken extends BaseModel { public static modelName = 'Broken' public static test = true } return { makeServicePlugin, BaseModel, todoService, taskService, noIdService, brokenService, Todo, Task, NoId, Broken } } const assertRejected = (promise, done, callback) => { // resolve handler promise.then( () => done(new Error('expected promise to be rejected')), // reject handler () => { try { callback() done() } catch (e) { done(e) } } ) } describe('Service Module - Actions', () => { beforeEach(() => { clearModels() }) describe('Find', () => { describe('without pagination', () => { it('Find without pagination', done => { const { makeServicePlugin, Todo } = makeContext() const todosPlugin = makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) const store = new Vuex.Store({ plugins: [todosPlugin] }) const todoState = store.state['my-todos'] const actions = mapActions('my-todos', ['find']) assert(todoState.ids.length === 0, 'no ids before find') assert(todoState.errorOnFind === null, 'no error before find') assert(todoState.isFindPending === false, 'isFindPending is false') assert(todoState.idField === 'id', 'idField is `id`') actions.find.call({ $store: store }, {}).then(response => { assert(todoState.ids.length === 10, 'three ids populated') assert(todoState.errorOnFind === null, 'errorOnFind still null') assert(todoState.isFindPending === false, 'isFindPending is false') const expectedKeyedById: NumberedList = makeStore() const currentKeyedById = JSON.parse( JSON.stringify(todoState.keyedById) ) assert.deepEqual( currentKeyedById, expectedKeyedById, 'keyedById matches' ) assert( typeof todoState.keyedById[1].save === 'function', 'added FeathersVuexModel class methods to the data' ) done() }) // Make sure proper state changes occurred before response assert(todoState.ids.length === 0) assert(todoState.errorOnFind === null) assert(todoState.isFindPending === true) assert.deepEqual(todoState.keyedById, {}) }) it('find with limit', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const actions = mapActions('my-todos', ['find']) actions.find .call({ $store: store }, { query: { $limit: 1 } }) .then(response => { const returnedRecord = JSON.parse(JSON.stringify(response[0])) assert(response.length === 1, 'only one record was returned') assert.deepEqual( returnedRecord, { id: 0, description: 'Do the first', isComplete: false }, 'the first record was returned' ) done() }) }) it('find with $select', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const actions = mapActions('my-todos', ['find']) actions.find .call( { $store: store }, { query: { $limit: 1, $select: ['id', 'description'] } } ) .then(response => { const returnedRecord = JSON.parse(JSON.stringify(response[0])) assert(response.length === 1, 'only one record was returned') assert.deepEqual( returnedRecord, { id: 0, description: 'Do the first' }, 'the first record was returned' ) done() }) }) it('find with skip', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const actions = mapActions('my-todos', ['find']) actions.find .call({ $store: store }, { query: { $skip: 9 } }) .then(response => { const returnedRecord = JSON.parse(JSON.stringify(response[0])) assert(response.length === 1, 'one record was returned') assert.deepEqual( returnedRecord, { id: 9, description: 'Do the tenth', isComplete: false }, 'the tenth record was returned' ) done() }) }) it('Find with limit and skip', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const actions = mapActions('my-todos', ['find']) actions.find .call({ $store: store }, { query: { $limit: 1, $skip: 8 } }) .then(response => { const returnedRecord = JSON.parse(JSON.stringify(response[0])) assert(response.length === 1, 'one record was returned') assert.deepEqual( returnedRecord, { id: 8, description: 'Do the ninth', isComplete: false }, 'the ninth record was returned' ) done() }) }) }) describe('with pagination', () => { it('find with limit', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['find']) actions.find .call({ $store: store }, { query: { $limit: 1 } }) .then(response => { const returnedRecord = JSON.parse(JSON.stringify(response.data[0])) assert(response.data.length === 1, 'only one record was returned') assert.deepEqual( returnedRecord, { id: 0, description: 'Do the first', isComplete: false }, 'the first record was returned' ) assert(response.limit === 1, 'limit was correct') assert(response.skip === 0, 'skip was correct') assert(response.total === 10, 'total was correct') done() }) }) it('find with $select', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['find']) actions.find .call( { $store: store }, { query: { $limit: 1, $select: ['id', 'description'] } } ) .then(response => { const returnedRecord = JSON.parse(JSON.stringify(response.data[0])) assert(response.data.length === 1, 'only one record was returned') assert.deepEqual( returnedRecord, { id: 0, description: 'Do the first' }, 'the first record was returned' ) assert(response.limit === 1, 'limit was correct') assert(response.skip === 0, 'skip was correct') assert(response.total === 10, 'total was correct') done() }) }) it('find with skip', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['find']) actions.find .call({ $store: store }, { query: { $skip: 9 } }) .then(response => { const returnedRecord = JSON.parse(JSON.stringify(response.data[0])) assert(response.data.length === 1, 'only one record was returned') assert.deepEqual( returnedRecord, { id: 9, description: 'Do the tenth', isComplete: false }, 'the tenth record was returned' ) assert(response.limit === 10, 'limit was correct') assert(response.skip === 9, 'skip was correct') assert(response.total === 10, 'total was correct') done() }) }) it('find with limit and skip', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['find']) actions.find .call({ $store: store }, { query: { $limit: 1, $skip: 8 } }) .then(response => { const returnedRecord = JSON.parse(JSON.stringify(response.data[0])) assert(response.data.length === 1, 'only one record was returned') assert.deepEqual( returnedRecord, { id: 8, description: 'Do the ninth', isComplete: false }, 'the ninth record was returned' ) assert(response.limit === 1, 'limit was correct') assert(response.skip === 8, 'skip was correct') assert(response.total === 10, 'total was correct') done() }) }) it('adds default pagination data to the store', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['find']) actions.find.call({ $store: store }, { query: {} }).then(() => { const { default: d } = store.state['my-tasks'].pagination assert(d.mostRecent) assert(d.mostRecent.queriedAt) assert(d.mostRecent.query) assert(d.mostRecent.queryId === '{}') assert(d.mostRecent.queryParams) assert(d.mostRecent.pageId === '{"$limit":10,"$skip":0}') assert.deepEqual(d.mostRecent.pageParams, { $limit: 10, $skip: 0 }) assert(d['{}']) assert(d['{}'].queryParams) assert(d['{}'].total === 10) assert(d['{}']['{"$limit":10,"$skip":0}']) assert(d['{}']['{"$limit":10,"$skip":0}'].ids.length === 10) assert(d['{}']['{"$limit":10,"$skip":0}'].queriedAt) assert.deepEqual(d['{}']['{"$limit":10,"$skip":0}'].pageParams, { $limit: 10, $skip: 0 }) done() }) }) it('can provide a query identifier to store pagination', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['find']) const qid = 'component-name' actions.find.call({ $store: store }, { query: {}, qid }).then(() => { const qidPaginationState = store.state['my-tasks'].pagination[qid] assert(qidPaginationState, 'got pagination state for qid') done() }) }) it('updates properly with limit and skip', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['find']) const qid = 'component-name' actions.find .call({ $store: store }, { query: { $limit: 5, $skip: 2 }, qid }) .then(response => { assert(store.state['my-tasks'].pagination[qid]) assert.deepEqual( store.state['my-tasks'].pagination[qid].mostRecent.query, { $limit: 5, $skip: 2 } ) done() }) }) it('works with multiple queries and identifiers', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['find']) const qids = ['component-query-zero', 'component-query-one'] actions.find actions.find .call({ $store: store }, { query: {}, qid: qids[0] }) .then(response => actions.find.call({ $store: store }, { query: {}, qid: qids[1] }) ) .then(response => { qids.forEach(qid => { assert(store.state['my-tasks'].pagination[qid]) }) done() }) }) it(`allows non-id'd data to pass through`, done => { const { makeServicePlugin, NoId } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'no-ids', Model: NoId, service: feathersClient.service('no-ids'), idField: '_id' }) ] }) const actions = mapActions('no-ids', ['find']) actions.find.call({ $store: store }, { query: {} }).then(response => { assert(response.data.length === 10, 'records were still returned') assert( store.state['no-ids'].ids.length === 0, 'no records were stored in the state' ) done() }) }) it(`runs the afterFind action`, done => { const { makeServicePlugin, NoId } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'no-ids', Model: NoId, service: feathersClient.service('no-ids'), idField: '_id', actions: { afterFind({}, response) { assert( response.data.length === 10, 'records were still returned' ) assert( store.state['no-ids'].ids.length === 0, 'no records were stored in the state' ) done() } } }) ] }) const actions = mapActions('no-ids', ['find']) actions.find.call({ $store: store }, { query: {} }) }) }) it('updates errorOnFind state on service failure', done => { const { makeServicePlugin, Broken } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'broken', Model: Broken, service: feathersClient.service('broken') }) ] }) const brokenState = store.state.broken const actions = mapActions('broken', ['find']) assertRejected(actions.find.call({ $store: store }, {}), done, () => { assert( brokenState.errorOnFind.message === 'find error', 'errorOnFind was set' ) assert(brokenState.isFindPending === false, 'pending state was cleared') assert(brokenState.ids.length === 0) }) // Make sure proper state changes occurred before response assert(brokenState.ids.length === 0) assert(brokenState.errorOnFind === null) assert(brokenState.isFindPending === true) }) }) describe('Count', () => { it('count without params fails', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['count']) try { actions.count.call({ $store: store }) } catch (err) { assert(err) done() } }) it('count with query returns number', done => { const { makeServicePlugin, Task } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-tasks', Model: Task, service: feathersClient.service('my-tasks') }) ] }) const actions = mapActions('my-tasks', ['count']) actions.count.call({ $store: store }, { query: {} }).then(response => { assert(response === 10, 'total is 10') done() }) }) }) describe('Get', function () { it('updates store list state on service success', async () => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const todoState = store.state['my-todos'] const actions = mapActions('my-todos', ['get']) assert(todoState.ids.length === 0) assert(todoState.errorOnGet === null) assert(todoState.isGetPending === false) assert(todoState.idField === 'id') const todo1 = await actions.get.call({ $store: store }, 0) assert(todoState.ids.length === 1, 'only one item is in the store') assert(todoState.errorOnGet === null, 'there was no errorOnGet') assert(todoState.isGetPending === false, 'isGetPending is set to false') let expectedKeyedById: NumberedList = { 0: { id: 0, description: 'Do the first', isComplete: false } } assert.deepEqual( JSON.parse(JSON.stringify(todoState.keyedById)), expectedKeyedById ) // Make a request with the array syntax that allows passing params const response2 = await actions.get.call({ $store: store }, [1, {}]) expectedKeyedById = { 0: { id: 0, description: 'Do the first', isComplete: false }, 1: { id: 1, description: 'Do the second', isComplete: false } } assert(response2.description === 'Do the second') assert.deepEqual( JSON.parse(JSON.stringify(todoState.keyedById)), expectedKeyedById ) // Edit the first record in the store so the data is different. // Make a request for the first record again, and it should be updated. const clone1 = todo1.clone() clone1.description = 'MODIFIED IN THE VUEX STORE' clone1.commit() assert.strictEqual( todoState.keyedById[0].description, clone1.description, 'the store instance was updated' ) const response3 = await actions.get.call({ $store: store }, [0, {}]) const todo0 = Todo.getFromStore(0) assert(response3.description === 'Do the first') assert.deepEqual( JSON.parse(JSON.stringify(todoState.keyedById)), expectedKeyedById, 'The data is back as it was on the API server' ) }) it('does not make remote call when skipRequestIfExists=true', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const todoState = store.state['my-todos'] const actions = mapActions('my-todos', ['get']) assert(todoState.ids.length === 0) assert(todoState.errorOnGet === null) assert(todoState.isGetPending === false) assert(todoState.idField === 'id') actions.get.call({ $store: store }, 0).then(() => { assert(todoState.ids.length === 1, 'only one item is in the store') assert(todoState.errorOnGet === null, 'there was no errorOnGet') assert(todoState.isGetPending === false, 'isGetPending is set to false') let expectedKeyedById: NumberedList = { 0: { id: 0, description: 'Do the first', isComplete: false } } assert.deepEqual( JSON.parse(JSON.stringify(todoState.keyedById)), expectedKeyedById ) // Make a request with the array syntax that allows passing params actions.get.call({ $store: store }, [1, {}]).then(response2 => { expectedKeyedById = { 0: { id: 0, description: 'Do the first', isComplete: false }, 1: { id: 1, description: 'Do the second', isComplete: false } } assert(response2.description === 'Do the second') assert.deepEqual( JSON.parse(JSON.stringify(todoState.keyedById)), expectedKeyedById ) // Make a request to an existing record and return the existing data first, then update `keyedById` todoState.keyedById = { 0: { id: 0, description: 'Do the FIRST', isComplete: false }, // twist the data to see difference 1: { id: 1, description: 'Do the second', isComplete: false } } actions.get .call({ $store: store }, [0, { skipRequestIfExists: true }]) .then(response3 => { expectedKeyedById = { 0: { id: 0, description: 'Do the FIRST', isComplete: false }, 1: { id: 1, description: 'Do the second', isComplete: false } } assert(response3.description === 'Do the FIRST') assert.deepEqual( JSON.parse(JSON.stringify(todoState.keyedById)), expectedKeyedById ) // The remote data will never arriive setTimeout(() => { expectedKeyedById = { 0: { id: 0, description: 'Do the FIRST', isComplete: false }, 1: { id: 1, description: 'Do the second', isComplete: false } } assert.deepEqual( JSON.parse(JSON.stringify(todoState.keyedById)), expectedKeyedById ) done() }, 100) }) }) }) // Make sure proper state changes occurred before response assert(todoState.ids.length === 0) assert(todoState.errorOnCreate === null) assert(todoState.isGetPending === true) assert.deepEqual(JSON.parse(JSON.stringify(todoState.keyedById)), {}) }) it('updates errorOnGet state on service failure', done => { const { makeServicePlugin, Broken } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'broken', Model: Broken, service: feathersClient.service('broken') }) ] }) const brokenState = store.state.broken const actions = mapActions('broken', ['get']) assertRejected(actions.get.call({ $store: store }, {}), done, () => { assert( brokenState.errorOnGet.message === 'get error', 'errorOnGet was set' ) assert(brokenState.isGetPending === false, 'pending state was cleared') assert(brokenState.ids.length === 0) }) // Make sure proper state changes occurred before response assert(brokenState.ids.length === 0) assert(brokenState.errorOnGet === null) assert(brokenState.isGetPending === true) }) }) describe('Create', function () { it('updates store list state on service success', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const todoState = store.state['my-todos'] const actions = mapActions('my-todos', ['create']) actions.create .call({ $store: store }, { description: 'Do the second' }) .then(response => { assert(todoState.ids.length === 1) assert(todoState.errorOnCreate === null) assert(todoState.isCreatePending === false) assert.deepEqual(todoState.keyedById[response.id], response) done() }) // Make sure proper state changes occurred before response assert(todoState.ids.length === 0) assert(todoState.errorOnCreate === null) assert(todoState.isCreatePending === true) assert(todoState.idField === 'id') assert.deepEqual(JSON.parse(JSON.stringify(todoState.keyedById)), {}) }) it('updates errorOnCreate state on service failure', done => { const { makeServicePlugin, Broken } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'broken', Model: Broken, service: feathersClient.service('broken') }) ] }) const brokenState = store.state.broken const actions = mapActions('broken', ['create']) assertRejected(actions.create.call({ $store: store }, {}), done, () => { assert( brokenState.errorOnCreate.message === 'create error', 'errorOnCreate was set' ) assert( brokenState.isCreatePending === false, 'pending state was cleared' ) assert(brokenState.ids.length === 0) }) // Make sure proper state changes occurred before response assert(brokenState.ids.length === 0) assert(brokenState.errorOnCreate === null) assert(brokenState.isCreatePending === true) }) }) describe('Update', () => { it('updates store list state on service success', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const todoState = store.state['my-todos'] const actions = mapActions('my-todos', ['create', 'update']) actions.create .call({ $store: store }, { description: 'Do the second' }) .then(() => { actions.update .call({ $store: store }, [ 0, { id: 0, description: 'Do da dishuz' } ]) .then(responseFromUpdate => { assert(todoState.ids.length === 1) assert(todoState.errorOnUpdate === null) assert(todoState.isUpdatePending === false) assert(store.getters['my-todos/isUpdatePendingById'](0) === false, 'ID pending update clear') assert(store.getters['my-todos/isSavePendingById'](0) === false, 'ID pending save clear') assert(store.getters['my-todos/isPendingById'](0) === false, 'ID pending clear') assert.deepEqual( todoState.keyedById[responseFromUpdate.id], responseFromUpdate ) done() }) // Make sure proper state changes occurred before response assert(todoState.ids.length === 1) assert(todoState.errorOnUpdate === null) assert(todoState.isUpdatePending === true) assert(store.getters['my-todos/isUpdatePendingById'](0) === true, 'ID pending update set') assert(store.getters['my-todos/isSavePendingById'](0) === true, 'ID pending save set') assert(store.getters['my-todos/isPendingById'](0) === true, 'ID pending set') assert(todoState.idField === 'id') }) .catch(error => { assert(!error, error) }) }) it('updates errorOnUpdate state on service failure', done => { const { makeServicePlugin, Broken } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'broken', Model: Broken, service: feathersClient.service('broken') }) ] }) const brokenState = store.state.broken const actions = mapActions('broken', ['update']) assertRejected( actions.update.call({ $store: store }, [0, { id: 0 }]), done, () => { assert( brokenState.errorOnUpdate.message === 'update error', 'errorOnUpdate was set' ) assert( brokenState.isUpdatePending === false, 'pending state was cleared' ) assert(brokenState.ids.length === 0) } ) // Make sure proper state changes occurred before response assert(brokenState.ids.length === 0) assert(brokenState.errorOnUpdate === null) assert(brokenState.isUpdatePending === true) }) }) describe('Patch', () => { it('updates only the changed properties', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const todoState = store.state['my-todos'] const actions = mapActions('my-todos', ['create', 'patch']) const dataUnchanged = { unchanged: true, deep: { changed: false, unchanged: true } } const dataChanged = { unchanged: true, deep: { changed: true, unchanged: true } } actions.create .call( { $store: store }, Object.assign({ description: 'Do the second' }, dataUnchanged) ) .then(() => { actions.patch .call({ $store: store }, [ 0, Object.assign({ description: 'Write a Vue app' }, dataChanged) ]) .then(responseFromPatch => { assert(todoState.ids.length === 1) assert(todoState.errorOnPatch === null) assert(todoState.isPatchPending === false) assert(store.getters['my-todos/isPatchPendingById'](0) === false, 'ID pending patch clear') assert(store.getters['my-todos/isSavePendingById'](0) === false, 'ID pending save clear') assert(store.getters['my-todos/isPendingById'](0) === false, 'ID pending clear') assert.deepEqual( todoState.keyedById[responseFromPatch.id], responseFromPatch ) done() }) // Make sure proper state changes occurred before response assert(todoState.ids.length === 1) assert(todoState.errorOnPatch === null) assert(todoState.isPatchPending === true) assert(store.getters['my-todos/isPatchPendingById'](0) === true, 'ID pending patch set') assert(store.getters['my-todos/isSavePendingById'](0) === true, 'ID pending save set') assert(store.getters['my-todos/isPendingById'](0) === true, 'ID pending set') assert(todoState.idField === 'id') }) }) it('overrides patch data with params.data', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const actions = mapActions('my-todos', ['create', 'patch']) const originalData = { description: 'Do something', test: true } actions.create.call({ $store: store }, originalData).then(() => { const data = { description: 'This description should not be patched since params.data is provided' } const params = { data: { test: false } } actions.patch .call({ $store: store }, [0, data, params]) .then(responseFromPatch => { assert.equal( responseFromPatch.description, originalData.description, 'description should not have changed' ) assert.equal( responseFromPatch.test, false, 'Providing params.data should have set the test attribute to false.' ) done() }) }) }) it('updates store state on service success', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const todoState = store.state['my-todos'] const actions = mapActions('my-todos', ['create', 'patch']) actions.create .call({ $store: store }, { description: 'Do the second' }) .then(() => { actions.patch .call({ $store: store }, [0, { description: 'Write a Vue app' }]) .then(responseFromPatch => { assert(todoState.ids.length === 1) assert(todoState.errorOnPatch === null) assert(todoState.isPatchPending === false) assert(store.getters['my-todos/isPatchPendingById'](0) === false, 'ID pending patch clear') assert(store.getters['my-todos/isSavePendingById'](0) === false, 'ID pending save clear') assert(store.getters['my-todos/isPendingById'](0) === false, 'ID pending clear') assert.deepEqual( todoState.keyedById[responseFromPatch.id], responseFromPatch ) done() }) // Make sure proper state changes occurred before response assert(todoState.ids.length === 1) assert(todoState.errorOnPatch === null) assert(todoState.isPatchPending === true) assert(store.getters['my-todos/isPatchPendingById'](0) === true, 'ID pending patch set') assert(store.getters['my-todos/isSavePendingById'](0) === true, 'ID pending save set') assert(store.getters['my-todos/isPendingById'](0) === true, 'ID pending set') assert(todoState.idField === 'id') }) }) it('updates errorOnPatch state on service failure', done => { const { makeServicePlugin, Broken } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'broken', Model: Broken, service: feathersClient.service('broken') }) ] }) const brokenState = store.state.broken const actions = mapActions('broken', ['patch']) assertRejected( actions.patch.call({ $store: store }, [0, { id: 0 }]), done, () => { assert( brokenState.errorOnPatch.message === 'patch error', 'errorOnPatch was set' ) assert( brokenState.isPatchPending === false, 'pending state was cleared' ) assert(brokenState.ids.length === 0) } ) // Make sure proper state changes occurred before response assert(brokenState.ids.length === 0) assert(brokenState.errorOnPatch === null) assert(brokenState.isPatchPending === true) }) }) describe('Remove', () => { it('updates store state on service success', done => { const { makeServicePlugin, Todo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'my-todos', Model: Todo, service: feathersClient.service('my-todos') }) ] }) const todoState = store.state['my-todos'] const actions = mapActions('my-todos', ['create', 'remove']) actions.create .call({ $store: store }, { description: 'Do the second' }) .then(() => { actions.remove .call({ $store: store }, 0) .then(() => { assert(todoState.ids.length === 0) assert(todoState.errorOnRemove === null) assert(todoState.isRemovePending === false) assert(store.getters['my-todos/isRemovePendingById'](0) === false, 'ID pending remove clear') assert(store.getters['my-todos/isSavePendingById'](0) === false, 'ID pending save clear') assert(store.getters['my-todos/isPendingById'](0) === false, 'ID pending clear') assert.deepEqual(todoState.keyedById, {}) done() }) .catch(error => { // eslint-disable-next-line no-console console.log(error) }) // Make sure proper state changes occurred before response assert(todoState.ids.length === 1) assert(todoState.errorOnRemove === null) assert(todoState.isRemovePending === true) assert(store.getters['my-todos/isRemovePendingById'](0) === true, 'ID pending remove set') assert(store.getters['my-todos/isSavePendingById'](0) === false, 'ID pending save clear') assert(store.getters['my-todos/isPendingById'](0) === true, 'ID pending set') assert(todoState.idField === 'id') }) }) it('updates errorOnRemove state on service failure', done => { const { makeServicePlugin, Broken } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'broken', Model: Broken, service: feathersClient.service('broken') }) ] }) const brokenState = store.state.broken const actions = mapActions('broken', ['remove']) assertRejected(actions.remove.call({ $store: store }, 0), done, () => { assert( brokenState.errorOnRemove.message === 'remove error', 'errorOnRemove was set' ) assert( brokenState.isRemovePending === false, 'pending state was cleared' ) assert(brokenState.ids.length === 0) }) // Make sure proper state changes occurred before response assert(brokenState.ids.length === 0) assert(brokenState.errorOnRemove === null) assert(brokenState.isRemovePending === true) }) }) }) ================================================ FILE: test/service-module/service-module.getters.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' import makeServiceGetters from '../../src/service-module/service-module.getters' import makeServiceMutations from '../../src/service-module/service-module.mutations' import makeServiceState from '../../src/service-module/service-module.state' import { globalModels, clearModels } from '../../src/service-module/global-models' import { values as _values } from 'lodash' const options = { idField: '_id', tempIdField: '__id', autoRemove: false, serverAlias: 'service-module-getters', Model: null, service: null } const { find, count, list, get, getCopyById, isCreatePendingById, isUpdatePendingById, isPatchPendingById, isRemovePendingById, isSavePendingById, isPendingById } = makeServiceGetters() const { addItems, setIdPending, unsetIdPending } = makeServiceMutations() describe('Service Module - Getters', function () { beforeEach(function () { const state = makeServiceState(options) this.items = [ { _id: 1, otherField: true, age: 21, teethRemaining: 2.501, test: true }, { _id: 2, name: 'Marshall', otherField: true, age: 24, teethRemaining: 2.5, test: true, movies: [{ actors: ['Jerry the Mouse'] }] }, { _id: 3, otherField: true, age: 27, teethRemaining: 12, test: false, movies: [{ actors: ['Tom Hanks', 'Tom Cruise', 'Tomcat'] }] }, { name: 'Mariah', age: 19, teethRemaining: 24, status: 'temp' } ] addItems(state, this.items) this.state = state }) it('list', function () { const { state, items } = this const results = list(state) results.forEach((record, index) => { const item = items[index] assert.deepEqual(record, item, 'item in correct order') }) }) it('getCopyById with keepCopiesInStore: true', function () { const state = { keepCopiesInStore: true, copiesById: { 1: { test: true } } } const result = getCopyById(state)(1) assert(result.test, 'got the copy') }) it('getCopyById with keepCopiesInStore: false', function () { const state = { keepCopiesInStore: false, servicePath: 'todos', serverAlias: 'my-getters-test' } Object.assign(globalModels, { [state.serverAlias]: { byServicePath: { todos: { copiesById: { 1: { test: true } } } } } }) const result = getCopyById(state)(1) assert(result.test, 'got the copy') clearModels() }) it('get works on keyedById', function () { const { state, items } = this const result = get(state)(1) assert.deepEqual(result, items[0]) }) it('get works on tempsById', function () { const { state } = this const tempId = Object.keys(state.tempsById)[0] const result = get(state)(tempId) assert(result.__id === tempId) }) it('find - no temps by default', function () { const { state, items } = this const params = { query: {} } const results = find(state)(params) assert.deepEqual( results.data, items.filter(i => i._id), 'the list was correct' ) assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 3, 'total was correct') }) it('find with temps', function () { const { state, items } = this // Set temps: false to skip the temps. const params = { query: {}, temps: true } const results = find(state)(params) assert.deepEqual(results.data, items, 'the list was correct') assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 4, 'total was correct') }) it('find - no copies by default', function () { const state = { keepCopiesInStore: false, servicePath: 'todos', serverAlias: 'my-getters-test', keyedById: { 1: { _id: 1, test: true, __isClone: false }, 2: { _id: 2, test: true, __isClone: false }, 3: { _id: 3, test: true, __isClone: false } }, copiesById: { 1: { _id: 1, test: true, __isClone: true } } } Object.assign(globalModels, { [state.serverAlias]: { byServicePath: { todos: { copiesById: { 1: { _id: 1, test: true, __isClone: true } } } } } }) const params = { query: {} } const results = find(state)(params) assert.deepEqual( results.data, _values(state.keyedById).filter(i => !i.__isClone), 'the list was correct' ) assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 3, 'total was correct') clearModels() }) it('find - with copies with keepCopiesInStore:true', function () { const state = { keepCopiesInStore: true, idField: '_id', keyedById: { 1: { _id: 1, test: true, __isClone: false }, 2: { _id: 2, test: true, __isClone: false }, 3: { _id: 3, test: true, __isClone: false } }, copiesById: { 1: { _id: 1, test: true, __isClone: true } } } const params = { query: {}, copies: true } const results = find(state)(params) const expected = [ { _id: 1, test: true, __isClone: true }, { _id: 2, test: true, __isClone: false }, { _id: 3, test: true, __isClone: false } ] assert.deepEqual(results.data, expected, 'the list was correct') assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 3, 'total was correct') }) it('find - with copies with keepCopiesInStore:false', function () { const state = { keepCopiesInStore: false, servicePath: 'todos', serverAlias: 'my-getters-test', idField: '_id', keyedById: { 1: { _id: 1, test: true, __isClone: false }, 2: { _id: 2, test: true, __isClone: false }, 3: { _id: 3, test: true, __isClone: false } } } Object.assign(globalModels, { [state.serverAlias]: { byServicePath: { todos: { copiesById: { 1: { _id: 1, test: true, __isClone: true } } } } } }) const params = { query: {}, copies: true } const results = find(state)(params) const expected = [ { _id: 1, test: true, __isClone: true }, { _id: 2, test: true, __isClone: false }, { _id: 3, test: true, __isClone: false } ] assert.deepEqual(results.data, expected, 'the list was correct') assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 3, 'total was correct') clearModels() }) it('find - with copies and temps', function () { const state = { keepCopiesInStore: false, servicePath: 'todos', serverAlias: 'my-getters-test', idField: '_id', keyedById: { 1: { _id: 1, test: true, __isClone: false }, 2: { _id: 2, test: true, __isClone: false }, 3: { _id: 3, test: true, __isClone: false } }, tempsById: { abc: { __id: 'abc', test: true, __isClone: false, __isTemp: true } } } Object.assign(globalModels, { [state.serverAlias]: { byServicePath: { todos: { copiesById: { 1: { _id: 1, test: true, __isClone: true } } } } } }) const params = { query: {}, copies: true, temps: true } const results = find(state)(params) const expected = [ { _id: 1, test: true, __isClone: true }, { _id: 2, test: true, __isClone: false }, { _id: 3, test: true, __isClone: false }, { __id: 'abc', test: true, __isClone: false, __isTemp: true } ] assert.deepEqual(results.data, expected, 'the list was correct') assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 4, 'total was correct') clearModels() }) it('find with query', function () { const { state } = this const params = { query: { test: false } } const results = find(state)(params) assert(results.data.length === 1, 'the length was correct') assert(results.data[0]._id === 3, 'the correct record was returned') assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 1, 'total was correct') }) it('find with custom operator', function () { const { state } = this const params = { query: { test: false, $populateParams: 'test' } } const results = find(state)(params) assert(results.data.length === 1, 'the length was correct') assert(results.data[0]._id === 3, 'the correct record was returned') assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 1, 'total was correct') }) it('find with paramsForServer option', function () { const { state } = this state.paramsForServer = ['_$client'] const params = { query: { test: false, _$client: 'test' } } const results = find(state)(params) assert(results.data.length === 1, 'the length was correct') assert(results.data[0]._id === 3, 'the correct record was returned') assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 1, 'total was correct') }) it('find with non-whitelisted custom operator fails', function () { const { state } = this const params = { query: { $client: 'test' } } try { find(state)(params) } catch (error) { assert(error) } }) it('find with whitelisted custom operators', function () { const { state } = this state.whitelist = ['$regex', '$options'] const query = { name: { $regex: 'marsh', $options: 'igm' } } const params = { query } let results try { results = find(state)(params) } catch (error) { assert(!error, 'should not have failed with whitelisted custom operator') } assert(results.data.length === 1, 'the length was correct') assert(results.data[0]._id === 2, 'the correct record was returned') assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 1, 'total was correct') }) it('find works with $elemMatch', function () { const { state } = this const query = { movies: { $elemMatch: { actors: 'Jerry the Mouse' } } } const params = { query } const results = find(state)(params) assert(results.data.length === 1, 'the length was correct') assert(results.data[0]._id === 2, 'the correct record was returned') assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 1, 'total was correct') }) it('find with limit', function () { const { state } = this const params = { query: { $limit: 1 } } const results = find(state)(params) assert(results.data.length === 1, 'the length was correct') assert(results.data[0]._id === 1, 'the correct record was returned') assert(results.limit === 1, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 3, 'total was correct') }) it('find with skip', function () { const { state } = this const params = { query: { $skip: 1 } } const results = find(state)(params) assert(results.data.length === 2, 'the length was correct') assert(results.data[0]._id === 2, 'the correct record was returned') assert(results.data[1]._id === 3, 'the correct record was returned') assert(results.limit === 0, 'limit was correct') assert(results.skip === 1, 'skip was correct') assert(results.total === 3, 'total was correct') }) it('find with limit and skip', function () { const { state } = this const params = { query: { $limit: 1, $skip: 1 } } const results = find(state)(params) assert(results.data.length === 1, 'the length was correct') assert(results.data[0]._id === 2, 'the correct record was returned') assert(results.limit === 1, 'limit was correct') assert(results.skip === 1, 'skip was correct') assert(results.total === 3, 'total was correct') }) it('find with select', function () { const { state } = this const params = { query: { $select: ['otherField'] } } const results = find(state)(params) assert(results.data.length === 3, 'the length was correct') results.data.forEach(result => { assert(Object.keys(result).length <= 1, 'only one field was returned') }) assert.equal( results.data.filter(i => i.otherField).length, 3, 'three records have the field.' ) assert(results.limit === 0, 'limit was correct') assert(results.skip === 0, 'skip was correct') assert(results.total === 3, 'total was correct') }) it('find with sort ascending on integers', function () { const { state } = this const params = { query: { $sort: { age: 1 } } } const results = find(state)(params) results.data .map(i => i.age) .reduce((oldest, current) => { assert(current > oldest, 'age should have been older than previous') return current }, 0) }) it('find with sort descending on integers', function () { const { state } = this const params = { query: { $sort: { age: -1 } } } const results = find(state)(params) results.data .map(i => i.age) .reduce((oldest, current) => { assert(current < oldest, 'age should have been younger than previous') return current }, 100) }) it('find with sort ascending on floats', function () { const { state } = this const params = { query: { $sort: { teethRemaining: 1 } } } const results = find(state)(params) results.data .map(i => i.teethRemaining) .reduce((oldest, current) => { assert( current > oldest, 'teethRemaining should have been older than previous' ) return current }, 0) }) it('find with sort descending on floats', function () { const { state } = this const params = { query: { $sort: { teethRemaining: -1 } } } const results = find(state)(params) results.data .map(i => i.teethRemaining) .reduce((oldest, current) => { assert( current < oldest, 'teethRemaining should have been younger than previous' ) return current }, 100) }) it('count without params fails', function () { const { state } = this try { count(state, { find })(null) } catch (error) { assert(error) } }) it('count without query fails', function () { const { state } = this try { count(state, { find: find(state) })({}) } catch (error) { assert(error) } }) it('count returns the number of records in the store', function () { const { state } = this const total = count(state, { find: find(state) })({ query: {} }) assert(total === 3, 'count is 3') }) it('is*PendingById', function() { const { state } = this // Set up getters const getters: any = { isCreatePendingById: isCreatePendingById(state), isUpdatePendingById: isUpdatePendingById(state), isPatchPendingById: isPatchPendingById(state), isRemovePendingById: isRemovePendingById(state), isSavePendingById, isPendingById } getters.isSavePendingById = isSavePendingById(state, getters) getters.isPendingById = isPendingById(state, getters) assert(isCreatePendingById(state)(42) === false, 'creating status is clear') assert(isUpdatePendingById(state)(42) === false, 'updating status is clear') assert(isPatchPendingById(state)(42) === false, 'patching status is clear') assert(isRemovePendingById(state)(42) === false, 'removing status is clear') assert(isSavePendingById(state, getters)(42) === false, 'saving status is clear') assert(isPendingById(state, getters)(42) === false, 'any method pending status is clear') // Create setIdPending(state, { method: 'create', id: 42}) assert(isCreatePendingById(state)(42) === true, 'creating status is set') assert(isSavePendingById(state, getters)(42) === true, 'saving status is set') assert(isPendingById(state, getters)(42) === true, 'any method pending status is set') unsetIdPending(state, { method: 'create', id: 42 }) assert(isCreatePendingById(state)(42) === false, 'creating status is clear') assert(isUpdatePendingById(state)(42) === false, 'updating status is clear') assert(isPatchPendingById(state)(42) === false, 'patching status is clear') assert(isRemovePendingById(state)(42) === false, 'removing status is clear') assert(isSavePendingById(state, getters)(42) === false, 'saving status is clear') assert(isPendingById(state, getters)(42) === false, 'any method pending status is clear') // Update setIdPending(state, { method: 'update', id: 42}) assert(isUpdatePendingById(state)(42) === true, 'updating status is set') assert(isSavePendingById(state, getters)(42) === true, 'saving status is set') assert(isPendingById(state, getters)(42) === true, 'any method pending status is set') unsetIdPending(state, { method: 'update', id: 42 }) assert(isCreatePendingById(state)(42) === false, 'creating status is clear') assert(isUpdatePendingById(state)(42) === false, 'updating status is clear') assert(isPatchPendingById(state)(42) === false, 'patching status is clear') assert(isRemovePendingById(state)(42) === false, 'removing status is clear') assert(isSavePendingById(state, getters)(42) === false, 'saving status is clear') assert(isPendingById(state, getters)(42) === false, 'any method pending status is clear') // Patch setIdPending(state, { method: 'patch', id: 42}) assert(isPatchPendingById(state)(42) === true, 'patching status is set') assert(isSavePendingById(state, getters)(42) === true, 'saving status is set') assert(isPendingById(state, getters)(42) === true, 'any method pending status is set') unsetIdPending(state, { method: 'patch', id: 42 }) assert(isCreatePendingById(state)(42) === false, 'creating status is clear') assert(isUpdatePendingById(state)(42) === false, 'updating status is clear') assert(isPatchPendingById(state)(42) === false, 'patching status is clear') assert(isRemovePendingById(state)(42) === false, 'removing status is clear') assert(isSavePendingById(state, getters)(42) === false, 'saving status is clear') assert(isPendingById(state, getters)(42) === false, 'any method pending status is clear') // Remove setIdPending(state, { method: 'remove', id: 42}) assert(isRemovePendingById(state)(42) === true, 'removing status is set') assert(isSavePendingById(state, getters)(42) === false, 'saving status is clear for remove') assert(isPendingById(state, getters)(42) === true, 'any method pending status is set') unsetIdPending(state, { method: 'remove', id: 42 }) assert(isCreatePendingById(state)(42) === false, 'creating status is clear') assert(isUpdatePendingById(state)(42) === false, 'updating status is clear') assert(isPatchPendingById(state)(42) === false, 'patching status is clear') assert(isRemovePendingById(state)(42) === false, 'removing status is clear') assert(isSavePendingById(state, getters)(42) === false, 'saving status is clear') assert(isPendingById(state, getters)(42) === false, 'any method pending status is clear') }) }) ================================================ FILE: test/service-module/service-module.mutations.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' import { assertGetter, makeStore } from '../test-utils' import makeServiceMutations, { PendingServiceMethodName, PendingIdServiceMethodName } from '../../src/service-module/service-module.mutations' import makeServiceState from '../../src/service-module/service-module.state' import errors from '@feathersjs/errors' import Vue from 'vue' import Vuex from 'vuex' import fakeData from '../fixtures/fake-data' import { Service as MemoryService } from 'feathers-memory' import { getQueryInfo } from '../../src/utils' import { diff as deepDiff } from 'deep-object-diff' import omitDeep from 'omit-deep-lodash' import feathersVuex from '../../src/index' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import { globalModels, clearModels } from '../../src/service-module/global-models' const { BaseModel } = feathersVuex(feathersClient, { serverAlias: 'mutations' }) Vue.use(Vuex) class Todo extends BaseModel { public static modelName = 'Todo' public static test = true } const options = { idField: '_id', tempIdField: '__id', autoRemove: false, serverAlias: 'myApi', service: feathersClient.service('mutations-todo'), Model: Todo } const { addItem, addItems, updateItem, updateItems, removeItem, removeItems, clearAll, createCopy, resetCopy, commitCopy, clearCopy, updatePaginationForQuery, setPending, unsetPending, setError, clearError, setIdPending, unsetIdPending } = makeServiceMutations() class ComicService extends MemoryService { public create(data, params) { return super.create(data, params).then(response => { delete response.__id delete response.__isTemp return response }) } // @ts-ignore public update(id, data, params) { data.createdAt = new Date() // this._super(data, params, callback) } } function makeContext() { feathersClient.use( 'comics', // @ts-ignore new ComicService({ store: makeStore() }) ) const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'service-module-mutations' }) class Comic extends BaseModel { public static modelName = 'Comic' public static test = true public constructor(data, options?) { super(data, options) } } const store = new Vuex.Store({ strict: true, plugins: [ makeServicePlugin({ Model: Comic, service: feathersClient.service('comics'), servicePath: 'comics', idField: '_id', tempIdField: '__id' }) ] }) return { makeServicePlugin, BaseModel, Comic, store } } describe('Service Module - Mutations', function () { beforeEach(function () { this.state = makeServiceState(options) this.state.keepCopiesInStore = true }) describe('Create, Update, Remove', function () { it('addItem', function () { const state = this.state const item1 = { _id: 1, test: true } const item2 = { _id: 2, test: true } addItem(state, item1) assert(state.ids.length === 1) assert(state.ids[0] === 1) assert(state.keyedById[1].test) // Add item 2 addItem(state, item2) assert(state.ids.length === 2) assert(state.ids[1] === 2) assert(state.keyedById[2].test) // Re-add item 1 addItem(state, item1) assert(state.ids.length === 2, 'still only two items in the ids array') assert(state.ids[0] === 1) assert(state.keyedById[1].test) assert(state.ids[1] === 2) assert(state.keyedById[2].test) }) it('addItems', function () { const state = this.state const item1 = { _id: 1, test: true } const item2 = { _id: 2, test: true } const items = [item1, item2] addItems(state, items) assert(state.ids.length === 2, 'still only two items in the ids array') assert(state.ids[0] === 1) assert(state.keyedById[1].test) assert(state.ids[1] === 2) assert(state.keyedById[2].test) }) it('updateItems', function () { const state = this.state const item1 = { _id: 1, test: true } const item2 = { _id: 2, test: true } const items = [item1, item2] addItems(state, items) const item1updated = { _id: 1, test: false } const item2updated = { _id: 2, test: false } const itemsToUpdate = [item1updated, item2updated] updateItems(state, itemsToUpdate) assert(state.keyedById[1].test === false) assert(state.keyedById[2].test === false) }) it('removeItem', function () { const state = this.state addItem(state, { _id: 1, test: true }) removeItem(state, 1) assert(state.ids.length === 0) assert(Object.keys(state.keyedById).length === 0) }) it('removeItem also removes clone', function () { const state = this.state const _id = 1 addItem(state, { _id, test: true }) createCopy(state, _id) assert(state.copiesById[_id], 'clone exists') removeItem(state, _id) assert(!state.copiesById[_id], 'clone is removed') }) it('removeItem also removes clone with keepCopiesInStore', function () { const context = makeContext() const { Comic, store } = context const _id = 1 store.commit('comics/addItem', { _id, test: true }) store.commit('comics/createCopy', _id) assert(Comic.copiesById[_id], 'clone exists') store.commit('comics/removeItem', _id) assert(!Comic.copiesById[_id], 'clone is removed') }) it('removeItems with array of ids', function () { const state = this.state const items = [ { _id: 1, test: true }, { _id: 2, test: true }, { _id: 3, test: true }, { _id: 4, test: true } ] addItems(state, items) const itemsToRemove = [1, 2] removeItems(state, itemsToRemove) assert(state.ids.length === 2, 'should have 2 ids left') assert( Object.keys(state.keyedById).length === 2, 'should have 2 items left' ) }) it('removeItems with array of items', function () { const state = this.state const items = [ { _id: 1, test: true }, { _id: 2, test: true }, { _id: 3, test: true }, { _id: 4, test: true } ] addItems(state, items) const itemsToRemove = [ { _id: 1, test: true }, { _id: 2, test: true } ] removeItems(state, itemsToRemove) assert(state.ids.length === 2, 'should have 2 ids left') assert( Object.keys(state.keyedById).length === 2, 'should have 2 items left' ) }) it('removeItems also removes clone', function () { const state = this.state addItems(state, [ { _id: 1, test: true }, { _id: 2, test: true }, { _id: 3, test: true }, { _id: 4, test: true } ]) const itemsToRemove = [1, 2] createCopy(state, 1) createCopy(state, 3) assert(state.copiesById[1], 'clone exists') removeItems(state, itemsToRemove) assert(!state.copiesById[1], 'clone is removed') assert(state.copiesById[3], 'other clone is not affected') }) it('removeItems also removes clone with keepCopiesInStore', function () { const context = makeContext() const { Comic, store } = context store.commit('comics/addItems', [ { _id: 1, test: true }, { _id: 2, test: true }, { _id: 3, test: true }, { _id: 4, test: true } ]) const itemsToRemove = [1, 2] store.commit('comics/createCopy', 1) store.commit('comics/createCopy', 3) assert(Comic.copiesById[1], 'clone exists') store.commit('comics/removeItems', itemsToRemove) assert(!Comic.copiesById[1], 'clone is removed') assert(Comic.copiesById[3], 'other clone is not affected') }) it('clearAll', function () { const state = this.state assert(state.ids.length === 0, 'initialy empty') assert(Object.keys(state.keyedById).length === 0, 'initialy empty') assert(Object.keys(state.copiesById).length === 0, 'initialy empty') const item1 = { _id: 1, test: true } const item2 = { _id: 2, test: true } const items = [item1, item2] addItems(state, items) createCopy(state, item1._id) assert(state.ids.length === 2, 'ids are added correctly') assert( Object.keys(state.keyedById).length === 2, 'items are added correctly' ) assert( Object.keys(state.copiesById).length === 1, 'clone is added correctly' ) clearAll(state) assert(state.ids.length === 0, 'ids empty again') assert(Object.keys(state.keyedById).length === 0, 'items empty again') assert(Object.keys(state.copiesById).length === 0, 'clones empty again') }) it('clearAll with keepCopiesInStore: false', function () { const context = makeContext() const { Comic, store } = context // @ts-ignore const state = store.state.comics assert(state.ids.length === 0, 'initialy empty') assert(Object.keys(state.keyedById).length === 0, 'initialy empty') assert(Object.keys(Comic.copiesById).length === 0, 'initialy empty') const item1 = { _id: 1, test: true } const item2 = { _id: 2, test: true } const items = [item1, item2] store.commit('comics/addItems', items) store.commit('comics/createCopy', item1._id) assert(state.ids.length === 2, 'ids are added correctly') assert( Object.keys(state.keyedById).length === 2, 'items are added correctly' ) assert( Object.keys(Comic.copiesById).length === 1, 'clone is added correctly' ) store.commit('comics/clearAll') assert(state.ids.length === 0, 'ids empty again') assert(Object.keys(state.keyedById).length === 0, 'items empty again') assert(Object.keys(Comic.copiesById).length === 0, 'clones empty again') }) }) describe('updateItem', function () { it('updates existing item when addOnUpsert=true', function () { const state = this.state state.addOnUpsert = true const item1 = { _id: 1, test: true } const items = [item1] addItems(state, items) const item1updated = { _id: 1, test: false } updateItem(state, item1updated) assert(state.keyedById[1].test === false) }) it('updates existing item when addOnUpsert=false', function () { const state = this.state state.addOnUpsert = false const item1 = { _id: 1, test: true } const items = [item1] addItems(state, items) const item1updated = { _id: 1, test: false } updateItem(state, item1updated) assert(state.keyedById[1].test === false) }) it('adds non-existing item when addOnUpsert=true', function () { const state = this.state state.addOnUpsert = true const item1updated = { _id: 1, test: false } updateItem(state, item1updated) assert.deepEqual( [state.addOnUpsert, state.ids, state.keyedById], [true, [1], { 1: { _id: 1, test: false } }] ) // assert(state.keyedById[1].test === false) }) it('discards non-existing item when addOnUpsert=false', function () { const state = this.state state.addOnUpsert = false const item1updated = { _id: 1, test: false } updateItem(state, item1updated) assert(state.keyedById[1] == null) }) }) describe('Vue event bindings', function () { it('does not break when attempting to overwrite a getter', function (done) { const state = this.state const item1 = { _id: 1, get getter() { return 'Release the flying monkeys!' } } assertGetter(item1, 'getter', 'Release the flying monkeys!') const items = [item1] addItems(state, items) // Prove the getter is still in place in the store assertGetter(state.keyedById[1], 'getter', 'Release the flying monkeys!') const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.getter'() { // eslint-disable-next-line no-console console.log(state.keyedById) throw new Error('this should never happen') } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, getter: true } updateItem(state, updatedItem) assert(state.keyedById[1].getter === 'Release the flying monkeys!') done() }) it('correctly emits events for existing array properties', function (done) { const state = this.state const item1 = { _id: 1, test: true, users: ['Marshall', 'Mariah'] } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.users'() { assert(this.item.users.length === 3) done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, test: false, users: ['Marshall', 'Mariah', 'Scooby Doo'] } updateItem(state, updatedItem) }) it('correctly emits events for new array properties', function (done) { const state = this.state const item1 = { _id: 1, test: true } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.users'() { assert(this.item.users.length === 3) done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, test: false, users: ['Marshall', 'Mariah', 'Scooby Doo'] } updateItem(state, updatedItem) }) it('correctly emits events for existing object properties', function (done) { const state = this.state const item1 = { _id: 1, obj: { test: true } } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.obj'() { assert(this.item.obj.test === false) done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, obj: { test: false } } updateItem(state, updatedItem) }) it('correctly emits events for new object properties', function (done) { const state = this.state const item1 = { _id: 1 } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.obj'() { assert(this.item.obj.test === false) done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, obj: { test: false } } updateItem(state, updatedItem) }) it('correctly emits events for existing boolean properties', function (done) { const state = this.state const item1 = { _id: 1, isValid: true } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.isValid'() { assert(this.item.isValid === false) done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, isValid: false } updateItem(state, updatedItem) }) it('correctly emits events for new boolean properties', function (done) { const state = this.state const item1 = { _id: 1 } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.isValid'() { assert(this.item.isValid === false) done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, isValid: false } updateItem(state, updatedItem) }) it('correctly emits events for existing string properties', function (done) { const state = this.state const item1 = { _id: 1, name: 'Marshall' } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.name'() { assert(this.item.name === 'Xavier') done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, name: 'Xavier' } updateItem(state, updatedItem) }) it('correctly emits events for new string properties', function (done) { const state = this.state const item1 = { _id: 1 } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.name'() { assert(this.item.name === 'Xavier') done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, name: 'Xavier' } updateItem(state, updatedItem) }) it('correctly emits events for existing null properties', function (done) { const state = this.state const item1 = { _id: 1, name: null } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.name'() { assert(this.item.name === 'Xavier') done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, name: 'Xavier' } updateItem(state, updatedItem) }) it('correctly emits events for properties set to null', function (done) { const state = this.state const item1 = { _id: 1, name: 'Marshall' } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.name'() { assert(this.item.name === null) done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, name: null } updateItem(state, updatedItem) }) it('correctly emits events for existing number properties', function (done) { const state = this.state const item1 = { _id: 1, age: 45 } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.age'() { assert(this.item.age === 50) done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, age: 50 } updateItem(state, updatedItem) }) it('correctly emits events for new number properties', function (done) { const state = this.state const item1 = { _id: 1 } const items = [item1] addItems(state, items) const vm = new Vue({ data: { item: state.keyedById[1] }, watch: { 'item.age'() { assert(this.item.age === 50) done() } } }) assert(vm.item, 'vm has item') const updatedItem = { _id: 1, age: 50 } updateItem(state, updatedItem) }) it('correctly emits events after commitCopy', function (done) { const state = this.state const item1 = { _id: 1, obj: { test: true }, get getter() { return this.obj.test }, set setter(val) { this.obj.test = val } } const items = [item1] addItems(state, items) const item = state.keyedById[item1._id] createCopy(state, item._id) const copy = state.copiesById[item1._id] const vm = new Vue({ data: { item, copy }, watch: { 'item.obj': { handler() { assert(this.item.obj.test === false) done() }, deep: true } } }) assert(vm.item, 'vm has item') assert(vm.copy, 'vm has copy') // Modify copy and commit vm.copy.setter = false commitCopy(state, item1._id) assert(item.obj.test === false, 'deep obj should be false') assert(vm.item.obj.test === false, 'deep obj should be false') }) it('correctly emits events after resetCopy', function (done) { const state = this.state const item1 = { _id: 1, obj: { test: true }, get getter() { return this.obj.test }, set setter(val) { this.obj.test = val } } const items = [item1] addItems(state, items) const item = state.keyedById[item1._id] // createCopy and modify, but don't commit createCopy(state, item._id) const copy = state.copiesById[item1._id] copy.setter = false const vm = new Vue({ data: { item, copy }, watch: { 'copy.obj': { handler() { assert(this.copy.obj.test === true) done() }, deep: true } } }) assert(vm.item, 'vm has item') assert(vm.copy, 'vm has copy') resetCopy(state, item1._id) assert(item.obj.test === true, 'deep obj should be true') assert(vm.item.obj.test === true, 'deep obj should be true') }) }) describe('Copy & Commit', function () { it('createCopy', function () { const { state } = this const item1 = { _id: 1, test: true, get getter() { return 'Life is a Joy!' }, set setter(val) { this.test = val } } addItem(state, item1) const original = state.keyedById[1] createCopy(state, item1._id) const copy = state.copiesById[item1._id] assert.deepEqual( original, copy, `original and copy have the same properties` ) copy.setter = false assert(copy.getter === 'Life is a Joy!', `getter was preserved`) assert(copy.test === false, `copy was changed through setter`) assert(original.test === true, `original item intact after copy changed`) }) it('createCopy with keepCopiesInStore: false', function () { const context = makeContext() const { Comic, store } = context const item1 = { _id: 1, test: true } store.commit('comics/addItem', item1) // @ts-ignore const original = store.state.comics.keyedById[1] store.commit('comics/createCopy', item1._id) const copy = Comic.copiesById[item1._id] assert.deepEqual( original, copy, `original and copy have the same properties` ) copy.test = false assert(copy.test === false, `copy was changed through setter`) assert(original.test === true, `original item intact after copy changed`) clearModels() }) it('createCopy of temp', function () { const { state } = this const item1 = { __id: 'abc', test: true, get getter() { return 'Life is a Joy!' }, set setter(val) { this.test = val } } addItem(state, item1) const original = state.tempsById[item1[state.tempIdField]] createCopy(state, original[state.tempIdField]) const copy = state.copiesById[original[state.tempIdField]] copy.setter = false assert(copy.getter === 'Life is a Joy!', `getter was preserved`) assert(copy.test === false, `copy was changed through setter`) assert(original.test === true, `original item intact after copy changed`) }) it('createCopy of temp with keepCopiesInStore: false', function () { const context = makeContext() const { Comic, store } = context const item1 = { __id: 'abc', test: true } store.commit('comics/addItem', item1) // @ts-ignore const original = store.state.comics.tempsById[item1.__id] store.commit('comics/createCopy', item1.__id) const copy = Comic.copiesById[item1.__id] copy.test = false assert(copy.test === false, `copy was changed through setter`) assert(original.test === true, `original item intact after copy changed`) clearModels() }) it('createCopy while existing copy', function () { const { state } = this const item1 = { _id: 1, test: true } addItem(state, item1) const original = state.keyedById[1] createCopy(state, item1._id) const copy = state.copiesById[item1._id] copy.test = false createCopy(state, item1._id) const copy2 = state.copiesById[item1._id] assert(copy === copy2, `only one clone exists`) assert( copy.test === true && copy2.test === true, `new clone overwrites old clone` ) }) it('createCopy while existing copy with keepCopiesInStore: false', function () { const context = makeContext() const { Comic, store } = context const item1 = { _id: 1, test: true } store.commit('comics/addItem', item1) // @ts-ignore const original = store.state.comics.keyedById[1] store.commit('comics/createCopy', item1._id) const copy = Comic.copiesById[item1._id] copy.test = false store.commit('comics/createCopy', original._id) const copy2 = Comic.copiesById[item1._id] assert(copy === copy2, `only one clone exists`) assert( copy.test === true && copy2.test === true, `new clone overwrites old clone` ) clearModels() }) it('resetCopy', function () { const { state } = this const item1 = { _id: 1, test: true, get getter() { return 'Life is a Joy!' }, set setter(val) { this.test = val } } addItem(state, item1) // Create a copy and modify it. createCopy(state, item1._id) const copy = state.copiesById[item1._id] copy.test = false // Call resetCopy and check that it's back to the original value resetCopy(state, item1._id) assert(copy.test === true, 'the copy was reset') // Make sure accessors stayed intact assertGetter(copy, 'getter', 'Life is a Joy!') copy.setter = false assert(copy.test === false, 'the setter is intact') }) it('resetCopy with keepCopiesInStore: false', function () { const context = makeContext() const { Comic, store } = context const item1 = { _id: 1, test: true } store.commit('comics/addItem', item1) // Create a copy and modify it. store.commit('comics/createCopy', item1._id) const copy = Comic.copiesById[item1._id] copy.test = false // Call resetCopy and check that it's back to the original value store.commit('comics/resetCopy', item1._id) assert(copy.test === true, 'the copy was reset') clearModels() }) it.skip('resetCopy with keepCopiesInStore: false and with intact getter/setter', function () { const context = makeContext() const { Comic, store } = context const item1 = { _id: 1, test: true, get getter() { return 'Life is a Joy!' }, set setter(val) { this.test = val } } store.commit('comics/addItem', item1) // Create a copy and modify it. store.commit('comics/createCopy', item1._id) const copy = Comic.copiesById[item1._id] copy.test = false // Call resetCopy and check that it's back to the original value store.commit('comics/resetCopy', item1._id) assert(copy.test === true, 'the copy was reset') // Make sure accessors stayed intact assertGetter(copy, 'getter', 'Life is a Joy!') copy.setter = false assert(copy.test === false, 'the setter is intact') clearModels() }) it('commitCopy', function () { const state = this.state const item1 = { _id: 1, test: true, get getter() { return 'Life is a Joy!' }, set setter(val) { this.test = val } } addItem(state, item1) const original = state.keyedById[item1._id] // Create a copy and modify it. createCopy(state, item1._id) const copy = state.copiesById[item1._id] copy.test = false commitCopy(state, item1._id) assert(copy.test === false, `the copy wasn't changed after commitCopy`) assert(original.test === false, 'original item updated after commitCopy') }) it('commitCopy with keepCopiesInStore: false', function () { const context = makeContext() const { Comic, store } = context const item1 = { _id: 1, test: true } store.commit('comics/addItem', item1) // @ts-ignore const original = store.state.comics.keyedById[item1._id] // Create a copy and modify it. store.commit('comics/createCopy', item1._id) const copy = Comic.copiesById[item1._id] copy.test = false store.commit('comics/commitCopy', item1._id) assert(copy.test === false, `the copy wasn't changed after commitCopy`) assert(original.test === false, 'original item updated after commitCopy') clearModels() }) it('clearCopy', function () { const state = this.state const item1 = { _id: 1, test: true } addItem(state, item1) // Create a copy then clear it. createCopy(state, item1._id) assert(state.copiesById[item1._id], `the copy is there!`) clearCopy(state, item1._id) assert(!state.copiesById[item1._id], `the copy is gone!`) }) it('clearCopy with keepCopiesInStore: false', function () { const context = makeContext() const { Comic, store } = context const item1 = { _id: 1, test: true } store.commit('comics/addItem', item1) // Create a copy then clear it. store.commit('comics/createCopy', item1._id) assert(Comic.copiesById[item1._id], `the copy is there!`) store.commit('comics/clearCopy', item1._id) assert(!Comic.copiesById[item1._id], `the copy is gone!`) clearModels() }) }) describe('Pagination', function () { it('updatePaginationForQuery', function () { this.timeout(600000) const state = this.state const qid = 'main-list' const decisionTable = [ { description: 'initial query empty', query: {}, response: { data: fakeData.transactions.slice(0, 10), limit: 10, skip: 0, total: fakeData.transactions.length }, makeResult(props) { const { query, queryId, queryParams, pageId, pageParams, queriedAt } = props return { defaultLimit: 10, defaultSkip: 0, 'main-list': { mostRecent: { query, queryId, queryParams, pageId, pageParams, queriedAt, total: 155 }, '{}': { total: fakeData.transactions.length, queryParams: {}, ["{\"$limit\":10,\"$skip\":0}"]: { //eslint-disable-line pageParams, ids: fakeData.transactions .slice(0, 10) .map(i => i[state.idField]), queriedAt } } } } } }, { description: 'initial query, limit 10, skip 0', query: { $limit: 10 }, response: { data: fakeData.transactions.slice(0, 10), limit: 10, skip: 0, total: fakeData.transactions.length }, makeResult(props) { const { query, queryId, queryParams, pageId, pageParams, queriedAt } = props return { defaultLimit: 10, defaultSkip: 0, 'main-list': { mostRecent: { query, queryId, queryParams, pageId, pageParams, queriedAt, total: 155 }, '{}': { total: fakeData.transactions.length, queryParams: {}, ["{\"$limit\":10,\"$skip\":0}"]: { //eslint-disable-line pageParams, ids: fakeData.transactions .slice(0, 10) .map(i => i[state.idField]), queriedAt } } } } } }, { description: 'initial query, limit 10, skip 10', query: { $limit: 10, $skip: 10 }, response: { data: fakeData.transactions.slice(10, 20), limit: 10, skip: 10, total: fakeData.transactions.length }, makeResult(props) { const { query, queryId, queryParams, pageId, pageParams, queriedAt } = props return { defaultLimit: 10, defaultSkip: 0, 'main-list': { mostRecent: { query, queryId, queryParams, pageId, pageParams, queriedAt, total: 155 }, '{}': { total: fakeData.transactions.length, queryParams: {}, ["{\"$limit\":10,\"$skip\":0}"]: { //eslint-disable-line pageParams: { $limit: 10, $skip: 0 }, ids: fakeData.transactions .slice(0, 10) .map(i => i[state.idField]), queriedAt }, ["{\"$limit\":10,\"$skip\":10}"]: { //eslint-disable-line pageParams: { $limit: 10, $skip: 10 }, ids: fakeData.transactions .slice(10, 20) .map(i => i[state.idField]), queriedAt } } } } } }, { description: 'separate query, limit 10, skip 10', query: { test: true, $limit: 10, $skip: 10 }, response: { data: fakeData.transactions.slice(10, 20), limit: 10, skip: 10, total: fakeData.transactions.length }, makeResult(props) { const { query, queryId, queryParams, pageId, pageParams, queriedAt } = props return { defaultLimit: 10, defaultSkip: 0, 'main-list': { mostRecent: { query, queryId, queryParams, pageId, pageParams, queriedAt, total: 155 }, '{}': { total: fakeData.transactions.length, queryParams: {}, ["{\"$limit\":10,\"$skip\":0}"]: { //eslint-disable-line pageParams: { $limit: 10, $skip: 0 }, ids: fakeData.transactions .slice(0, 10) .map(i => i[state.idField]), queriedAt }, ["{\"$limit\":10,\"$skip\":10}"]: { //eslint-disable-line pageParams: { $limit: 10, $skip: 10 }, ids: fakeData.transactions .slice(10, 20) .map(i => i[state.idField]), queriedAt } }, '{"test":true}': { total: fakeData.transactions.length, queryParams: { test: true }, ["{\"$limit\":10,\"$skip\":10}"]: { //eslint-disable-line pageParams: { $limit: 10, $skip: 10 }, ids: fakeData.transactions .slice(10, 20) .map(i => i[state.idField]), queriedAt } } } } } } ] decisionTable.forEach(({ description, query, response, makeResult }) => { const { queryId, queryParams, pageId, pageParams } = getQueryInfo( { qid, query }, response ) const queriedAt = new Date().getTime() const expectedResult = makeResult({ query, queryId, queryParams, pageId, pageParams, queriedAt }) updatePaginationForQuery(state, { qid, response, query }) const diff = deepDiff( omitDeep(state.pagination, 'queriedAt'), omitDeep(expectedResult, 'queriedAt') ) assert.deepEqual( omitDeep(state.pagination, 'queriedAt'), omitDeep(expectedResult, 'queriedAt'), description ) }) }) }) describe('Pending', function () { it('setPending && unsetPending', function () { const state = this.state const methods: PendingServiceMethodName[] = [ 'find', 'get', 'create', 'update', 'patch', 'remove' ] methods.forEach(method => { const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) assert(!state[`is${uppercaseMethod}Pending`]) // Set pending & check setPending(state, method) assert(state[`is${uppercaseMethod}Pending`]) // Unset pending & check unsetPending(state, method) assert(!state[`is${uppercaseMethod}Pending`]) }) }) }) describe('Per-instance Pending', function() { it('setIdPending && unsetIdPending', function() { const state = this.state const methods: PendingIdServiceMethodName[] = [ 'create', 'update', 'patch', 'remove' ] methods.forEach(method => { const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) assert(state[`isId${uppercaseMethod}Pending`].length === 0) // Set pending & check setIdPending(state, { method, id: 42 }) assert(state[`isId${uppercaseMethod}Pending`].includes(42)) // Unset pending & check unsetIdPending(state, { method, id: 42 }) assert(state[`isId${uppercaseMethod}Pending`].length === 0) }) }) }) describe('Errors', function () { it('setError', function () { const state = this.state const methods: PendingServiceMethodName[] = [ 'find', 'get', 'create', 'update', 'patch', 'remove' ] methods.forEach(method => { const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) setError(state, { method, error: new Error('This is a test') }) assert(state[`errorOn${uppercaseMethod}`].message) assert(state[`errorOn${uppercaseMethod}`].name) assert(state[`errorOn${uppercaseMethod}`].stack) }) }) it('setError with feathers-errors', function () { const state = this.state const methods: PendingServiceMethodName[] = [ 'find', 'get', 'create', 'update', 'patch', 'remove' ] methods.forEach(method => { const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) setError(state, { method, error: new errors.NotAuthenticated('You are not logged in') }) assert(state[`errorOn${uppercaseMethod}`].className) assert(state[`errorOn${uppercaseMethod}`].code) assert(state[`errorOn${uppercaseMethod}`].hasOwnProperty('errors')) assert(state[`errorOn${uppercaseMethod}`].hasOwnProperty('data')) assert(state[`errorOn${uppercaseMethod}`].message) assert(state[`errorOn${uppercaseMethod}`].name) assert(state[`errorOn${uppercaseMethod}`].stack) }) }) it('clearError', function () { const state = this.state const methods: PendingServiceMethodName[] = [ 'find', 'get', 'create', 'update', 'patch', 'remove' ] methods.forEach(method => { const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1) setError(state, { method, error: new Error('This is a test') }) clearError(state, method) assert( state[`errorOn${uppercaseMethod}`] === null, `errorOn${uppercaseMethod} was cleared` ) }) }) }) }) ================================================ FILE: test/service-module/service-module.reinitialization.test.ts ================================================ import { assert } from 'chai' import Vuex from 'vuex' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import feathersVuex from '../../src/index' interface RootState { todos: any } function makeContext() { const todoService = feathersClient.service('todos') const serverAlias = 'reinitialization' const { makeServicePlugin, BaseModel, models } = feathersVuex( feathersClient, { serverAlias } ) class Todo extends BaseModel { public static modelName = 'Todo' } return { makeServicePlugin, BaseModel, todoService, Todo, models, serverAlias } } describe('Service Module - Reinitialization', function () { /** * Tests that when the make service plugin is reinitialized state * is reset in the vuex module/model. * This prevents state pollution in SSR setups. */ it('does not preserve module/model state when reinitialized', function () { const { makeServicePlugin, todoService, Todo, models, serverAlias } = makeContext() const todosPlugin = makeServicePlugin({ servicePath: 'todos', Model: Todo, service: todoService }) let store = new Vuex.Store({ plugins: [todosPlugin] }) let todoState = store.state['todos'] const virginState = { addOnUpsert: false, autoRemove: false, debug: false, copiesById: {}, enableEvents: true, errorOnCreate: null, errorOnFind: null, errorOnGet: null, errorOnPatch: null, errorOnRemove: null, errorOnUpdate: null, idField: 'id', tempIdField: '__id', ids: [], isCreatePending: false, isFindPending: false, isGetPending: false, isPatchPending: false, isRemovePending: false, isUpdatePending: false, keepCopiesInStore: false, debounceEventsTime: null, debounceEventsMaxWait: 1000, keyedById: {}, modelName: 'Todo', nameStyle: 'short', namespace: 'todos', pagination: { defaultLimit: null, defaultSkip: null }, paramsForServer: ['$populateParams'], preferUpdate: false, replaceItems: false, serverAlias, servicePath: 'todos', skipRequestIfExists: false, tempsById: {}, whitelist: [], isIdCreatePending: [], isIdUpdatePending: [], isIdPatchPending: [], isIdRemovePending: [], } assert.deepEqual( todoState, virginState, 'vuex module state is correct on first initialization' ) assert.deepEqual( models[serverAlias][Todo.name].store.state[Todo.namespace], todoState, 'model state is the same as vuex module state on first initialization' ) // Simulate some mutations on the store. const todo = { id: 1, testProp: true } store.commit('todos/addItem', todo) const serviceTodo = store.state['todos'].keyedById[1] assert.equal( todo.testProp, serviceTodo.testProp, 'todo is added to the store' ) assert.deepEqual( models[serverAlias][Todo.name].store.state[Todo.namespace], todoState, 'model state is the same as vuex module state when store is mutated' ) // Here we are going to simulate the make service plugin being reinitialized. // This is the default behaviour in SSR setups, e.g. nuxt universal mode, // although unlikely in SPAs. store = new Vuex.Store({ plugins: [todosPlugin] }) todoState = store.state['todos'] // We expect vuex module state for this service to be reset. assert.deepEqual( todoState, virginState, 'store state in vuex module is not preserved on reinitialization' ) // We also expect model store state for this service to be reset. assert.deepEqual( models[serverAlias][Todo.name].store.state[Todo.namespace], virginState, 'store state in service model is not preserved on reinitialization' ) }) }) ================================================ FILE: test/service-module/service-module.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { ServiceState } from './types' import { assert } from 'chai' import feathersVuex, { models } from '../../src/index' import { clearModels } from '../../src/service-module/global-models' import { makeFeathersRestClient, feathersRestClient as feathersClient, feathersSocketioClient } from '../fixtures/feathers-client' import { stripSlashes } from '../../src/utils' import memory from 'feathers-memory' import { makeTodos } from '../fixtures/todos' import Vuex from 'vuex' import { performance } from 'perf_hooks' import enableServiceEvents from '../../src/service-module/service-module.events' import { Service } from '@feathersjs/feathers' interface Options { idField: string } interface TodoState extends ServiceState { test: any test2: { test: boolean } isTrue: boolean } interface RootState { todos: TodoState tasks: ServiceState tests: ServiceState blah: ServiceState things: ServiceState } function makeContext() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'service-module' }) class ServiceTodo extends BaseModel { public static modelName = 'ServiceTodo' public id public description: string public constructor(data, options?) { super(data, options) } // eslint-disable-next-line @typescript-eslint/no-unused-vars public static instanceDefaults(data) { return { description: '' } } } class HotspotMedia extends BaseModel { public static modelName = 'HotspotMedia' public id public description: string } class Media extends BaseModel { public static modelName = 'Media' public id public description: string } class Person extends BaseModel { public static modelName = 'Person' public static test = true } class Item extends BaseModel { public static modelName = 'Item' public static test = true } class Task extends BaseModel { public static modelName = 'Task' public static test = true } class Car extends BaseModel { public static modelName = 'Car' public static test = true } class Group extends BaseModel { public static modelName = 'Group' public static test = true } class Test extends BaseModel { public static modelName = 'Test' public static test = true } class Thing extends BaseModel { public static modelName = 'Thing' public static test = true } return { makeServicePlugin, BaseModel, ServiceTodo, HotspotMedia, Media, Person, Item, Task, Car, Group, Test, Thing } } function makeContextWithState() { const feathers = makeFeathersRestClient() const service = feathers.use('service-todos', memory({ store: makeTodos() })) const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { serverAlias: 'basics' }) class ServiceTodo extends BaseModel { public static modelName = 'ServiceTodo' public static test = true } return { feathers, service, makeServicePlugin, BaseModel, ServiceTodo } } function makeAutoRemoveContext() { const feathers = makeFeathersRestClient() .use( 'todos', memory({ store: makeTodos() }) ) .use( 'tasks', memory({ store: makeTodos(), paginate: { default: 10, max: 50 } }) ) const todosService = feathers.service('todos') const tasksService = feathers.service('tasks') const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'autoRemove' }) class Todo extends BaseModel { public static modelName = 'Todo' public static servicePath = 'todos' public static test = true } class Task extends BaseModel { public static modelName = 'Task' public static servicePath = 'tasks' public static test = true } return { feathers, todosService, tasksService, makeServicePlugin, BaseModel, Todo, Task } } function makeSocketIoContext() { const { makeServicePlugin, BaseModel } = feathersVuex( feathersSocketioClient, { serverAlias: 'updates-store-on-events' } ) class Thing extends BaseModel { public static modelName = 'Thing' public static test = true public constructor(data = {}, options?) { super(data, options) } } class ThingDebounced extends BaseModel { public static modelName = 'ThingDebounced' public static test = true public constructor(data = {}, options?) { super(data, options) } } class TodoDebounced extends BaseModel { public static modelName = 'TodoDebounced' public static test = true public constructor(data = {}, options?) { super(data, options) } } const store = new Vuex.Store({ strict: true, plugins: [ makeServicePlugin({ Model: Thing, service: feathersSocketioClient.service('things'), servicePath: 'things' }), makeServicePlugin({ Model: ThingDebounced, service: feathersSocketioClient.service('things-debounced'), servicePath: 'things-debounced', debounceEventsTime: 20, namespace: 'things-debounced' }), makeServicePlugin({ Model: TodoDebounced, service: feathersSocketioClient.service('todos-debounced'), servicePath: 'todos-debounced', debounceEventsTime: 20, namespace: 'todos-debounced' }) ] }) const debouncedQueue = enableServiceEvents({ Model: TodoDebounced, service: feathersSocketioClient.service('todos-debounced'), store, options: store.state['todos-debounced'] }) return { feathersSocketioClient, makeServicePlugin, BaseModel, Thing, ThingDebounced, TodoDebounced, store, debouncedQueue } } describe('Service Module', function () { beforeEach(() => { clearModels() }) it('registers a vuex plugin and Model for the service', function () { const { makeServicePlugin, ServiceTodo, BaseModel } = makeContext() const serviceName = 'service-todos' const feathersService = feathersClient.service(serviceName) const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service(serviceName) }) ] }) assert( models['service-module'].hasOwnProperty('ServiceTodo'), 'the Model was added to the models' ) assert( feathersService.FeathersVuexModel === ServiceTodo, 'the Model is also found at service.FeathersVuexModel' ) const serviceTodo = new ServiceTodo({ description: 'Do the dishes', isComplete: false }) assert(serviceTodo instanceof ServiceTodo, 'Model can be instantiated.') assert(serviceTodo instanceof BaseModel, 'Model can be instantiated.') assert(store.state[serviceName]) }) describe('Models', function () { beforeEach(function () { const { makeServicePlugin, ServiceTodo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos') }) ] }) assert(store) assert( models['service-module'].hasOwnProperty('ServiceTodo'), 'the Model was added to the models' ) const owners = (this.owners = [ { id: 1, name: 'Marshall' }, { id: 2, name: 'Mariah' }, { id: 3, name: 'Leah' } ]) const data = { id: 1, description: 'Do the dishes', isComplete: false, owners } store.commit('service-todos/addItem', data) const serviceTodo = store.state['service-todos'].keyedById[1] this.serviceTodo = serviceTodo this.ServiceTodo = ServiceTodo }) it('allows creating model clones', function () { const { ServiceTodo } = this const serviceTodoClone = this.serviceTodo.clone() assert( serviceTodoClone.__isClone, 'created a todo clone with isClone attribute' ) assert( serviceTodoClone instanceof ServiceTodo, 'the copy is an instance of the same class' ) }) it('allows modifying clones without affecting the original', function () { const { serviceTodo } = this const serviceTodoClone = serviceTodo.clone() serviceTodoClone.description = 'Do something else' assert( serviceTodo.description === 'Do the dishes', 'the original todo remained intact' ) }) it('allows commiting changes back to the original in the store', function () { const { serviceTodo } = this const serviceTodoClone = serviceTodo.clone() serviceTodoClone.description = 'Do something else' serviceTodoClone.commit() assert( serviceTodo.description === 'Do something else', 'the original todo was updated' ) }) it('performs a shallow merge when commiting back to the original record', function () { const { serviceTodo, owners } = this const serviceTodoClone = serviceTodo.clone() serviceTodoClone.owners = [ { id: 1, name: 'Marshall' }, { id: 2, name: 'Mariah' } ] assert.deepEqual( serviceTodo.owners, owners, 'original todo remained unchanged' ) serviceTodoClone.commit() assert.deepEqual( serviceTodo.owners, [owners[0], owners[1]], 'ownerIds were updated properly' ) }) it(`the object returned from clone is not the same as the original`, function () { const { serviceTodo } = this const serviceTodoClone = serviceTodo.clone() assert(serviceTodo !== serviceTodoClone, 'the objects are distinct') }) it(`the object returned from commit is not the same as the clone`, function () { const { serviceTodo } = this const serviceTodoClone = serviceTodo.clone() const committedTodo = serviceTodoClone.commit() assert(committedTodo !== serviceTodoClone, 'the objects are distinct') }) it(`the object returned from commit is the same as the original`, function () { const { serviceTodo } = this const serviceTodoClone = serviceTodo.clone() const committedTodo = serviceTodoClone.commit() assert(serviceTodo === committedTodo, 'the objects are the same') }) it(`nested arrays are distinct after clone`, function () { const { ServiceTodo } = this const todo = new ServiceTodo({ description: 'test', owners: ['Marshall', 'Mariah'] }) const clone = todo.clone() assert( todo.owners !== clone.owners, 'the arrays are not the same in memory' ) }) it.skip(`modifying a clone after calling commit() does not change the original `, function () { const { serviceTodo, owners } = this const serviceTodoClone = serviceTodo.clone() assert.deepEqual( serviceTodo.owners, owners, 'original todo remained unchanged' ) serviceTodoClone.commit() serviceTodoClone.owners[0].name = 'Ted' assert.deepEqual( serviceTodo.owners[0].name, 'Marshall', 'nested object in original todo was unchanged' ) }) it(`changes the original if you modify return value of a commit`, function () { const { serviceTodo, owners } = this let serviceTodoClone = serviceTodo.clone() assert.deepEqual( serviceTodo.owners, owners, 'original todo remained unchanged' ) serviceTodoClone = serviceTodoClone.commit() serviceTodoClone.owners[0].name = 'Ted' assert.deepEqual( serviceTodo.owners[0].name, 'Ted', 'nested object in original todo was changed' ) }) it(`allows shallow assign of data when cloning`, function () { const { serviceTodo } = this const serviceTodoClone = serviceTodo.clone({ isComplete: !serviceTodo.isComplete }) assert.equal( !serviceTodo.isComplete, serviceTodoClone.isComplete, 'clone value has changed' ) serviceTodoClone.commit() assert.equal( serviceTodo.isComplete, true, 'value has changed after commit' ) }) it('allows reseting copy changes back to match the original', function () { const { serviceTodo } = this const serviceTodoClone = serviceTodo.clone() serviceTodoClone.description = 'Do something else' serviceTodoClone.reset() assert( serviceTodo.description === 'Do the dishes', 'the original todo was untouched' ) assert( serviceTodoClone.description === 'Do the dishes', 'the clone was reset to match the original' ) }) it('adds additional properties to model instances when more data arrives for the same id', function () { const { serviceTodo, owners } = this const newData = { id: 1, description: 'Do the dishes', isComplete: false, owners, test: true } const newTodo = new serviceTodo.constructor(newData) assert(newTodo === serviceTodo, 'the records are the same') assert(newTodo.test === true, 'the new attribute was added') assert( serviceTodo.test === true, 'the new attribute was also added to the original' ) }) it('ignores when new data with matching id has fewer props than current record', function () { const { serviceTodo, owners } = this const newData = { id: 1, owners } const newTodo = new serviceTodo.constructor(newData) assert(newTodo === serviceTodo, 'the records are the same') assert( serviceTodo.description === 'Do the dishes', 'the existing attributes remained in place' ) assert( serviceTodo.isComplete === false, 'the existing attributes remained in place' ) }) it('updates the new record when non-null, non-undefined values do not match', function () { const { serviceTodo, owners } = this const newData = { id: 1, description: 'Do the mopping', isComplete: true, owners } const newTodo = new serviceTodo.constructor(newData) assert(newTodo === serviceTodo, 'the records are the same') assert( serviceTodo.description === 'Do the mopping', 'non-matching string was updated' ) assert( serviceTodo.isComplete === true, 'non-matching boolean was updated' ) }) }) describe('Setting Up', () => { it('service stores have global defaults', function () { const { makeServicePlugin, BaseModel, Task } = makeContext() class Todo extends BaseModel { public static modelName = 'Todo' public static test = true } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Task, service: feathersClient.service('tasks') }), makeServicePlugin({ Model: Todo, service: feathersClient.service('/v2/todos') }) ] }) const { state } = store assert(state.tasks.idField === 'id', 'default idField is `id`') assert(state.tasks.autoRemove === false, 'autoRemove is off by default') assert(state.todos, 'uses `short` nameStyle by default') }) it('can customize the idField for each service', function () { const { makeServicePlugin, Test, Person } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ idField: '_id', Model: Test, service: feathersClient.service('tests') }), makeServicePlugin({ idField: 'name', Model: Person, service: feathersClient.service('people') }) ] }) assert( store.state.tests.idField === '_id', 'the idField was properly set' ) assert( // @ts-ignore store.state.people.idField === 'name', 'the idField was properly set' ) }) it('allows enabling autoRemove', function () { const { makeServicePlugin, Test } = makeContext() const autoRemove = true const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Test, service: feathersClient.service('tests'), autoRemove }) ] }) assert( store.state.tests.autoRemove === autoRemove, 'the autoRemove was enabled' ) }) it('can switch to path name as namespace', function () { const { makeServicePlugin, Test } = makeContext() const plugin = makeServicePlugin({ Model: Test, service: feathersClient.service('/v1/tests'), nameStyle: 'path' }) const store = new Vuex.Store({ plugins: [plugin] }) const namespace = stripSlashes('/v1/tests') assert( store.state[namespace], 'the full path name was used as a namespace' ) }) it('can explicitly provide a namespace', function () { const { makeServicePlugin, Test } = makeContext() const namespace = 'blah' const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Test, service: feathersClient.service('/v1/tests'), namespace }) ] }) assert(store.state.blah, 'the namespace option was used as the namespace') }) it('prioritizes the explicit namespace', function () { const { makeServicePlugin, Test } = makeContext() const namespace = 'blah' const nameStyle = 'path' const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Test, service: feathersClient.service('/v1/tests'), namespace, nameStyle }) ] }) assert(store.state.blah, 'the namespace option was used as the namespace') }) }) describe('Basics', () => { it('populates default store', function () { const { makeServicePlugin, feathers, ServiceTodo } = makeContextWithState() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ servicePath: 'service-todos', Model: ServiceTodo, service: feathers.service('service-todos') }) ] }) const todoState = store.state['service-todos'] const expectedState = { addOnUpsert: false, autoRemove: false, copiesById: {}, debug: false, enableEvents: true, errorOnCreate: null, errorOnGet: null, errorOnPatch: null, errorOnRemove: null, errorOnUpdate: null, errorOnFind: null, idField: 'id', ids: [], isFindPending: false, isGetPending: false, isCreatePending: false, isUpdatePending: false, isPatchPending: false, isRemovePending: false, keepCopiesInStore: false, debounceEventsTime: null, debounceEventsMaxWait: 1000, keyedById: {}, nameStyle: 'short', namespace: 'service-todos', modelName: 'ServiceTodo', serverAlias: 'basics', skipRequestIfExists: false, preferUpdate: false, replaceItems: false, servicePath: 'service-todos', tempIdField: '__id', tempsById: {}, pagination: { defaultLimit: null, defaultSkip: null }, paramsForServer: ['$populateParams'], whitelist: [], isIdCreatePending: [], isIdUpdatePending: [], isIdPatchPending: [], isIdRemovePending: [] } assert.deepEqual( todoState, expectedState, 'the expected state was returned' ) }) it('throws an error if no service is provided', function () { const { makeServicePlugin } = makeContext() try { new Vuex.Store({ // @ts-ignore plugins: [makeServicePlugin({})] }) } catch (error) { assert.equal( error.message, 'No service was provided. If you passed one in, check that you have configured a transport plugin on the Feathers Client. Make sure you use the client version of the transport.', 'threw an error' ) } }) describe('Auto-Remove Items', function () { beforeEach(function () { clearModels() }) it(`removes missing items when pagination is off`, function (done) { const { makeServicePlugin, Todo, todosService } = makeAutoRemoveContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Todo, service: todosService, idField: '_id', autoRemove: true }) ] }) const todoState = store.state['todos'] assert(todoState.ids.length === 0) // Load some data into the store store .dispatch('todos/find', { query: {} }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .then(todos => { // Remove the third item from the service // @ts-ignore delete todosService.store[3] // We went around using the store actions, so there will still be three items. assert( todoState.ids.length === 3, 'there are still three items in the store' ) // Perform the same query again return store.dispatch('todos/find', { query: {} }) }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .then(todos => { assert( todoState.ids.length === 2, 'there are now two items in the store' ) done() }) .catch(error => { assert(!error, error.message) done() }) }) it(`does not remove missing items when pagination is on`, function (done) { const { makeServicePlugin, Task, tasksService } = makeAutoRemoveContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Task, service: tasksService, idField: '_id', autoRemove: true }) ] }) const taskState = store.state.tasks assert(taskState.ids.length === 0) // Load some data into the store store .dispatch('tasks/find', { query: {} }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .then(todos => { // Remove the third item from the service // @ts-ignore delete tasksService.store[3] // We went around using the store actions, so there will still be three items. assert( taskState.ids.length === 3, 'there are still three items in the store' ) // Perform the same query again return store.dispatch('tasks/find', { query: {} }) }) .then(todos => { assert(todos.hasOwnProperty('total'), 'pagination is on') assert( taskState.ids.length === 3, 'there are still three items in the store' ) done() }) .catch(error => { assert(!error, error.message) done() }) }) it(`does not remove missing items when autoRemove is off`, function (done) { const { makeServicePlugin, Todo, todosService } = makeAutoRemoveContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Todo, service: todosService, idField: '_id' }) ] }) const todoState = store.state.todos assert(todoState.ids.length === 0) // Load some data into the store store .dispatch('todos/find', { query: {} }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .then(todos => { // Remove the third item from the service // @ts-ignore delete todosService.store[3] // We went around using the store actions, so there will still be three items. assert( todoState.ids.length === 3, 'there are still three items in the store' ) // Perform the same query again return store.dispatch('todos/find', { query: {} }) }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .then(todos => { assert( todoState.ids.length === 3, 'there are still three items in the store' ) done() }) .catch(error => { assert(!error, error.message) done() }) }) }) }) describe('Customizing Service Stores', function () { describe('New "extend" method', () => { it('allows access to the store and default module', function () { const { makeServicePlugin, ServiceTodo } = makeContext() new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos'), extend: ({ store, module }) => { assert.ok(store, 'should have received received the store') assert.ok(module.state, 'should have default state') assert.ok(module.getters, 'should have default getters') assert.ok(module.mutations, 'should have default mutations') assert.ok(module.actions, 'should have default actions') assert.ok(module.namespaced, 'should have default namespaced') return {} } }) ] }) }) it('allows adding custom state', function () { const { makeServicePlugin, ServiceTodo } = makeContext() const customState = { test: true, test2: { test: true } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos'), extend: () => { return { state: customState } } }) ] }) assert(store.state['service-todos'].test === true, 'added custom state') assert( store.state['service-todos'].test2.test === true, 'added custom state' ) }) it('allows custom mutations', function () { const { makeServicePlugin, ServiceTodo } = makeContext() const state = { test: true } const customMutations = { setTestToFalse(state) { state.test = false } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos'), extend: () => ({ state, mutations: customMutations }) }) ] }) store.commit('service-todos/setTestToFalse') assert( store.state['service-todos'].test === false, 'the custom state was modified by the custom mutation' ) }) it('allows custom getters', function () { const { makeServicePlugin, ServiceTodo } = makeContext() const customGetters = { // eslint-disable-next-line @typescript-eslint/no-unused-vars oneTwoThree(state) { return 123 } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos'), extend: () => ({ getters: customGetters }) }) ] }) assert( store.getters['service-todos/oneTwoThree'] === 123, 'the custom getter was available' ) }) it('allows adding custom actions', function () { const { makeServicePlugin, ServiceTodo } = makeContext() const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos'), extend: () => ({ state: { isTrue: false }, mutations: { setToTrue(state) { state.isTrue = true } }, actions: { trigger(context) { context.commit('setToTrue') } } }) }) ] }) store.dispatch('service-todos/trigger') assert( store.state['service-todos'].isTrue === true, 'the custom action was run' ) }) }) describe('Deprecated options', () => { it('allows adding custom state', function () { const { makeServicePlugin, ServiceTodo } = makeContext() const customState = { test: true, test2: { test: true } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos'), state: customState }) ] }) assert(store.state['service-todos'].test === true, 'added custom state') assert( store.state['service-todos'].test2.test === true, 'added custom state' ) }) it('allows custom mutations', function () { const { makeServicePlugin, ServiceTodo } = makeContext() const state = { test: true } const customMutations = { setTestToFalse(state) { state.test = false } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos'), state, mutations: customMutations }) ] }) store.commit('service-todos/setTestToFalse') assert( store.state['service-todos'].test === false, 'the custom state was modified by the custom mutation' ) }) it('allows custom getters', function () { const { makeServicePlugin, ServiceTodo } = makeContext() const customGetters = { // eslint-disable-next-line @typescript-eslint/no-unused-vars oneTwoThree(state) { return 123 } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos'), getters: customGetters }) ] }) assert( store.getters['service-todos/oneTwoThree'] === 123, 'the custom getter was available' ) }) it('allows adding custom actions', function () { const { makeServicePlugin, ServiceTodo } = makeContext() const config = { state: { isTrue: false }, mutations: { setToTrue(state) { state.isTrue = true } }, actions: { trigger(context) { context.commit('setToTrue') } } } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: ServiceTodo, service: feathersClient.service('service-todos'), ...config }) ] }) store.dispatch('service-todos/trigger') assert( store.state['service-todos'].isTrue === true, 'the custom action was run' ) }) }) }) describe('Updates the Store on Events', function () { let feathersSocketioClient, debouncedQueue, store, debouncedService: Service beforeEach(() => { const context = makeSocketIoContext() feathersSocketioClient = context.feathersSocketioClient debouncedQueue = context.debouncedQueue store = context.store debouncedService = feathersSocketioClient.service('things-debounced') }) it('created', function (done) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const listener = item => { assert( // @ts-ignore store.state.things.keyedById[0].test, 'the item received from the socket event was added to the store' ) feathersSocketioClient.service('things').off('created', listener) done() } feathersSocketioClient.service('things').on('created', listener) feathersSocketioClient.service('things').create({ test: true }) }) it('created debounced', function (done) { const { debounceEventsTime } = store.state['things-debounced'] // eslint-disable-next-line @typescript-eslint/no-unused-vars const listener = item => { assert( !store.state['things-debounced'].keyedById[0], 'the item is not added immediately' ) setTimeout(() => { assert( store.state['things-debounced'].keyedById[0].test, 'the item received from the socket event was added to the store' ) debouncedService.off('created', listener) done() }, debounceEventsTime * 2) } debouncedService.on('created', listener) debouncedService.create({ test: true }) }) it('patched', function (done) { store.commit('things/addItem', { id: 1, test: false }) // eslint-disable-next-line @typescript-eslint/no-unused-vars feathersSocketioClient.service('things').on('patched', item => { assert( store.state.things.keyedById[1].test, 'the item received from the socket event was updated in the store' ) done() }) feathersSocketioClient.service('things').patch(1, { test: true }) }) it('patched debounced', function (done) { const { debounceEventsTime } = store.state['things-debounced'] store.commit('things-debounced/clearAll') store.commit('things-debounced/addItem', { id: 1, test: false }) // eslint-disable-next-line @typescript-eslint/no-unused-vars const listener = item => { assert( !store.state['things-debounced'].keyedById[1].test, 'the item is not updated immediately' ) setTimeout(() => { assert( store.state['things-debounced'].keyedById[1].test, 'the item received from the socket event was updated in the store' ) }, debounceEventsTime * 2) debouncedService.off('patched', listener) done() } debouncedService.on('patched', listener) debouncedService.patch(1, { test: true }) }) it('updated', function (done) { store.commit('things/addItem', { id: 1, test: false }) // eslint-disable-next-line @typescript-eslint/no-unused-vars feathersSocketioClient.service('things').on('updated', item => { assert( store.state.things.keyedById[1].test, 'the item received from the socket event was updated in the store' ) done() }) feathersSocketioClient.service('things').update(1, { test: true }) }) it('updated debounced', function (done) { const { debounceEventsTime } = store.state['things-debounced'] store.commit('things-debounced/clearAll') store.commit('things-debounced/addItem', { id: 1, test: false }) // eslint-disable-next-line @typescript-eslint/no-unused-vars const listener = item => { assert( !store.state['things-debounced'].keyedById[1].test, 'the item is not updated immediately' ) setTimeout(() => { assert( store.state['things-debounced'].keyedById[1].test, 'the item received from the socket event was updated in the store' ) done() }, debounceEventsTime * 2) debouncedService.off('updated', listener) } debouncedService.on('updated', listener) debouncedService.update(1, { test: true }) }) it('removed', function (done) { store.commit('things/addItem', { id: 1, test: false }) // eslint-disable-next-line @typescript-eslint/no-unused-vars feathersSocketioClient.service('things').on('removed', item => { assert( !store.state.things.keyedById[1], 'the item received from the socket event was removed from the store' ) done() }) feathersSocketioClient.service('things').remove(1) }) it('removed debounced', function (done) { const { debounceEventsTime } = store.state['things-debounced'] store.commit('things-debounced/clearAll') store.commit('things-debounced/addItem', { id: 1, test: false }) // eslint-disable-next-line @typescript-eslint/no-unused-vars const listener = item => { assert( store.state['things-debounced'].keyedById[1], 'the item is not removed immediately' ) setTimeout(() => { assert( !store.state.things.keyedById[1], 'the item received from the socket event was removed from the store' ) done() }, debounceEventsTime * 2) debouncedService.off('removed', listener) } debouncedService.on('removed', listener) debouncedService.remove(1) }) it('debounce works with plenty items', function (done) { store.commit('things-debounced/clearAll') const { debounceEventsTime, debounceEventsMaxWait } = store.state[ 'things-debounced' ] const itemsCount = 100 let i = 0 assert( Object.keys(store.state['things-debounced'].keyedById).length === 0, 'no items at start' ) const now = performance.now() const setTimeoutCreate = () => { setTimeout(() => { debouncedService.create({ test: true, i }) i++ if (i < itemsCount) { if (performance.now() - now < debounceEventsMaxWait) { assert( Object.keys(store.state['things-debounced'].keyedById) .length === 0, `no items at i: ${i}` ) } setTimeoutCreate() } else { setTimeout(() => { assert( Object.keys( store.state['things-debounced'].keyedById.length === itemsCount ), 'all items are in store' ) done() }, debounceEventsTime * 2) } }, debounceEventsTime / 4) } setTimeoutCreate() }) it('debounced events get invoked during continuous events', function (done) { store.commit('things-debounced/clearAll') const { debounceEventsTime, debounceEventsMaxWait } = store.state[ 'things-debounced' ] assert( Object.keys(store.state['things-debounced'].keyedById).length === 0, 'no items at start' ) assert(debounceEventsMaxWait > 0, 'maxWait is set') const startedAt = performance.now() let i = 0 const setTimeoutCreate = () => { setTimeout(() => { debouncedService.create({ test: true, i }) i++ const timePassed = Math.floor( performance.now() - startedAt - debounceEventsTime ) if (timePassed <= debounceEventsMaxWait) { if (performance.now() - startedAt <= debounceEventsMaxWait) { assert( Object.keys(store.state['things-debounced'].keyedById) .length === 0, `no items at i: ${i}, milliseconds passed: ${timePassed}` ) } setTimeoutCreate() } else { assert( Object.keys(store.state['things-debounced'].keyedById).length === i - 1, `items are inserted after maxWait` ) done() } }, debounceEventsTime / 4) } setTimeoutCreate() }) it('debounded remove after addOrUpdate also removes addOrUpdate queue and vise versa', function () { const { idField } = store.state['todos-debounced'] assert( Object.keys(debouncedQueue.addOrUpdateById).length === 0, "'addOrUpdateById' initially empty" ) assert( Object.keys(debouncedQueue.removeItemById).length === 0, "'removeItemById' initially empty" ) debouncedQueue.enqueueAddOrUpdate({ [idField]: 1, test: true }) assert( debouncedQueue.addOrUpdateById[1], "queued item for 'addOrUpdate' correctly" ) debouncedQueue.enqueueRemoval({ [idField]: 1, test: false }) assert( !debouncedQueue.addOrUpdateById[1], "queued item for 'addOrUpdate' removed immediately" ) assert( debouncedQueue.removeItemById[1], 'queued item for removal correctly' ) debouncedQueue.enqueueAddOrUpdate({ [idField]: 1, test: true }) assert( debouncedQueue.addOrUpdateById[1], "queued item for 'addOrUpdate' correctly again" ) assert( !debouncedQueue.removeItemById[1], "queued item for 'remove' removed immediately" ) }) }) }) ================================================ FILE: test/service-module/types.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ export interface ServiceState { options: {} ids: (string | number)[] autoRemove: boolean errorOnFind: any errorOnGet: any errorOnCreate: any errorOnPatch: any errorOnUpdate: any errorOnRemove: any isFindPending: boolean isGetPending: boolean isCreatePending: boolean isPatchPending: boolean isUpdatePending: boolean isRemovePending: boolean idField: string keyedById: {} tempsById: {} tempsByNewId: {} whitelist: string[] paramsForServer: string[] namespace: string nameStyle: string // Should be enum of 'short' or 'path' pagination?: { default: PaginationState } modelName: string } export interface PaginationState { ids: any limit: number skip: number ip: number total: number mostRecent: any } export interface Location { coordinates: number[] } ================================================ FILE: test/test-utils.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' export function assertGetter(item, prop, value) { assert( typeof Object.getOwnPropertyDescriptor(item, prop).get === 'function', 'getter in place' ) assert.equal(item[prop], value, 'returned value matches') } export const makeStore = () => { return { 0: { id: 0, description: 'Do the first', isComplete: false }, 1: { id: 1, description: 'Do the second', isComplete: false }, 2: { id: 2, description: 'Do the third', isComplete: false }, 3: { id: 3, description: 'Do the fourth', isComplete: false }, 4: { id: 4, description: 'Do the fifth', isComplete: false }, 5: { id: 5, description: 'Do the sixth', isComplete: false }, 6: { id: 6, description: 'Do the seventh', isComplete: false }, 7: { id: 7, description: 'Do the eighth', isComplete: false }, 8: { id: 8, description: 'Do the ninth', isComplete: false }, 9: { id: 9, description: 'Do the tenth', isComplete: false } } } export const makeStoreWithAtypicalIds = () => { return { 0: { someId: 0, description: 'Do the first', isComplete: false }, 1: { someId: 1, description: 'Do the second', isComplete: false }, 2: { someId: 2, description: 'Do the third', isComplete: false }, 3: { someId: 3, description: 'Do the fourth', isComplete: false }, 4: { someId: 4, description: 'Do the fifth', isComplete: false }, 5: { someId: 5, description: 'Do the sixth', isComplete: false }, 6: { someId: 6, description: 'Do the seventh', isComplete: false }, 7: { someId: 7, description: 'Do the eighth', isComplete: false }, 8: { someId: 8, description: 'Do the ninth', isComplete: false }, 9: { someId: 9, description: 'Do the tenth', isComplete: false } } } ================================================ FILE: test/use/InstrumentComponent.js ================================================ import useGet from '../../src/useGet' export default { name: 'InstrumentComponent', template: '
{{ instrument }}
', props: { id: { type: String, default: '' } }, setup(props, context) { const { Instrument } = context.root.$FeathersVuex const instrumentData = useGet({ model: Instrument, id: props.id }) return { instrument: instrumentData.item } } } ================================================ FILE: test/use/find.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0, @typescript-eslint/no-empty-function: 0 */ import Vue from 'vue' import VueCompositionApi from '@vue/composition-api' Vue.use(VueCompositionApi) import jsdom from 'jsdom-global' import { assert } from 'chai' import feathersVuex, { FeathersVuex } from '../../src/index' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import useFind from '../../src/useFind' import Vuex from 'vuex' // import { shallowMount } from '@vue/test-utils' import { computed, isRef } from '@vue/composition-api' jsdom() require('events').EventEmitter.prototype._maxListeners = 100 Vue.use(Vuex) Vue.use(FeathersVuex) function makeContext() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'useFind' }) class Instrument extends BaseModel { public static modelName = 'Instrument' } const serviceName = 'things' const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Instrument, service: feathersClient.service(serviceName) }) ] }) return { store, Instrument, BaseModel, makeServicePlugin } } describe('use/find', function () { it('returns correct default data', function () { const { Instrument } = makeContext() const instrumentParams = computed(() => { return { query: {}, paginate: false } }) const instrumentsData = useFind({ model: Instrument, params: instrumentParams }) const { debounceTime, error, haveBeenRequested, haveLoaded, isPending, isLocal, items, latestQuery, paginationData, qid } = instrumentsData assert(isRef(debounceTime)) assert(debounceTime.value === null) assert(isRef(error)) assert(error.value === null) assert(isRef(haveBeenRequested)) assert(haveBeenRequested.value === true) assert(isRef(haveLoaded)) assert(haveLoaded.value === false) assert(isRef(isPending)) assert(isPending.value === true) assert(isRef(isLocal)) assert(isLocal.value === false) assert(isRef(items)) assert(Array.isArray(items.value)) assert(items.value.length === 0) assert(isRef(latestQuery)) assert(latestQuery.value === null) assert(isRef(paginationData)) assert.deepStrictEqual(paginationData.value, { defaultLimit: null, defaultSkip: null }) assert(isRef(qid)) assert(qid.value === 'default') }) it.skip('returns correct default data even when params is not reactive', function () { const { Instrument } = makeContext() const instrumentsData = useFind({ model: Instrument, params: { query: {}, paginate: false } }) const { debounceTime, error, haveBeenRequested, haveLoaded, isPending, isLocal, items, latestQuery, paginationData, qid } = instrumentsData assert(isRef(debounceTime)) assert(debounceTime.value === null) assert(isRef(error)) assert(error.value === null) assert(isRef(haveBeenRequested)) assert(haveBeenRequested.value === true) assert(isRef(haveLoaded)) assert(haveLoaded.value === false) assert(isRef(isPending)) assert(isPending.value === true) assert(isRef(isLocal)) assert(isLocal.value === false) assert(isRef(items)) assert(Array.isArray(items.value)) assert(items.value.length === 0) assert(isRef(latestQuery)) assert(latestQuery.value === null) assert(isRef(paginationData)) assert.deepStrictEqual(paginationData.value, { defaultLimit: null, defaultSkip: null }) assert(isRef(qid)) assert(qid.value === 'default') }) it('allows passing {immediate:false} to not query immediately', function () { const { Instrument } = makeContext() const instrumentParams = computed(() => { return { query: {}, paginate: false } }) const instrumentsData = useFind({ model: Instrument, params: instrumentParams, immediate: false }) const { haveBeenRequested } = instrumentsData assert(isRef(haveBeenRequested)) assert(haveBeenRequested.value === false) }) it('params can return null to prevent the query', function () { const { Instrument } = makeContext() const instrumentParams = computed(() => { return null }) const instrumentsData = useFind({ model: Instrument, params: instrumentParams, immediate: true }) const { haveBeenRequested } = instrumentsData assert(isRef(haveBeenRequested)) assert(haveBeenRequested.value === false) }) it('allows using `local: true` to prevent API calls from being made', function () { const { Instrument } = makeContext() const instrumentParams = computed(() => { return { query: {} } }) const instrumentsData = useFind({ model: Instrument, params: instrumentParams, local: true }) const { haveBeenRequested, find } = instrumentsData assert(isRef(haveBeenRequested)) assert(haveBeenRequested.value === false, 'no request during init') find() assert(haveBeenRequested.value === false, 'no request after find') }) }) ================================================ FILE: test/use/get.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0, @typescript-eslint/no-empty-function: 0 */ import Vue from 'vue' import VueCompositionApi from '@vue/composition-api' Vue.use(VueCompositionApi) import jsdom from 'jsdom-global' import { assert } from 'chai' import feathersVuex, { FeathersVuex } from '../../src/index' import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' import useGet from '../../src/useGet' import memory from 'feathers-memory' import Vuex from 'vuex' // import { mount, shallowMount } from '@vue/test-utils' // import InstrumentComponent from './InstrumentComponent' import { isRef } from '@vue/composition-api' import { HookContext } from '@feathersjs/feathers' jsdom() require('events').EventEmitter.prototype._maxListeners = 100 Vue.use(Vuex) Vue.use(FeathersVuex) // function timeoutPromise(wait = 0) { // return new Promise(resolve => { // setTimeout(() => { // resolve() // }, wait) // }) // } function makeContext() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'useGet' }) class Instrument extends BaseModel { public constructor(data, options?) { super(data, options) } public static modelName = 'Instrument' public static instanceDefaults(data) { return { name: '' } } } feathersClient.use( 'things', memory({ store: { 0: { id: 0, name: 'trumpet' }, 1: { id: 1, name: 'trombone' } }, paginate: { default: 10, max: 50 } }) ) const servicePath = 'instruments' const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Instrument, servicePath, service: feathersClient.service(servicePath) }) ] }) return { store, Instrument, BaseModel, makeServicePlugin } } describe('use/get', function () { it('returns correct default data', function () { const { Instrument } = makeContext() const id = 1 const existing = Instrument.getFromStore(id) assert(!existing, 'the current instrument is not in the store.') const instrumentData = useGet({ model: Instrument, id }) const { error, hasBeenRequested, hasLoaded, isPending, isLocal, item } = instrumentData assert(isRef(error)) assert(error.value === null) assert(isRef(hasBeenRequested)) assert(hasBeenRequested.value === true) assert(isRef(hasLoaded)) assert(hasLoaded.value === false) assert(isRef(isPending)) assert(isPending.value === true) assert(isRef(isLocal)) assert(isLocal.value === false) assert(isRef(item)) assert(item.value === null) }) it('allows passing {immediate:false} to not query immediately', function () { const { Instrument } = makeContext() const id = 1 const instrumentData = useGet({ model: Instrument, id, immediate: false }) const { hasBeenRequested } = instrumentData assert(isRef(hasBeenRequested)) assert(hasBeenRequested.value === false) }) it('id can return null id to prevent the query', function () { const { Instrument } = makeContext() const id = null const instrumentData = useGet({ model: Instrument, id }) const { hasBeenRequested } = instrumentData assert(isRef(hasBeenRequested)) assert(hasBeenRequested.value === false) }) it('allows using `local: true` to prevent API calls from being made', function () { const { Instrument } = makeContext() const id = 1 const instrumentData = useGet({ model: Instrument, id, local: true }) const { hasBeenRequested, get } = instrumentData assert(isRef(hasBeenRequested)) assert(hasBeenRequested.value === false, 'no request during init') get(id) assert(hasBeenRequested.value === false, 'no request after get') }) it('API only hit once on initial render', async function () { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'useGet' }) class Dohickey extends BaseModel { public static modelName = 'Dohickey' } const servicePath = 'dohickies' const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: Dohickey, servicePath, service: feathersClient.service(servicePath) }) ] }) let getCalls = 0 feathersClient.service(servicePath).hooks({ before: { get: [ (ctx: HookContext) => { getCalls += 1 ctx.result = { id: ctx.id } } ] } }) useGet({ model: Dohickey, id: 42 }) await new Promise((resolve) => setTimeout(resolve, 100)) assert(getCalls === 1, '`get` called once') }) }) ================================================ FILE: test/utils.test.ts ================================================ import { assert } from 'chai' import { AuthState } from '../src/auth-module/types' import { ServiceState } from './service-module/types' import { isNode, isBrowser } from '../src/utils' import { diff as deepDiff } from 'deep-object-diff' import { getId, initAuth, hydrateApi, getServicePrefix, getServiceCapitalization, getQueryInfo } from '../src/utils' import feathersVuex from '../src/index' import { feathersSocketioClient as feathersClient } from './fixtures/feathers-client' import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) interface RootState { auth: AuthState users: ServiceState } describe('Utils', function () { describe('getId', () => { const idField = '_id' it('converts objects to strings', () => { const _id = { test: true } const id = getId({ _id }, idField) assert.strictEqual(typeof id, 'string') assert.strictEqual(id, _id.toString()) }) it('does not convert number ids', () => { const _id = 1 const id = getId({ _id }, idField) assert.strictEqual(typeof id, 'number') assert.strictEqual(id, _id) }) it('automatically finds _id', () => { const _id = 1 const id = getId({ _id }) assert.strictEqual(id, _id) }) it('automatically finds id', () => { const referenceId = 1 const id = getId({ id: referenceId }) assert.strictEqual(id, referenceId) }) it('prefers id over _id (only due to their order in the code)', () => { const _id = 1 const referenceId = 2 const id = getId({ _id, id: referenceId }) assert.strictEqual(id, referenceId) }) }) describe('Auth & SSR', () => { before(function () { const { makeServicePlugin, makeAuthPlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'utils' }) class User extends BaseModel { public static modelName = 'User' public static test = true } Object.assign(this, { makeServicePlugin, makeAuthPlugin, BaseModel, User }) }) it('properly populates auth', function () { const store = new Vuex.Store({ plugins: [ this.makeServicePlugin({ Model: this.User, servicePath: 'users', service: feathersClient.service('users') }), this.makeAuthPlugin({}) ] }) const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoiOTk5OTk5OTk5OTkiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.lUlEd3xH-TnlNRbKM3jnDVTNoIg10zgzaS6QyFZE-6g' const req = { headers: { cookie: 'feathers-jwt=' + accessToken } } return initAuth({ commit: store.commit, req, moduleName: 'auth', cookieName: 'feathers-jwt', feathersClient }) .then(() => { assert( store.state.auth.accessToken === accessToken, 'the token was in place' ) assert(store.state.auth.payload, 'the payload was set') return feathersClient.authentication.getAccessToken() }) .then(token => { assert.isDefined(token, 'the feathers client storage was set') }) }) it('properly hydrate SSR store', function () { const { makeServicePlugin, BaseModel, models } = feathersVuex(feathersClient, { serverAlias: 'hydrate' }) class User extends BaseModel { public static modelName = 'User' public static test = true } const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: User, servicePath: 'users', service: feathersClient.service('users'), mutations: { addServerItem(state) { state.keyedById['abcdefg'] = { id: 'abcdefg', name: 'Guzz' } } } }) ] }) store.commit('users/addServerItem') assert(store.state.users.keyedById['abcdefg'], 'server document added') assert( store.state.users.keyedById['abcdefg'] instanceof Object, 'server document is pure javascript object' ) hydrateApi({ api: models.hydrate }) assert( store.state.users.keyedById['abcdefg'] instanceof User, 'document hydrated' ) }) }) describe('Inflections', function () { it('properly inflects the service prefix', function () { const decisionTable = [ ['todos', 'todos'], ['TODOS', 'tODOS'], ['environment-Panos', 'environmentPanos'], ['env-panos', 'envPanos'], ['envPanos', 'envPanos'], ['api/v1/env-panos', 'envPanos'], ['very-long-service', 'veryLongService'] ] decisionTable.forEach(([path, prefix]) => { assert( getServicePrefix(path) === prefix, `The service prefix for path "${path}" was "${getServicePrefix( path )}", expected "${prefix}"` ) }) }) it('properly inflects the service capitalization', function () { const decisionTable = [ ['todos', 'Todos'], ['TODOS', 'TODOS'], ['environment-Panos', 'EnvironmentPanos'], ['env-panos', 'EnvPanos'], ['envPanos', 'EnvPanos'], ['api/v1/env-panos', 'EnvPanos'], ['very-long-service', 'VeryLongService'] ] decisionTable.forEach(([path, prefix]) => { assert( getServiceCapitalization(path) === prefix, `The service prefix for path "${path}" was "${getServiceCapitalization( path )}", expected "${prefix}"` ) }) }) }) describe('Environments', () => { it('sets isNode to true', () => { assert(isNode, 'isNode was true') }) it('sets isBrowser to false', () => { assert(!isBrowser, 'isBrowser was false') }) }) }) describe('Pagination', function () { it('getQueryInfo', function () { const params = { qid: 'main-list', query: { test: true, $limit: 10, $skip: 0 } } const response = { data: [], limit: 10, skip: 0, total: 500 } const info = getQueryInfo(params, response) const expected = { isOutdated: undefined, qid: 'main-list', query: { test: true, $limit: 10, $skip: 0 }, queryId: '{"test":true}', queryParams: { test: true }, pageParams: { $limit: 10, $skip: 0 }, pageId: '{"$limit":10,"$skip":0}', response: undefined } const diff = deepDiff(info, expected) assert.deepEqual(info, expected, 'query info formatted correctly') }) it('getQueryInfo no limit or skip', function () { const params = { qid: 'main-list', query: { test: true } } const response = { data: [], limit: 10, skip: 0, total: 500 } const info = getQueryInfo(params, response) const expected = { isOutdated: undefined, qid: 'main-list', query: { test: true }, queryId: '{"test":true}', queryParams: { test: true }, pageParams: { $limit: 10, $skip: 0 }, pageId: '{"$limit":10,"$skip":0}', response: undefined } const diff = deepDiff(info, expected) assert.deepEqual(info, expected, 'query info formatted correctly') }) }) ================================================ FILE: test/vue-plugin.test.ts ================================================ /* eslint @typescript-eslint/explicit-function-return-type: 0, @typescript-eslint/no-explicit-any: 0 */ import { assert } from 'chai' import feathersVuex, { FeathersVuex } from '../src/index' import { feathersRestClient as feathersClient } from './fixtures/feathers-client' import Vue from 'vue/dist/vue' import Vuex from 'vuex' // @ts-ignore Vue.use(Vuex) // @ts-ignore Vue.use(FeathersVuex) interface VueWithFeathers { $FeathersVuex: {} } function makeContext() { const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { serverAlias: 'make-find-mixin' }) class FindModel extends BaseModel { public static modelName = 'FindModel' public static test: boolean = true } const serviceName = 'todos' const store = new Vuex.Store({ plugins: [ makeServicePlugin({ Model: FindModel, service: feathersClient.service(serviceName) }) ] }) return { store } } describe('Vue Plugin', function () { it('Adds the `$FeathersVuex` object to components', function () { const { store } = makeContext() const vm = new Vue({ name: 'todos-component', store, template: `
` }).$mount() assert(vm.$FeathersVuex, 'registeredPlugin correctly') }) }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, "outDir": "dist", "moduleResolution": "node", "target": "es6", "sourceMap": false, "declaration": true }, "include": ["src/**/*"], "exclude": ["node_modules", "**/*.test.js"] } ================================================ FILE: tsconfig.test.json ================================================ { "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, "outDir": "dist", "moduleResolution": "node", "target": "esnext", "sourceMap": true, "allowJs": true }, "include": ["src/**/*"], "exclude": ["node_modules", "**/*.test.js"] }