├── .babelrc ├── .editorconfig ├── .github ├── FUNDING.yml ├── contributing.md ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .istanbul.yml ├── .npmignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .vuepress │ ├── config.js │ └── public │ │ ├── favicon.ico │ │ └── img │ │ └── devtools.jpg ├── 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-lock.json ├── package.json ├── service-logo.png ├── 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 /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "add-module-exports" ], 3 | "presets": [ 4 | [ "env", { "modules": false } ], 5 | "es2015", 6 | "stage-2" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: marshallswain 4 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | 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. 6 | 7 | ## Report a bug 8 | 9 | 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. 10 | 11 | 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. 12 | 13 | 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**. 14 | 15 | ## Report a Security Concern 16 | 17 | 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. 18 | 19 | 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. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | 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. 26 | 27 | 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. 28 | 29 | 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: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | 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`. 36 | 37 | ### ES6 compilation 38 | 39 | 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 40 | 41 | > npm run compile 42 | 43 | __Note:__ `npm test` will run the compilation automatically before the tests. 44 | 45 | ### Tests 46 | 47 | [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. 48 | 49 | ### Documentation 50 | 51 | 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. 52 | 53 | ## External Modules 54 | 55 | 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. 56 | 57 | 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: 58 | 59 | ## Contributor Code of Conduct 60 | 61 | 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. 62 | 63 | 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. 64 | 65 | 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. 66 | 67 | 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. 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 70 | 71 | 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/) 72 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | (First please check that this issue is not already solved as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) 5 | 6 | - [ ] Tell us what broke. The more detailed the better. 7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 8 | 9 | ### Expected behavior 10 | Tell us what should happen 11 | 12 | ### Actual behavior 13 | Tell us what happens instead 14 | 15 | ### System configuration 16 | 17 | Tell us about the applicable parts of your setup. 18 | 19 | **Module versions** (especially the part that's not working): 20 | 21 | **NodeJS version**: 22 | 23 | **Operating System**: 24 | 25 | **Browser Version**: 26 | 27 | **React Native Version**: 28 | 29 | **Module Loader**: -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | ### Other Information 13 | 14 | If there's anything else that's important and relevant to your pull 15 | request, mention that information here. This could include 16 | benchmarks, or other information. 17 | 18 | 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. 19 | 20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 21 | 22 | Thanks for contributing to Feathers! :heart: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | 32 | # The compiled/babelified modules 33 | lib/ 34 | 35 | # Editor directories and files 36 | .idea 37 | .vscode/settings.json 38 | *.suo 39 | *.ntvs* 40 | *.njsproj 41 | *.sln 42 | /dist 43 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: ./src/ 4 | excludes: 5 | - lib/ 6 | include-all-sources: true 7 | reporting: 8 | print: summary 9 | reports: 10 | - html 11 | - text 12 | - lcov 13 | watermarks: 14 | statements: [50, 80] 15 | lines: [50, 80] 16 | functions: [50, 80] 17 | branches: [50, 80] -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .jshintrc 3 | .travis.yml 4 | .istanbul.yml 5 | .babelrc 6 | .idea/ 7 | test/ 8 | !lib/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | cache: yarn 4 | addons: 5 | code_climate: 6 | repo_token: 'your repo token' 7 | firefox: "51.0" 8 | services: 9 | - xvfb 10 | notifications: 11 | email: false -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "TS - Mocha Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "--require", 14 | "ts-node/register", 15 | "-u", 16 | "bdd", 17 | "--timeout", 18 | "999999", 19 | "--colors", 20 | "--recursive", 21 | "${workspaceFolder}/test/**/*.ts" 22 | ], 23 | "env": { 24 | "TS_NODE_PROJECT": "tsconfig.test.json" 25 | }, 26 | "internalConsoleOptions": "openOnSessionStart" 27 | }, 28 | { 29 | "type": "node", 30 | "request": "launch", 31 | "name": "Launch Program", 32 | "program": "${workspaceRoot}/lib/", 33 | "cwd": "${workspaceRoot}" 34 | }, 35 | { 36 | "type": "node", 37 | "request": "attach", 38 | "name": "Attach to Process", 39 | "port": 5858 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.trimTrailingWhitespace": true, 4 | "jshint.enable": false, 5 | "semistandard.enable": false, 6 | "standard.enable": true, 7 | "search.exclude": { 8 | "lib/**": true, 9 | "**/node_modules": true, 10 | "**/bower_components": true, 11 | "**/yarn.lock": true, 12 | "**/package-lock.json": true 13 | }, 14 | "workbench.colorCustomizations": { 15 | "activityBar.background": "#2B3011", 16 | "titleBar.activeBackground": "#3C4418", 17 | "titleBar.activeForeground": "#FAFBF4" 18 | }, 19 | "editor.defaultFormatter": "esbenp.prettier-vscode", 20 | "prettier.arrowParens": "avoid", 21 | "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false 22 | } 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.0.0-pre.3](https://github.com/feathersjs/feathers-vuex/tree/v1.0.0-pre.3) (2017-08-26) 4 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v1.0.0-pre.2...v1.0.0-pre.3) 5 | 6 | **Closed issues:** 7 | 8 | - \[Proposal\] Allow existing store modules to provide additional properties like the customActions, etc. [\#36](https://github.com/feathersjs/feathers-vuex/issues/36) 9 | 10 | ## [v1.0.0-pre.2](https://github.com/feathersjs/feathers-vuex/tree/v1.0.0-pre.2) (2017-08-24) 11 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v1.0.0-pre.1...v1.0.0-pre.2) 12 | 13 | ## [v1.0.0-pre.1](https://github.com/feathersjs/feathers-vuex/tree/v1.0.0-pre.1) (2017-08-22) 14 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.8.0...v1.0.0-pre.1) 15 | 16 | **Closed issues:** 17 | 18 | - Update mutation not changing store or backend [\#38](https://github.com/feathersjs/feathers-vuex/issues/38) 19 | - namespaces cannot be arrays for nested modules \(supported by vuex\) [\#34](https://github.com/feathersjs/feathers-vuex/issues/34) 20 | - using deep-assign package [\#15](https://github.com/feathersjs/feathers-vuex/issues/15) 21 | 22 | ## [v0.8.0](https://github.com/feathersjs/feathers-vuex/tree/v0.8.0) (2017-08-18) 23 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.7.2...v0.8.0) 24 | 25 | **Closed issues:** 26 | 27 | - 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) 28 | - OAuth with Google and feathers-vuex [\#33](https://github.com/feathersjs/feathers-vuex/issues/33) 29 | 30 | **Merged pull requests:** 31 | 32 | - Support array namespaces for module nesting [\#35](https://github.com/feathersjs/feathers-vuex/pull/35) ([jskrzypek](https://github.com/jskrzypek)) 33 | 34 | ## [v0.7.2](https://github.com/feathersjs/feathers-vuex/tree/v0.7.2) (2017-08-04) 35 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.7.1...v0.7.2) 36 | 37 | **Closed issues:** 38 | 39 | - Mapped Action is not a function [\#31](https://github.com/feathersjs/feathers-vuex/issues/31) 40 | - Real time updates stops working sometimes depending on the query [\#28](https://github.com/feathersjs/feathers-vuex/issues/28) 41 | 42 | **Merged pull requests:** 43 | 44 | - 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)) 45 | 46 | ## [v0.7.1](https://github.com/feathersjs/feathers-vuex/tree/v0.7.1) (2017-07-08) 47 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.7.0...v0.7.1) 48 | 49 | **Closed issues:** 50 | 51 | - errorOnAuthenticate is not reset [\#29](https://github.com/feathersjs/feathers-vuex/issues/29) 52 | - Hoping to support server paging parameters [\#23](https://github.com/feathersjs/feathers-vuex/issues/23) 53 | 54 | **Merged pull requests:** 55 | 56 | - Fix 29 [\#30](https://github.com/feathersjs/feathers-vuex/pull/30) ([mgesmundo](https://github.com/mgesmundo)) 57 | 58 | ## [v0.7.0](https://github.com/feathersjs/feathers-vuex/tree/v0.7.0) (2017-06-27) 59 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.6.0...v0.7.0) 60 | 61 | **Merged pull requests:** 62 | 63 | - Remove “feathers” module completely [\#27](https://github.com/feathersjs/feathers-vuex/pull/27) ([marshallswain](https://github.com/marshallswain)) 64 | 65 | ## [v0.6.0](https://github.com/feathersjs/feathers-vuex/tree/v0.6.0) (2017-06-27) 66 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.5.0...v0.6.0) 67 | 68 | **Merged pull requests:** 69 | 70 | - Allow customizing a service’s default store [\#26](https://github.com/feathersjs/feathers-vuex/pull/26) ([marshallswain](https://github.com/marshallswain)) 71 | 72 | ## [v0.5.0](https://github.com/feathersjs/feathers-vuex/tree/v0.5.0) (2017-06-27) 73 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.4.2...v0.5.0) 74 | 75 | **Closed issues:** 76 | 77 | - Are feathers-reactive and RxJS dependencies necessary? [\#22](https://github.com/feathersjs/feathers-vuex/issues/22) 78 | - Weird / destructive behaviour using mapActions service find between components [\#21](https://github.com/feathersjs/feathers-vuex/issues/21) 79 | - 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) 80 | 81 | **Merged pull requests:** 82 | 83 | - Remove all non-serializable data from the state [\#25](https://github.com/feathersjs/feathers-vuex/pull/25) ([marshallswain](https://github.com/marshallswain)) 84 | - Opt in to `autoRemove` [\#24](https://github.com/feathersjs/feathers-vuex/pull/24) ([marshallswain](https://github.com/marshallswain)) 85 | 86 | ## [v0.4.2](https://github.com/feathersjs/feathers-vuex/tree/v0.4.2) (2017-06-07) 87 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.4.1...v0.4.2) 88 | 89 | **Closed issues:** 90 | 91 | - 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) 92 | - items not being removed from the list - fix proposal [\#12](https://github.com/feathersjs/feathers-vuex/issues/12) 93 | 94 | **Merged pull requests:** 95 | 96 | - QuickFix: Use idField for removal [\#20](https://github.com/feathersjs/feathers-vuex/pull/20) ([cmeissl](https://github.com/cmeissl)) 97 | 98 | ## [v0.4.1](https://github.com/feathersjs/feathers-vuex/tree/v0.4.1) (2017-05-26) 99 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.4.0...v0.4.1) 100 | 101 | **Closed issues:** 102 | 103 | - Is possible to upload a file using feathers-vuex? [\#11](https://github.com/feathersjs/feathers-vuex/issues/11) 104 | - Integration with Nuxt [\#8](https://github.com/feathersjs/feathers-vuex/issues/8) 105 | 106 | **Merged pull requests:** 107 | 108 | - Bugfix - add params to patch action service call [\#14](https://github.com/feathersjs/feathers-vuex/pull/14) ([ndamjan](https://github.com/ndamjan)) 109 | - fix item removal in addOrUpdateList \(\#12\) [\#13](https://github.com/feathersjs/feathers-vuex/pull/13) ([ndamjan](https://github.com/ndamjan)) 110 | 111 | ## [v0.4.0](https://github.com/feathersjs/feathers-vuex/tree/v0.4.0) (2017-05-01) 112 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.3.1...v0.4.0) 113 | 114 | ## [v0.3.1](https://github.com/feathersjs/feathers-vuex/tree/v0.3.1) (2017-05-01) 115 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.3.0...v0.3.1) 116 | 117 | ## [v0.3.0](https://github.com/feathersjs/feathers-vuex/tree/v0.3.0) (2017-05-01) 118 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.2.2...v0.3.0) 119 | 120 | **Merged pull requests:** 121 | 122 | - Node: Keep functions out of Vuex state [\#9](https://github.com/feathersjs/feathers-vuex/pull/9) ([marshallswain](https://github.com/marshallswain)) 123 | 124 | ## [v0.2.2](https://github.com/feathersjs/feathers-vuex/tree/v0.2.2) (2017-04-28) 125 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.2.1...v0.2.2) 126 | 127 | ## [v0.2.1](https://github.com/feathersjs/feathers-vuex/tree/v0.2.1) (2017-04-18) 128 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.2.0...v0.2.1) 129 | 130 | **Closed issues:** 131 | 132 | - `clearList` mutation behaves unexpectedly if `current` isn't defined [\#5](https://github.com/feathersjs/feathers-vuex/issues/5) 133 | 134 | **Merged pull requests:** 135 | 136 | - Fix clearList unexpected behavior. Closes \#5 [\#6](https://github.com/feathersjs/feathers-vuex/pull/6) ([silvestreh](https://github.com/silvestreh)) 137 | 138 | ## [v0.2.0](https://github.com/feathersjs/feathers-vuex/tree/v0.2.0) (2017-04-18) 139 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.1.1...v0.2.0) 140 | 141 | **Merged pull requests:** 142 | 143 | - Actions [\#4](https://github.com/feathersjs/feathers-vuex/pull/4) ([marshallswain](https://github.com/marshallswain)) 144 | 145 | ## [v0.1.1](https://github.com/feathersjs/feathers-vuex/tree/v0.1.1) (2017-04-18) 146 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.1.0...v0.1.1) 147 | 148 | ## [v0.1.0](https://github.com/feathersjs/feathers-vuex/tree/v0.1.0) (2017-04-18) 149 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.0.5...v0.1.0) 150 | 151 | **Merged pull requests:** 152 | 153 | - Service tests clear list [\#3](https://github.com/feathersjs/feathers-vuex/pull/3) ([marshallswain](https://github.com/marshallswain)) 154 | - add before-install script [\#2](https://github.com/feathersjs/feathers-vuex/pull/2) ([marshallswain](https://github.com/marshallswain)) 155 | 156 | ## [v0.0.5](https://github.com/feathersjs/feathers-vuex/tree/v0.0.5) (2017-04-15) 157 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.0.4...v0.0.5) 158 | 159 | **Merged pull requests:** 160 | 161 | - Update readme.md [\#1](https://github.com/feathersjs/feathers-vuex/pull/1) ([marshallswain](https://github.com/marshallswain)) 162 | 163 | ## [v0.0.4](https://github.com/feathersjs/feathers-vuex/tree/v0.0.4) (2017-04-12) 164 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.0.3...v0.0.4) 165 | 166 | ## [v0.0.3](https://github.com/feathersjs/feathers-vuex/tree/v0.0.3) (2017-03-15) 167 | [Full Changelog](https://github.com/feathersjs/feathers-vuex/compare/v0.0.2...v0.0.3) 168 | 169 | ## [v0.0.2](https://github.com/feathersjs/feathers-vuex/tree/v0.0.2) (2017-03-15) 170 | 171 | 172 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Feathers-Vuex 2 | 3 | [![Build Status](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex.png?branch=master)](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex) 4 | [![Dependency Status](https://img.shields.io/david/feathersjs-ecosystem/feathers-vuex.svg?style=flat-square)](https://david-dm.org/feathersjs-ecosystem/feathers-vuex) 5 | [![Download Status](https://img.shields.io/npm/dm/feathers-vuex.svg?style=flat-square)](https://www.npmjs.com/package/feathers-vuex) 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/feathersjs-ecosystem/feathers-vuex.svg)](https://greenkeeper.io/) 7 | 8 | `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. 9 | 10 | ![feathers-vuex service logo](./service-logo.png) 11 | 12 | ## Demo & Documentation 13 | 14 | [Demo](https://codesandbox.io/s/xk52mqm7o) 15 | 16 | See [https://vuex.feathersjs.com](https://vuex.feathersjs.com) for full documentation. 17 | 18 | ## Installation 19 | 20 | ```bash 21 | npm install feathers-vuex --save 22 | ``` 23 | 24 | ```bash 25 | yarn add feathers-vuex 26 | ``` 27 | 28 | 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. 29 | 30 | ## Contributing 31 | 32 | 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. 33 | 34 | ## License 35 | 36 | Copyright (c) Forever and Ever, or at least the current year. 37 | 38 | Licensed under the [MIT license](https://github.com/feathersjs-ecosystem/feathers-vuex/blob/master/LICENSE). 39 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'FeathersVuex', 3 | description: 'Integration of FeathersJS, Vue, and Nuxt for the artisan developer', 4 | head: [['link', { rel: 'icon', href: '/favicon.ico' }]], 5 | theme: 'default-prefers-color-scheme', 6 | themeConfig: { 7 | repo: 'feathersjs-ecosystem/feathers-vuex', 8 | docsDir: 'docs', 9 | editLinks: true, 10 | sidebar: [ 11 | '/api-overview.md', 12 | '/3.0-major-release.md', 13 | '/getting-started.md', 14 | '/example-applications.md', 15 | '/vue-plugin.md', 16 | '/service-plugin.md', 17 | '/auth-plugin.md', 18 | '/model-classes.md', 19 | '/common-patterns.md', 20 | '/composition-api.md', 21 | '/mixins.md', 22 | '/data-components.md', 23 | '/feathers-vuex-forms.md', 24 | '/nuxt.md', 25 | '/2.0-major-release.md', 26 | ], 27 | serviceWorker: { 28 | updatePopup: true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-vuex/8643bd7b0cd045fcfd1e407143c3221ae58b7486/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /docs/.vuepress/public/img/devtools.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-vuex/8643bd7b0cd045fcfd1e407143c3221ae58b7486/docs/.vuepress/public/img/devtools.jpg -------------------------------------------------------------------------------- /docs/3.0-major-release.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What's New in 3.0 3 | sidebarDepth: 3 4 | --- 5 | 6 | # What's new in Feathers-Vuex 3.0 7 | 8 | ## Vue Composition API Support 9 | 10 | 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: 11 | 12 | - It is the most powerful feature added to Vue since its first release. 13 | - It improves the ability to create dynamic functionality in components. 14 | - It greatly enhances organization of code in components. 15 | - It encourages code re-use. Check out the [vue-use-web](https://tarektouati.github.io/vue-use-web/) collection for some great examples. 16 | 17 | 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). 18 | 19 | ## New `extend` option for `makeServicePlugin` 20 | 21 | 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: 22 | 23 | ```js 24 | import { makeServicePlugin } from ‘feathers-vuex’ 25 | import { feathersClient } from ‘./feathers-client.js’ 26 | 27 | class Todo { /* truncated */ } 28 | 29 | export default makeServicePlugin({ 30 | Model: Todo, 31 | service: feathersClient.service(‘todos’), 32 | extend({ store, module }) { 33 | // Listen to other parts of the store 34 | store.watch(/* truncated */) 35 | 36 | return { 37 | state: {}, 38 | getters: {}, 39 | mutations: {}, 40 | actions: {} 41 | } 42 | } 43 | }) 44 | ``` 45 | 46 | ## Partial data on patch 47 | 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: 48 | 49 | ```js 50 | import { models } from 'feathers-vuex' 51 | const { Todo } = models.api 52 | 53 | const todo = new Todo({ description: 'Do Something', isComplete: false }) 54 | 55 | todo.patch({ data: { isComplete: true } }) 56 | 57 | // also works for patching with instance.save 58 | todo.save({ data: { isComplete: true } }) 59 | ``` 60 | 61 | ## FeathersVuexPagination Component 62 | 63 | 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). 64 | 65 | ## Custom Handling for Feathers Events 66 | 67 | 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. 68 | 69 | ## Breaking Changes 70 | 71 | Feathers-Vuex follows semantic versioning. There are two breaking changes in this release: 72 | 73 | ### Auth Plugin `user` Not Reactive 74 | 75 | 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. 76 | 77 | - `user`: returns the reactive, logged-in user from the `userService` specified in the options. 78 | - `isAuthenticated`: a easy to remember boolean attribute for if the user is logged in. 79 | 80 | If you depend on a reactive, logged-in user in your apps, here is how to fix the reactivity: 81 | 82 | - Replace any reference to `store.state.auth.user` with `store.getters['auth/user']`. 83 | 84 | 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. 85 | 86 | 87 | ### Server-Side Pagination Support is Off by Default 88 | 89 | 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. 90 | 91 | 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: 92 | 93 | ```js 94 | import { makeFindMixin } from 'feathers-vuex' 95 | 96 | export default { 97 | name: 'MyComponent', 98 | mixins: [ makeFindMixin({ service: 'users', watch: true })], 99 | computed: { 100 | usersParams() { 101 | return { 102 | query: {}, 103 | paginate: true // explicitly enable pagination, now. 104 | } 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | This behavior exactly matches the new `useFind` utility. 111 | 112 | ## Deprecations 113 | 114 | ### The `keepCopiesInStore` Option 115 | 116 | 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. 117 | 118 | 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). 119 | 120 | The `keepCopiesInStore` feature is set to be removed in Feathers-Vuex 4.0. 121 | 122 | ### Auth Plugin State: `user` 123 | 124 | 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. 125 | 126 | ### Renderless Data Components: `query`, `fetchQuery` and `temps` 127 | 128 | 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`. -------------------------------------------------------------------------------- /docs/api-overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API Overview 3 | sidebarDepth: 3 4 | --- 5 | 6 | 7 | [![Build Status](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex.png?branch=master)](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex) 8 | [![Dependency Status](https://img.shields.io/david/feathersjs-ecosystem/feathers-vuex.svg?style=flat-square)](https://david-dm.org/feathersjs-ecosystem/feathers-vuex) 9 | [![Download Status](https://img.shields.io/npm/dm/feathers-vuex.svg?style=flat-square)](https://www.npmjs.com/package/feathers-vuex) 10 | 11 | ![feathers-vuex service logo](https://github.com/feathersjs-ecosystem/feathers-vuex/raw/master/service-logo.png) 12 | 13 | > Integrate the Feathers Client into Vuex 14 | 15 | `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. 16 | 17 | 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). 18 | 19 | ## Features 20 | 21 | - Fully powered by Vuex & Feathers 22 | - Realtime By Default 23 | - Actions With Reactive Data 24 | - Local Queries 25 | - Live Queries 26 | - Feathers Query Syntax 27 | - Vuex Strict Mode Support 28 | - [Client-Side Pagination Support](./service-plugin.md#pagination-and-the-find-getter) 29 | - Fall-Through Caching 30 | - [`$FeathersVuex` Plugin for Vue](./vue-plugin.md) 31 | - [Per-Service Data Modeling](./common-patterns.md#Basic-Data-Modeling-with-instanceDefaults) 32 | - [Clone & Commit](./feathers-vuex-forms.md#the-clone-and-commit-pattern) 33 | - Simplified Auth 34 | - [Per-Record Defaults](./model-classes.md#instancedefaults) 35 | - [Data Level Computed Properties](./2.0-major-release.md#getter-and-setter-props-go-on-the-model-classes) 36 | - [Improved Relation Support](./2.0-major-release.md#define-relationships-and-modify-data-with-setupinstance) 37 | - [Powerful Mixins](./mixins.md) 38 | - [Renderless Data Components](./data-components.md) 39 | - [Renderless Form Component](./feathers-vuex-forms.md#feathersvuexformwrapper) for Simplified Vuex Forms 40 | - [Temporary (Local-only) Record Support](./2.0-major-release.md#support-for-temporary-records) * 41 | - New `useFind` and `useGet` Vue Composition API super powers! 42 | - [Server-Powered Pagination Support](./service-plugin.md#pagination-and-the-find-action) * 43 | - [VuePress Dark Mode Support](https://tolking.github.io/vuepress-theme-default-prefers-color-scheme/) for the Docs 44 | 45 | `** Improved in v3.0.0` 46 | 47 | ## License 48 | 49 | Licensed under the [MIT license](LICENSE). 50 | 51 | Feathers-Vuex is developed and maintained by [Marshall Thompson](https://www.github.com/marshallswain). 52 | 53 | -------------------------------------------------------------------------------- /docs/auth-plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Auth Plugin 3 | --- 4 | 5 | The Auth module assists setting up user login and logout. 6 | 7 | ## Setup 8 | 9 | See the [Auth Setup](/getting-started.html#auth-plugin) section for an example of how to setup the Auth Plugin. 10 | 11 | ## Breaking Changes in 2.0 12 | 13 | The following breaking changes were made between 1.x and 2.0: 14 | 15 | - The `auth` method is now called `makeAuthPlugin`. 16 | 17 | ## Configuration 18 | 19 | You can provide a `userService` in the auth plugin's options to automatically populate the user upon successful login. 20 | 21 | ## State 22 | 23 | It includes the following state by default: 24 | 25 | ```js 26 | { 27 | accessToken: undefined, // The JWT 28 | payload: undefined, // The JWT payload 29 | 30 | userService: null, // Specify the userService to automatically populate the user upon login. 31 | entityIdField: 'userId', // The property in the payload storing the user id 32 | responseEntityField: 'user', // The property in the payload storing the user 33 | user: null, // Deprecated: This is no longer reactive, so use the `user` getter. See below. 34 | 35 | isAuthenticatePending: false, 36 | isLogoutPending: false, 37 | 38 | errorOnAuthenticate: undefined, 39 | errorOnLogout: undefined 40 | } 41 | ``` 42 | 43 | ## Getters 44 | 45 | Two getters are available when a `userService` is provided to the `makeAuthPlugin` options. 46 | 47 | - `user`: returns the reactive, logged-in user from the `userService` specified in the options. Returns `null` if not logged in. 48 | - `isAuthenticated`: a easy to remember boolean attribute for if the user is logged in. 49 | 50 | ## Actions 51 | 52 | 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. 53 | 54 | - `authenticate`: use instead of `feathersClient.authenticate()` 55 | - `logout`: use instead of `feathersClient.logout()` 56 | 57 | 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. 58 | 59 | > Note: The Vuex auth store will not update if you use the feathers client version of the above methods. 60 | 61 | ## Example 62 | 63 | Here's a short example that implements the `authenticate` and `logout` actions. 64 | 65 | ```js 66 | export default { 67 | // ... 68 | methods: { 69 | 70 | login() { 71 | this.$store.dispatch('auth/authenticate', { 72 | email: '...', 73 | password: '...' 74 | }) 75 | } 76 | 77 | // ... 78 | 79 | logout() { 80 | this.$store.dispatch('auth/logout') 81 | } 82 | 83 | } 84 | // ... 85 | } 86 | ``` 87 | 88 | Note that if you customize the auth plugin's `namespace` then the `auth/` prefix in the above example would change to the provided namespace. 89 | -------------------------------------------------------------------------------- /docs/example-applications.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example Applications 3 | sidebarDepth: 3 4 | --- 5 | 6 | # Example Applications 7 | 8 | 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). 9 | 10 | ## Feathers Chat 11 | 12 | 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. 13 | 14 | ![Feathers Chat](https://camo.githubusercontent.com/14b6b2d6dd2475c3b83eb1ade6aedbcd8cf94139/68747470733a2f2f646f63732e66656174686572736a732e636f6d2f6173736574732f696d672f66656174686572732d636861742e39313936303738352e706e67) 15 | 16 | ### Before and After Comparisons 17 | 18 | - 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. 19 | - `/components/Home.vue` is now `/views/Home.vue` 20 | - `/components/Signup.vue` is now `/views/Signup.vue` 21 | - `/components/Login.vue` is now `/views/Login.vue` 22 | - `/components/Chat/Chat.vue` is now `/views/Chat.vue` 23 | - The `/components` folder has been flattened. There are no more subfolders. 24 | - Component refactors: 25 | - [Login.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/eb9ba377c5705c1378bee72661a13dd0db48be05) 26 | - [Signup.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/478710ed84869d33a9286078496c1e5974a95067) 27 | - [Users.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/02b47149c80c27cdeb611c2f4438b4c62159c644) 28 | - [Messages.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/930743c1679cc4ed9d691532a7dff1d6a34398e6) 29 | - [Compuser.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/cd5c8898ede270d5e22f9c6ef1450d3f3c6278c9) 30 | - [Chat.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/39eb3e13f6921b0d0524ae4ac7942b9ce78b222c) 31 | - [Messages.vue](https://github.com/feathersjs-ecosystem/feathers-chat-vuex/commit/e5cf7fb0cc8eab80ee3dc441afafb1399d69059e) 32 | 33 | ### More to Come 34 | 35 | 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: 36 | 37 | 1. Still allows comparison of Feathers Chat applications made with other frameworks. 38 | 2. Allows the version of Feathers Chat built with Feathers-Vuex to add features and showcase things you might actually use in production. 39 | 40 | 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. 41 | 42 | ## Community Examples 43 | 44 | If you have created or know of an example application, please add it, here. 45 | 46 | - [Feathers-Chat-Vuex](https://github.com/feathersjs-ecosystem/feathers-chat-vuex) 47 | -------------------------------------------------------------------------------- /docs/feathervuex-in-vuejs3-setup.md: -------------------------------------------------------------------------------- 1 | # Using Vuejs 3 setup() 2 | 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. 3 | 4 | 5 | ## Setup() method 6 | The context is no longer passed into the setup() as a parameter: 7 | 8 | ``` 9 | setup(props, context) { 10 | // old way 11 | const { User } = root.$FeathersVuex.api 12 | } 13 | ``` 14 | 15 | We now must `inject` it into setup(): 16 | 17 | ``` 18 | export default defineComponent({ 19 | import { inject } from 'vue'; 20 | 21 | setup() { 22 | // both $FeatherVuex and $fv work here 23 | const models: any = inject('$FeathersVuex') 24 | const newUser = new models.api.User() 25 | 26 | return { 27 | newUser 28 | } 29 | } 30 | }) 31 | ``` 32 | 33 | 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). 34 | 35 | **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) 36 | 37 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: https://github.com/feathersjs-ecosystem/feathers-vuex/raw/master/service-logo.png 4 | heroText: Feathers-Vuex 3.x 5 | tagLine: Integration of FeathersJS, Vue, and Nuxt for the artisan developer 6 | actionText: Get Started 7 | actionLink: ./api-overview.md 8 | features: 9 | - title: Realtime by Default 10 | details: It's fully powered by Vuex and FeathersJS, lightweight, & realtime out of the box. 11 | - title: Simplified Auth & Services 12 | details: Includes service and auth plugins powered by Vuex. All plugins can be easily customized to fit your app. Fully flexible. 13 | - title: Best Practices, Baked In 14 | details: Vue Composition API 😎 Common Redux patterns included. Fall-through cache by default. Query the Vuex store like a database. 15 | footer: MIT Licensed | Copyright © 2017-present Marshall Thompson 16 | --- 17 | -------------------------------------------------------------------------------- /docs/vue-plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vue Plugin 3 | --- 4 | 5 | # The Vue Plugin 6 | 7 | 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). 8 | 9 | ```js 10 | // src/store/store.js 11 | import Vue from 'vue' 12 | import Vuex from 'vuex' 13 | import { FeathersVuex } from '../feathers-client' 14 | import auth from './store.auth' 15 | 16 | Vue.use(Vuex) 17 | Vue.use(FeathersVuex) 18 | 19 | const requireModule = require.context( 20 | // The path where the service modules live 21 | './services', 22 | // Whether to look in subfolders 23 | false, 24 | // Only include .js files (prevents duplicate imports`) 25 | /.js$/ 26 | ) 27 | const servicePlugins = requireModule 28 | .keys() 29 | .map(modulePath => requireModule(modulePath).default) 30 | 31 | export default new Vuex.Store({ 32 | state: {}, 33 | mutations: {}, 34 | actions: {}, 35 | plugins: [...servicePlugins, auth] 36 | }) 37 | ``` 38 | 39 | ## Using the Vue Plugin 40 | 41 | 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: 42 | 43 | ```js 44 | // 1.x way 45 | new this.$FeathersVuex.User({}) 46 | 47 | // 2.x way 48 | new this.$FeathersVuex.api.User({}) // Assuming default serverAlias of `api`. 49 | new this.$FeathersVuex.myApi.user({}) // If you customized the serverAlias to be `myApi`. 50 | ``` 51 | 52 | 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: 53 | 54 | | Service Name | Model Name in `$FeathersVuex` | 55 | | ------------------------- | ----------------------------- | 56 | | /cart | Cart | 57 | | /todos | Todo | 58 | | /v1/districts | District | 59 | | /some/deeply/nested/items | Item | 60 | 61 | The `$FeathersVuex` object is available on the Vue object, directly at `Vue.$FeathersVuex`, as well as on the prototype, making it available in components: 62 | 63 | ```js 64 | // In your Vue component 65 | created () { 66 | const todo = new this.$FeathersVuex.Todo({ description: 'Do something!' }) 67 | // `todo` is now a model instance 68 | } 69 | ``` 70 | 71 | ## New in 2.0 72 | 73 | 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: 74 | 75 | ```js 76 | import { models } from 'feathers-vuex' 77 | 78 | const user = new models.api.User({ 79 | email: 'test@test.com' 80 | }) 81 | ``` 82 | 83 | ## Included Components 84 | 85 | When you register the Vue Plugin, a few components are automatically globally registered: 86 | 87 | - The [Renderless Data components](/data-components.html) 88 | - The [`FeathersVuexFormWrapper` component](/feathers-vuex-forms.html#feathersvuexformwrapper) 89 | - The [`FeathersVuexInputWrapper` component](/feathers-vuex-forms.html#feathersvuexinputwrapper) 90 | - The [`FeathersVuexPagination` component](/composition-api.html#feathersvuexpagination) 91 | 92 | You can pass `components: false` in the options to not globally register the component: 93 | 94 | ```js 95 | Vue.use(FeathersVuex, { components: false }) 96 | ``` -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-core/register 2 | test/node.test.js -------------------------------------------------------------------------------- /notes.old.md: -------------------------------------------------------------------------------- 1 | ## Extending the built-in Model classes 2 | 3 | If you desire to extend the built-in Models 4 | 5 | **store/index.js:** 6 | ```js 7 | import Vue from 'vue' 8 | import Vuex from 'vuex' 9 | import feathersVuex from 'feathers-vuex' 10 | import feathersClient from '../feathers-client' 11 | 12 | const { service, auth, FeathersVuex } = feathersVuex(feathersClient, { idField: '_id' }) 13 | const { serviceModule, serviceModel, servicePlugin } = service 14 | 15 | const api1Client = feathersVuex(feathersClient, { idField: '_id', apiPrefix: 'api1' }) 16 | const api2Client = feathersVuex(feathersClient2, { idField: '_id' }) 17 | 18 | Vue.use(FeathersVuex) 19 | 20 | const todoModule = serviceModule('todos') 21 | 22 | // const Model = serviceModel(todoModule) // TodoModel is an extensible class 23 | const Model = serviceModel() 24 | class TodoModel = extends Model {} 25 | const todoPlugin = servicePlugin(todoModule, TodoModel) 26 | 27 | const TaskModel extends Model {} 28 | 29 | export { TaskModel } 30 | 31 | 32 | created () { 33 | this.todo = new this.$FeathersVuex.api1.Todo(data) 34 | } 35 | 36 | Vue.use(Vuex) 37 | Vue.use(FeathersVuex) 38 | 39 | export default new Vuex.Store({ 40 | plugins: [ 41 | servicePlugin('/tasks', TaskModel), // With our potentially customized TodoModel 42 | 43 | service('todos'), 44 | 45 | // Specify custom options per service 46 | service('/v1/tasks', { 47 | idField: '_id', // The field in each record that will contain the id 48 | nameStyle: 'path', // Use the full service path as the Vuex module name, instead of just the last section 49 | namespace: 'custom-namespace', // Customize the Vuex module name. Overrides nameStyle. 50 | autoRemove: true, // Automatically remove records missing from responses (only use with feathers-rest) 51 | enableEvents: false, // Turn off socket event listeners. It's true by default 52 | addOnUpsert: true, // Add new records pushed by 'updated/patched' socketio events into store, instead of discarding them. It's false by default 53 | skipRequestIfExists: true, // For get action, if the record already exists in store, skip the remote request. It's false by default 54 | modelName: 'Task' 55 | }) 56 | 57 | // Add custom state, getters, mutations, or actions, if needed. See example in another section, below. 58 | service('things', { 59 | state: {}, 60 | getters: {}, 61 | mutations: {}, 62 | actions: {} 63 | }) 64 | 65 | auth() 66 | ] 67 | }) 68 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-vuex", 3 | "description": "FeathersJS, Vue, and Nuxt for the artisan developer", 4 | "version": "3.16.0", 5 | "homepage": "https:feathers-vuex.feathersjs-ecosystem.com", 6 | "main": "dist/", 7 | "module": "dist/", 8 | "types": "dist/", 9 | "keywords": [ 10 | "vue", 11 | "feathers", 12 | "feathers-plugin" 13 | ], 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/feathersjs-ecosystem/feathers-vuex.git" 18 | }, 19 | "author": { 20 | "name": "Marshall Thompson", 21 | "email": "marshall@creativeideal.net", 22 | "url": "https://github.com/marshallswain" 23 | }, 24 | "funding": { 25 | "type": "Github sponsor", 26 | "url": "https://github.com/sponsors/marshallswain" 27 | }, 28 | "contributors": [], 29 | "bugs": { 30 | "url": "https://github.com/feathersjs-ecosystem/feathers-vuex/issues" 31 | }, 32 | "engines": { 33 | "node": ">= 4.6.0" 34 | }, 35 | "scripts": { 36 | "prepublish": "npm run compile", 37 | "publish": "git push origin --tags && git push origin", 38 | "release:pre": "npm version prerelease && npm publish --tag pre", 39 | "release:patch": "npm version patch && npm publish", 40 | "release:minor": "npm version minor && npm publish", 41 | "release:major": "npm version major && npm publish", 42 | "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 43 | "compile": "shx rm -rf lib/ && tsc && npm run lint-dist", 44 | "lint-dist": "prettier --write \"dist/**/*.js\"", 45 | "watch": "shx rm -rf lib/ && babel --watch -d lib/ src/", 46 | "lint": "standard --fix", 47 | "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --opts mocha.opts", 48 | "test": "cross-env TS_NODE_PROJECT='tsconfig.test.json' mocha --require ts-node/register 'test/**/*.test.ts'", 49 | "testee": "testee test/index.html --browsers firefox", 50 | "start": "npm run compile && node example/app", 51 | "docs": "vuepress dev docs", 52 | "docs:build": "vuepress build docs" 53 | }, 54 | "prettier": { 55 | "singleQuote": true, 56 | "semi": false, 57 | "trailingComma": "none", 58 | "tabWidth": 2 59 | }, 60 | "eslintConfig": { 61 | "root": true, 62 | "env": { 63 | "node": true, 64 | "mocha": true 65 | }, 66 | "extends": [ 67 | "plugin:@typescript-eslint/recommended", 68 | "prettier/@typescript-eslint", 69 | "plugin:prettier/recommended" 70 | ], 71 | "rules": { 72 | "linebreak-style": [ 73 | "warn", 74 | "unix" 75 | ], 76 | "prettier/prettier": [ 77 | "warn", 78 | { 79 | "fix": true, 80 | "singleQuote": true, 81 | "semi": false, 82 | "trailingComma": "none", 83 | "arrowParens": "avoid" 84 | } 85 | ] 86 | }, 87 | "parserOptions": { 88 | "parser": "@typescript-eslint/parser", 89 | "ecmaVersion": 2018, 90 | "sourceType": "module" 91 | } 92 | }, 93 | "steal": { 94 | "map": { 95 | "assert": "chai/chai" 96 | }, 97 | "meta": { 98 | "chai/chai": { 99 | "format": "global", 100 | "exports": "chai.assert" 101 | } 102 | }, 103 | "plugins": [ 104 | "chai" 105 | ] 106 | }, 107 | "directories": { 108 | "lib": "lib" 109 | }, 110 | "peerDependencies": { 111 | "@vue/composition-api": "*" 112 | }, 113 | "dependencies": { 114 | "@feathersjs/adapter-commons": "^4.5.2", 115 | "@feathersjs/commons": "^4.5.3", 116 | "@feathersjs/errors": "^4.5.3", 117 | "@types/feathersjs__feathers": "^3.1.5", 118 | "@types/inflection": "^1.5.28", 119 | "@types/lodash": "^4.14.150", 120 | "@types/npm": "^2.0.31", 121 | "bson-objectid": "^1.3.0", 122 | "debug": "^4.1.1", 123 | "events": "^3.1.0", 124 | "fast-copy": "^2.1.0", 125 | "fast-json-stable-stringify": "^2.1.0", 126 | "inflection": "^1.12.0", 127 | "jwt-decode": "^2.2.0", 128 | "lodash": "^4.17.15", 129 | "lodash.isobject": "^3.0.2", 130 | "lodash.isplainobject": "^4.0.6", 131 | "lodash.merge": "^4.6.2", 132 | "lodash.omit": "^4.5.0", 133 | "lodash.pick": "^4.4.0", 134 | "lodash.trim": "^4.5.1", 135 | "serialize-error": "^5.0.0", 136 | "sift": "^9.0.4" 137 | }, 138 | "devDependencies": { 139 | "@feathersjs/authentication-client": "^4.5.4", 140 | "@feathersjs/authentication-jwt": "^2.0.10", 141 | "@feathersjs/client": "^4.5.4", 142 | "@feathersjs/feathers": "^4.5.3", 143 | "@feathersjs/rest-client": "^4.5.4", 144 | "@feathersjs/socketio-client": "^4.5.4", 145 | "@types/chai": "^4.2.11", 146 | "@types/mocha": "^7.0.2", 147 | "@typescript-eslint/eslint-plugin": "^2.31.0", 148 | "@typescript-eslint/parser": "^2.31.0", 149 | "@vue/composition-api": "^1.2.4", 150 | "@vue/eslint-config-prettier": "^6.0.0", 151 | "@vue/eslint-config-typescript": "^5.0.2", 152 | "@vue/test-utils": "^1.0.2", 153 | "axios": "^0.21.1", 154 | "babel-cli": "^6.26.0", 155 | "babel-core": "^6.26.3", 156 | "babel-eslint": "^10.1.0", 157 | "babel-plugin-add-module-exports": "^1.0.2", 158 | "babel-preset-es2015": "^6.24.1", 159 | "babel-preset-stage-2": "^6.24.1", 160 | "body-parser": "^1.19.0", 161 | "can-fixture-socket": "^2.0.3", 162 | "chai": "^4.2.0", 163 | "cross-env": "^7.0.2", 164 | "date-fns": "^2.13.0", 165 | "deep-object-diff": "^1.1.0", 166 | "eslint": "^6.8.0", 167 | "eslint-config-prettier": "^6.11.0", 168 | "eslint-plugin-prettier": "^3.1.3", 169 | "eslint-plugin-vue": "^6.2.2", 170 | "feathers-memory": "^4.1.0", 171 | "istanbul": "^1.1.0-alpha.1", 172 | "jsdom": "^16.2.2", 173 | "jsdom-global": "^3.0.2", 174 | "mocha": "^7.1.2", 175 | "omit-deep-lodash": "^1.1.4", 176 | "prettier": "^2.0.5", 177 | "shx": "^0.3.2", 178 | "socket.io-client": "^2.3.0", 179 | "standard": "^14.3.3", 180 | "steal": "^2.2.4", 181 | "steal-mocha": "^2.0.1", 182 | "steal-typescript": "^0.5.0", 183 | "testee": "^0.9.1", 184 | "ts-node": "^8.10.1", 185 | "typescript": "^3.8.3", 186 | "vue": "^2.6.11", 187 | "vue-server-renderer": "^2.6.11", 188 | "vue-template-compiler": "^2.6.11", 189 | "vuepress": "^1.4.1", 190 | "vuepress-theme-default-prefers-color-scheme": "^1.0.7", 191 | "vuex": "^3.3.0" 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /service-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-vuex/8643bd7b0cd045fcfd1e407143c3221ae58b7486/service-logo.png -------------------------------------------------------------------------------- /src/FeathersVuexCount.ts: -------------------------------------------------------------------------------- 1 | import { randomString } from './utils' 2 | 3 | export default { 4 | props: { 5 | service: { 6 | type: String, 7 | required: true 8 | }, 9 | params: { 10 | type: Object, 11 | default: () => { 12 | return { 13 | query: {} 14 | } 15 | } 16 | }, 17 | queryWhen: { 18 | type: [Boolean, Function], 19 | default: true 20 | }, 21 | // If separate params are desired to fetch data, use fetchParams 22 | // The watchers will automatically be updated, so you don't have to write 'fetchParams.query.propName' 23 | fetchParams: { 24 | type: Object 25 | }, 26 | watch: { 27 | type: [String, Array], 28 | default: () => [] 29 | }, 30 | local: { 31 | type: Boolean, 32 | default: false 33 | } 34 | }, 35 | data: () => ({ 36 | isCountPending: false, 37 | serverTotal: null 38 | }), 39 | computed: { 40 | total() { 41 | if (!this.local) { 42 | return this.serverTotal 43 | } else { 44 | const { params, service, $store, temps } = this 45 | return params ? $store.getters[`${service}/count`](params) : 0 46 | } 47 | }, 48 | scope() { 49 | const { total, isCountPending } = this 50 | 51 | return { total, isCountPending } 52 | } 53 | }, 54 | methods: { 55 | findData() { 56 | const params = this.fetchParams || this.params 57 | 58 | if ( 59 | typeof this.queryWhen === 'function' 60 | ? this.queryWhen(this.params) 61 | : this.queryWhen 62 | ) { 63 | this.isCountPending = true 64 | 65 | if (params) { 66 | return this.$store 67 | .dispatch(`${this.service}/count`, params) 68 | .then(response => { 69 | this.isCountPending = false 70 | this.serverTotal = response 71 | }) 72 | } 73 | } 74 | }, 75 | fetchData() { 76 | if (!this.local) { 77 | if (this.params) { 78 | return this.findData() 79 | } else { 80 | // TODO: access debug boolean from the store config, somehow. 81 | // eslint-disable-next-line no-console 82 | console.log( 83 | `No query and no id provided, so no data will be fetched.` 84 | ) 85 | } 86 | } 87 | } 88 | }, 89 | created() { 90 | if (!this.$FeathersVuex) { 91 | throw new Error( 92 | `You must first Vue.use the FeathersVuex plugin before using the 'FeathersVuexFind' component.` 93 | ) 94 | } 95 | if (!this.$store.state[this.service]) { 96 | throw new Error( 97 | `The '${this.service}' plugin not registered with feathers-vuex` 98 | ) 99 | } 100 | 101 | const watch = Array.isArray(this.watch) ? this.watch : [this.watch] 102 | 103 | if (this.fetchParams || this.params) { 104 | watch.forEach(prop => { 105 | if (typeof prop !== 'string') { 106 | throw new Error(`Values in the 'watch' array must be strings.`) 107 | } 108 | if (this.fetchParams) { 109 | if (prop.startsWith('params')) { 110 | prop = prop.replace('params', 'fetchParams') 111 | } 112 | } 113 | this.$watch(prop, this.fetchData) 114 | }) 115 | 116 | this.fetchData() 117 | } 118 | }, 119 | render() { 120 | return this.$scopedSlots.default(this.scope) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/FeathersVuexFind.ts: -------------------------------------------------------------------------------- 1 | import { randomString, getQueryInfo } from './utils' 2 | import _get from 'lodash/get' 3 | 4 | export default { 5 | props: { 6 | service: { 7 | type: String, 8 | required: true 9 | }, 10 | query: { 11 | type: Object, 12 | default: null 13 | }, 14 | queryWhen: { 15 | type: [Boolean, Function], 16 | default: true 17 | }, 18 | // If a separate query is desired to fetch data, use fetchQuery 19 | // The watchers will automatically be updated, so you don't have to write 'fetchQuery.propName' 20 | fetchQuery: { 21 | type: Object 22 | }, 23 | /** 24 | * Can be used in place of the `query` prop to provide more params. Only params.query is 25 | * passed to the getter. 26 | */ 27 | params: { 28 | type: Object, 29 | default: null 30 | }, 31 | /** 32 | * Can be used in place of the `fetchQuery` prop to provide more params. Only params.query is 33 | * passed to the getter. 34 | */ 35 | fetchParams: { 36 | type: Object, 37 | default: null 38 | }, 39 | watch: { 40 | type: [String, Array], 41 | default() { 42 | return [] 43 | } 44 | }, 45 | local: { 46 | type: Boolean, 47 | default: false 48 | }, 49 | editScope: { 50 | type: Function, 51 | default(scope) { 52 | return scope 53 | } 54 | }, 55 | qid: { 56 | type: String, 57 | default() { 58 | return randomString(10) 59 | } 60 | }, 61 | /** 62 | * Set `temps` to true to include temporary records from the store. 63 | */ 64 | temps: { 65 | type: Boolean, 66 | default: false 67 | } 68 | }, 69 | data: () => ({ 70 | isFindPending: false, 71 | queryId: null, 72 | pageId: null 73 | }), 74 | computed: { 75 | items() { 76 | let { query, service, $store, temps } = this 77 | let { params } = this 78 | 79 | query = query || {} 80 | 81 | params = params || { query, temps } 82 | 83 | return $store.getters[`${service}/find`](params).data 84 | }, 85 | pagination() { 86 | return this.$store.state[this.service].pagination[this.qid] 87 | }, 88 | queryInfo() { 89 | if (this.pagination == null || this.queryId == null) return {} 90 | return _get(this.pagination, this.queryId, {}) 91 | }, 92 | pageInfo() { 93 | if ( 94 | this.pagination == null || 95 | this.queryId == null || 96 | this.pageId == null 97 | ) 98 | return {} 99 | return _get(this.pagination, [this.queryId, this.pageId], {}) 100 | }, 101 | scope() { 102 | const { items, isFindPending, pagination, queryInfo, pageInfo } = this 103 | const defaultScope = { 104 | isFindPending, 105 | pagination, 106 | items, 107 | queryInfo, 108 | pageInfo 109 | } 110 | 111 | return this.editScope(defaultScope) || defaultScope 112 | } 113 | }, 114 | methods: { 115 | findData() { 116 | const query = this.fetchQuery || this.query 117 | let params = this.fetchParams || this.params 118 | 119 | if ( 120 | typeof this.queryWhen === 'function' 121 | ? this.queryWhen(this.params || this.query) 122 | : this.queryWhen 123 | ) { 124 | this.isFindPending = true 125 | 126 | if (params || query) { 127 | if (params) { 128 | params = Object.assign({}, params, { qid: this.qid || 'default' }) 129 | } else { 130 | params = { query, qid: this.qid || 'default' } 131 | } 132 | 133 | return this.$store 134 | .dispatch(`${this.service}/find`, params) 135 | .then(response => { 136 | this.isFindPending = false 137 | const { queryId, pageId } = getQueryInfo(params, response) 138 | this.queryId = queryId 139 | this.pageId = pageId 140 | }) 141 | } 142 | } 143 | }, 144 | fetchData() { 145 | if (!this.local) { 146 | if (this.params || this.query) { 147 | return this.findData() 148 | } else { 149 | // TODO: access debug boolean from the store config, somehow. 150 | // eslint-disable-next-line no-console 151 | console.log( 152 | `No query and no id provided, so no data will be fetched.` 153 | ) 154 | } 155 | } 156 | } 157 | }, 158 | created() { 159 | if (!this.$FeathersVuex) { 160 | throw new Error( 161 | `You must first Vue.use the FeathersVuex plugin before using the 'FeathersVuexFind' component.` 162 | ) 163 | } 164 | if (!this.$store.state[this.service]) { 165 | throw new Error( 166 | `The '${this.service}' plugin not registered with feathers-vuex` 167 | ) 168 | } 169 | 170 | const watch = Array.isArray(this.watch) ? this.watch : [this.watch] 171 | 172 | if (this.fetchQuery || this.query || this.params) { 173 | watch.forEach(prop => { 174 | if (typeof prop !== 'string') { 175 | throw new Error(`Values in the 'watch' array must be strings.`) 176 | } 177 | if (this.fetchQuery) { 178 | if (prop.startsWith('query')) { 179 | prop = prop.replace('query', 'fetchQuery') 180 | } 181 | } 182 | if (this.fetchParams) { 183 | if (prop.startsWith('params')) { 184 | prop = prop.replace('params', 'fetchParams') 185 | } 186 | } 187 | this.$watch(prop, this.fetchData) 188 | }) 189 | 190 | this.fetchData() 191 | } 192 | }, 193 | render() { 194 | return this.$scopedSlots.default(this.scope) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/FeathersVuexFormWrapper.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'FeathersVuexFormWrapper', 3 | model: { 4 | prop: 'item', 5 | event: 'update:item' 6 | }, 7 | props: { 8 | item: { 9 | type: Object, 10 | required: true 11 | }, 12 | /** 13 | * By default, when you call the `save` method, the cloned data will be 14 | * committed to the store BEFORE saving tot he API server. Set 15 | * `:eager="false"` to only update the store with the API server response. 16 | */ 17 | eager: { 18 | type: Boolean, 19 | default: true 20 | }, 21 | // Set to false to prevent re-cloning if the object updates. 22 | watch: { 23 | type: Boolean, 24 | default: true 25 | } 26 | }, 27 | data: () => ({ 28 | clone: null, 29 | isDirty: false 30 | }), 31 | computed: { 32 | isNew() { 33 | return (this.item && this.item.__isTemp) || false 34 | } 35 | }, 36 | watch: { 37 | item: { 38 | handler: 'setup', 39 | immediate: true, 40 | deep: true 41 | } 42 | }, 43 | methods: { 44 | setup() { 45 | if (this.item) { 46 | this.isDirty = false 47 | // Unwatch the clone to prevent running watchers during reclone 48 | if (this.unwatchClone) { 49 | this.unwatchClone() 50 | } 51 | 52 | this.clone = this.item.clone() 53 | 54 | // Watch the new clone. 55 | this.unwatchClone = this.$watch('clone', { 56 | handler: 'markAsDirty', 57 | deep: true 58 | }) 59 | } 60 | }, 61 | save(params) { 62 | if (this.eager) { 63 | this.clone.commit() 64 | } 65 | return this.clone.save(params).then(response => { 66 | this.$emit('saved', response) 67 | if (this.isNew) { 68 | this.$emit('saved-new', response) 69 | } 70 | return response 71 | }) 72 | }, 73 | reset() { 74 | this.clone.reset() 75 | this.isDirty = false 76 | this.$emit('reset', this.item) 77 | }, 78 | async remove() { 79 | await this.item.remove() 80 | this.$emit('removed', this.item) 81 | return this.item 82 | }, 83 | markAsDirty() { 84 | if (!this.isDirty) { 85 | this.isDirty = true 86 | } 87 | } 88 | }, 89 | render() { 90 | const { clone, save, reset, remove, isDirty, isNew } = this 91 | return this.$scopedSlots.default({ 92 | clone, 93 | save, 94 | reset, 95 | remove, 96 | isDirty, 97 | isNew 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/FeathersVuexGet.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | export default { 3 | props: { 4 | /** 5 | * The path of the service from which to pull records. 6 | */ 7 | service: { 8 | type: String, 9 | required: true 10 | }, 11 | /** 12 | * Must match the `serverAlias` that was provided in the service's configuration. 13 | */ 14 | serverAlias: { 15 | type: String, 16 | default: 'api' 17 | }, 18 | /** 19 | * By default, `query` is used to get data from the Vuex store AND the API request. 20 | * If you specify a `fetchQuery`, then `query` will only be used for the Vuex store. 21 | */ 22 | query: { 23 | type: Object, 24 | default: null 25 | }, 26 | /** 27 | * If a separate query is desired to fetch data, use fetchQuery 28 | * The watchers are automatically updated, so you don't have to write 'fetchQuery.propName' 29 | */ 30 | fetchQuery: { 31 | type: Object 32 | }, 33 | /** 34 | * Can be used in place of the `query` prop to provide more params. Only params.query is 35 | * passed to the getter. 36 | */ 37 | params: { 38 | type: Object, 39 | default: null 40 | }, 41 | /** 42 | * Can be used in place of the `fetchQuery` prop to provide more params. Only params.query is 43 | * passed to the getter. 44 | */ 45 | fetchParams: { 46 | type: Object, 47 | default: null 48 | }, 49 | /** 50 | * When `queryWhen` evaluates to false, no API request will be made. 51 | */ 52 | queryWhen: { 53 | type: [Boolean, Function], 54 | default: true 55 | }, 56 | // For get requests 57 | id: { 58 | type: [Number, String], 59 | default: null 60 | }, 61 | /** 62 | * Specify which properties in the query to watch and re-trigger API requests. 63 | */ 64 | watch: { 65 | type: [String, Array], 66 | default() { 67 | return [] 68 | } 69 | }, 70 | /** 71 | * Set `local` to true to only requests from the Vuex data store and not make API requests. 72 | */ 73 | local: { 74 | type: Boolean, 75 | default: false 76 | }, 77 | /** 78 | * This function is called by the getter and allows you to intercept the `item` in the 79 | * response to pass it into the parent component's scope. It's a dirty little cheater 80 | * function (because it's called from a getter), but it actually works well ;) 81 | */ 82 | editScope: { 83 | type: Function, 84 | default(scope) { 85 | return scope 86 | } 87 | } 88 | }, 89 | data: () => ({ 90 | isFindPending: false, 91 | isGetPending: false 92 | }), 93 | computed: { 94 | item() { 95 | const getArgs = this.getArgs(this.query) 96 | if (this.id) { 97 | if (getArgs.length === 1) { 98 | return this.$store.getters[`${this.service}/get`](this.id) || null 99 | } else { 100 | const args = [this.id] 101 | const query = getArgs[1].query 102 | if (query) { 103 | args.push(query) 104 | } 105 | return this.$store.getters[`${this.service}/get`](args) || null 106 | } 107 | } else { 108 | return null 109 | } 110 | }, 111 | scope() { 112 | const { item, isGetPending } = this 113 | const defaultScope = { item, isGetPending } 114 | 115 | return this.editScope(defaultScope) || defaultScope 116 | } 117 | }, 118 | methods: { 119 | getArgs(queryToUse) { 120 | const query = queryToUse || this.fetchQuery || this.query 121 | const params = this.fetchParams || this.params 122 | 123 | const getArgs = [this.id] 124 | if (params) { 125 | getArgs.push(params) 126 | } else if (query && Object.keys(query).length > 0) { 127 | getArgs.push({ query }) 128 | } 129 | return getArgs 130 | }, 131 | getData() { 132 | const getArgs = this.getArgs() 133 | 134 | if ( 135 | typeof this.queryWhen === 'function' 136 | ? this.queryWhen(...getArgs) 137 | : this.queryWhen 138 | ) { 139 | this.isGetPending = true 140 | 141 | if (this.id) { 142 | return this.$store 143 | .dispatch( 144 | `${this.service}/get`, 145 | getArgs.length === 1 ? this.id : getArgs 146 | ) 147 | .then(response => { 148 | this.isGetPending = false 149 | return response 150 | }) 151 | } 152 | } 153 | }, 154 | fetchData() { 155 | if (this.local || this.id === 'new') { 156 | return 157 | } else if ( 158 | this.fetchQuery || 159 | this.query || 160 | this.params || 161 | (this.id !== null && this.id !== undefined) 162 | ) { 163 | return this.getData() 164 | } else { 165 | // eslint-disable-next-line no-console 166 | console.log(`No query and no id provided, so no data will be fetched.`) 167 | } 168 | } 169 | }, 170 | created() { 171 | if (!this.$FeathersVuex) { 172 | throw new Error( 173 | `You must first Vue.use the FeathersVuex plugin before using the 'FeathersVuexGet' component.` 174 | ) 175 | } 176 | if (!this.$store.state[this.service]) { 177 | throw new Error( 178 | `The '${this.service}' plugin is not registered with feathers-vuex` 179 | ) 180 | } 181 | 182 | const watch = Array.isArray(this.watch) ? this.watch : [this.watch] 183 | 184 | if ( 185 | this.fetchQuery || 186 | this.query || 187 | this.params || 188 | (this.id !== null && this.id !== undefined) 189 | ) { 190 | watch.forEach(prop => { 191 | if (typeof prop !== 'string') { 192 | throw new Error(`Values in the 'watch' array must be strings.`) 193 | } 194 | if (this.fetchQuery) { 195 | if (prop.startsWith('query')) { 196 | prop.replace('query', 'fetchQuery') 197 | } 198 | } 199 | this.$watch(prop, this.fetchData) 200 | }) 201 | 202 | this.fetchData() 203 | } 204 | }, 205 | render() { 206 | return this.$scopedSlots.default(this.scope) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/FeathersVuexInputWrapper.ts: -------------------------------------------------------------------------------- 1 | import _debounce from 'lodash/debounce' 2 | 3 | export default { 4 | name: 'FeathersVuexInputWrapper', 5 | props: { 6 | item: { 7 | type: Object, 8 | required: true 9 | }, 10 | prop: { 11 | type: String, 12 | required: true 13 | }, 14 | debounce: { 15 | type: Number, 16 | default: 0 17 | } 18 | }, 19 | data: () => ({ 20 | clone: null 21 | }), 22 | computed: { 23 | current() { 24 | return this.clone || this.item 25 | } 26 | }, 27 | watch: { 28 | debounce: { 29 | handler(wait) { 30 | this.debouncedHandler = _debounce(this.handler, wait) 31 | }, 32 | immediate: true 33 | } 34 | }, 35 | methods: { 36 | createClone(e) { 37 | this.clone = this.item.clone() 38 | }, 39 | cleanup() { 40 | this.$nextTick(() => { 41 | this.clone = null 42 | }) 43 | }, 44 | handler(e, callback) { 45 | if (!this.clone) { 46 | this.createClone() 47 | } 48 | const maybePromise = callback({ 49 | event: e, 50 | clone: this.clone, 51 | prop: this.prop, 52 | data: { [this.prop]: this.clone[this.prop] } 53 | }) 54 | if (maybePromise && maybePromise.then) { 55 | maybePromise.then(this.cleanup) 56 | } else { 57 | this.cleanup() 58 | } 59 | } 60 | }, 61 | render() { 62 | const { current, prop, createClone } = this 63 | const handler = this.debounce ? this.debouncedHandler : this.handler 64 | 65 | return this.$scopedSlots.default({ current, prop, createClone, handler }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/FeathersVuexPagination.ts: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | computed, 4 | watch 5 | } from '@vue/composition-api' 6 | 7 | export default { 8 | name: 'FeathersVuexPagination', 9 | props: { 10 | /** 11 | * An object containing { $limit, and $skip } 12 | */ 13 | value: { 14 | type: Object, 15 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 16 | default: () => null 17 | }, 18 | /** 19 | * The `latestQuery` object from the useFind data 20 | */ 21 | latestQuery: { 22 | type: Object, 23 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 24 | default: () => null 25 | } 26 | }, 27 | // eslint-disable-next-line 28 | setup(props, context) { 29 | /** 30 | * The number of pages available based on the results returned in the latestQuery prop. 31 | */ 32 | const pageCount = computed(() => { 33 | const q = props.latestQuery 34 | if (q && q.response) { 35 | return Math.ceil(q.response.total / props.value.$limit) 36 | } else { 37 | return 1 38 | } 39 | }) 40 | 41 | /** 42 | * The `currentPage` is calculated based on the $limit and $skip values provided in 43 | * the v-model object. 44 | * 45 | * Setting `currentPage` to a new numeric value will emit the appropriate values out 46 | * the v-model. (using the default `input` event) 47 | */ 48 | const currentPage = computed({ 49 | set(pageNumber: number) { 50 | if (pageNumber < 1) { 51 | pageNumber = 1 52 | } else if (pageNumber > pageCount.value) { 53 | pageNumber = pageCount.value 54 | } 55 | const $limit = props.value.$limit 56 | const $skip = $limit * (pageNumber - 1) 57 | 58 | context.emit('input', { $limit, $skip }) 59 | }, 60 | get() { 61 | const params = props.value 62 | if (params) { 63 | return pageCount.value === 0 ? 0 : params.$skip / params.$limit + 1 64 | } else { 65 | return 1 66 | } 67 | } 68 | }) 69 | 70 | watch( 71 | () => pageCount.value, 72 | () => { 73 | const lq = props.latestQuery 74 | if (lq && lq.response && currentPage.value > pageCount.value) { 75 | currentPage.value = pageCount.value 76 | } 77 | } 78 | ) 79 | 80 | const canPrev = computed(() => { 81 | return currentPage.value - 1 > 0 82 | }) 83 | const canNext = computed(() => { 84 | return currentPage.value < pageCount.value 85 | }) 86 | 87 | function toStart(): void { 88 | currentPage.value = 1 89 | } 90 | function toEnd(): void { 91 | currentPage.value = pageCount.value 92 | } 93 | function toPage(pageNumber): void { 94 | currentPage.value = pageNumber 95 | } 96 | 97 | function next(): void { 98 | currentPage.value++ 99 | } 100 | function prev(): void { 101 | currentPage.value-- 102 | } 103 | 104 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 105 | return () => { 106 | if (context.slots.default) { 107 | return context.slots.default({ 108 | currentPage: currentPage.value, 109 | pageCount: pageCount.value, 110 | canPrev: canPrev.value, 111 | canNext: canNext.value, 112 | toStart, 113 | toEnd, 114 | toPage, 115 | prev, 116 | next 117 | }) 118 | } else { 119 | return h('div', {}, [ 120 | h('p', `FeathersVuexPagination uses the default slot:`), 121 | h('p', `#default="{ currentPage, pageCount }"`) 122 | ]) 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/auth-module/auth-module.actions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import fastCopy from 'fast-copy' 7 | import { globalModels as models } from '../service-module/global-models' 8 | import { getNameFromPath } from '../utils' 9 | 10 | export default function makeAuthActions(feathersClient) { 11 | return { 12 | authenticate(store, dataOrArray) { 13 | const { commit, state, dispatch } = store 14 | const [data, params] = Array.isArray(dataOrArray) 15 | ? dataOrArray 16 | : [dataOrArray] 17 | 18 | commit('setAuthenticatePending') 19 | if (state.errorOnAuthenticate) { 20 | commit('clearAuthenticateError') 21 | } 22 | return feathersClient 23 | .authenticate(data, params) 24 | .then(response => { 25 | return dispatch('responseHandler', response) 26 | }) 27 | .catch(error => { 28 | commit('setAuthenticateError', error) 29 | commit('unsetAuthenticatePending') 30 | return Promise.reject(error) 31 | }) 32 | }, 33 | 34 | responseHandler({ commit, state, dispatch }, response) { 35 | if (response.accessToken) { 36 | commit('setAccessToken', response.accessToken) 37 | commit('setPayload', response) 38 | 39 | // Handle when user is returned in the authenticate response 40 | let user = response[state.responseEntityField] 41 | 42 | if (user) { 43 | if (state.serverAlias && state.userService) { 44 | const Model = Object.keys(models[state.serverAlias]) 45 | .map(modelName => models[state.serverAlias][modelName]) 46 | .find(model => getNameFromPath(model.servicePath) === getNameFromPath(state.userService)) 47 | if (Model) { 48 | // Copy user object to avoid setupInstance modifying payload state 49 | user = new Model(fastCopy(user)) 50 | } 51 | } 52 | commit('setUser', user) 53 | commit('unsetAuthenticatePending') 54 | } else if ( 55 | state.userService && 56 | response.hasOwnProperty(state.entityIdField) 57 | ) { 58 | return dispatch( 59 | 'populateUser', 60 | response[state.entityIdField] 61 | ).then(() => { 62 | commit('unsetAuthenticatePending') 63 | return response 64 | }) 65 | } 66 | return response 67 | 68 | // If there was not an accessToken in the response, allow the response to pass through to handle two-factor-auth 69 | } else { 70 | return response 71 | } 72 | }, 73 | 74 | populateUser({ commit, state, dispatch }, userId) { 75 | return dispatch(`${state.userService}/get`, userId, { root: true }).then( 76 | user => { 77 | commit('setUser', user) 78 | return user 79 | } 80 | ) 81 | }, 82 | 83 | logout({ commit }) { 84 | commit('setLogoutPending') 85 | return feathersClient 86 | .logout() 87 | .then(response => { 88 | commit('logout') 89 | commit('unsetLogoutPending') 90 | return response 91 | }) 92 | .catch(error => { 93 | return Promise.reject(error) 94 | }) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/auth-module/auth-module.getters.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | export default function makeAuthGetters({ userService }) { 7 | const getters = {} 8 | 9 | if (userService) { 10 | Object.assign(getters, { 11 | // A reactive user object 12 | user(state, getters, rootState) { 13 | if (!state.user) { 14 | return null 15 | } 16 | const { idField } = rootState[userService] 17 | const userId = state.user[idField] 18 | return rootState[userService].keyedById[userId] || null 19 | }, 20 | isAuthenticated(state, getters) { 21 | return !!getters.user 22 | } 23 | }) 24 | } 25 | 26 | return getters 27 | } 28 | -------------------------------------------------------------------------------- /src/auth-module/auth-module.mutations.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { serializeError } from 'serialize-error' 7 | 8 | export default function makeAuthMutations() { 9 | return { 10 | setAccessToken(state, payload) { 11 | state.accessToken = payload 12 | }, 13 | setPayload(state, payload) { 14 | state.payload = payload 15 | }, 16 | setUser(state, payload) { 17 | state.user = payload 18 | }, 19 | 20 | setAuthenticatePending(state) { 21 | state.isAuthenticatePending = true 22 | }, 23 | unsetAuthenticatePending(state) { 24 | state.isAuthenticatePending = false 25 | }, 26 | setLogoutPending(state) { 27 | state.isLogoutPending = true 28 | }, 29 | unsetLogoutPending(state) { 30 | state.isLogoutPending = false 31 | }, 32 | 33 | setAuthenticateError(state, error) { 34 | state.errorOnAuthenticate = Object.assign({}, serializeError(error)) 35 | }, 36 | clearAuthenticateError(state) { 37 | state.errorOnAuthenticate = null 38 | }, 39 | setLogoutError(state, error) { 40 | state.errorOnLogout = Object.assign({}, serializeError(error)) 41 | }, 42 | clearLogoutError(state) { 43 | state.errorOnLogout = null 44 | }, 45 | 46 | logout(state) { 47 | state.payload = null 48 | state.accessToken = null 49 | if (state.user) { 50 | state.user = null 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/auth-module/auth-module.state.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { AuthState } from './types' 7 | 8 | export default function setupAuthState({ 9 | userService, 10 | serverAlias, 11 | responseEntityField = 'user', 12 | entityIdField = 'userId' 13 | }) { 14 | const state: AuthState = { 15 | accessToken: null, // The JWT 16 | payload: null, // The JWT payload 17 | entityIdField, 18 | responseEntityField, 19 | 20 | isAuthenticatePending: false, 21 | isLogoutPending: false, 22 | 23 | errorOnAuthenticate: null, 24 | errorOnLogout: null, 25 | user: null, // For a reactive user object, use the `user` getter. 26 | userService: null, 27 | serverAlias 28 | } 29 | // If a userService string was passed, add a user attribute 30 | if (userService) { 31 | Object.assign(state, { userService }) 32 | } 33 | return state 34 | } 35 | -------------------------------------------------------------------------------- /src/auth-module/make-auth-plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { FeathersVuexOptions } from '../service-module/types' 7 | import setupState from './auth-module.state' 8 | import setupGetters from './auth-module.getters' 9 | import setupMutations from './auth-module.mutations' 10 | import setupActions from './auth-module.actions' 11 | 12 | const defaults = { 13 | namespace: 'auth', 14 | userService: '', // Set this to automatically populate the user (using an additional request) on login success. 15 | serverAlias: 'api', 16 | debug: false, 17 | state: {}, // for custom state 18 | getters: {}, // for custom getters 19 | mutations: {}, // for custom mutations 20 | actions: {} // for custom actions 21 | } 22 | 23 | export default function authPluginInit( 24 | feathersClient, 25 | globalOptions: FeathersVuexOptions 26 | ) { 27 | if (!feathersClient || !feathersClient.service) { 28 | throw new Error('You must pass a Feathers Client instance to feathers-vuex') 29 | } 30 | 31 | return function makeAuthPlugin(options) { 32 | options = Object.assign( 33 | {}, 34 | defaults, 35 | { serverAlias: globalOptions.serverAlias }, 36 | options 37 | ) 38 | 39 | if (!feathersClient.authenticate) { 40 | throw new Error( 41 | 'You must register the @feathersjs/authentication-client plugin before using the feathers-vuex auth module' 42 | ) 43 | } 44 | if (options.debug && options.userService && !options.serverAlias) { 45 | console.warn( 46 | '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.' 47 | ) 48 | } 49 | 50 | const defaultState = setupState(options) 51 | const defaultGetters = setupGetters(options) 52 | const defaultMutations = setupMutations() 53 | const defaultActions = setupActions(feathersClient) 54 | 55 | return function setupStore(store) { 56 | const { namespace } = options 57 | 58 | store.registerModule(namespace, { 59 | namespaced: true, 60 | state: Object.assign({}, defaultState, options.state), 61 | getters: Object.assign({}, defaultGetters, options.getters), 62 | mutations: Object.assign({}, defaultMutations, options.mutations), 63 | actions: Object.assign({}, defaultActions, options.actions) 64 | }) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/auth-module/types.ts: -------------------------------------------------------------------------------- 1 | export interface AuthState { 2 | accessToken: string 3 | payload: {} 4 | entityIdField: string 5 | responseEntityField: string 6 | 7 | isAuthenticatePending: boolean 8 | isLogoutPending: boolean 9 | 10 | errorOnAuthenticate: Error 11 | errorOnLogout: Error 12 | user: {} 13 | userService: string 14 | serverAlias: string 15 | } 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import FeathersVuexFind from './FeathersVuexFind' 7 | import FeathersVuexGet from './FeathersVuexGet' 8 | import FeathersVuexFormWrapper from './FeathersVuexFormWrapper' 9 | import FeathersVuexInputWrapper from './FeathersVuexInputWrapper' 10 | import FeathersVuexPagination from './FeathersVuexPagination' 11 | import makeFindMixin from './make-find-mixin' 12 | import makeGetMixin from './make-get-mixin' 13 | import { globalModels as models } from './service-module/global-models' 14 | import { clients, addClient } from './service-module/global-clients' 15 | import makeBaseModel from './service-module/make-base-model' 16 | import prepareMakeServicePlugin from './service-module/make-service-plugin' 17 | import prepareMakeAuthPlugin from './auth-module/make-auth-plugin' 18 | import useFind from './useFind' 19 | import useGet from './useGet' 20 | 21 | import { 22 | FeathersVuexOptions, 23 | HandleEvents, 24 | Model, 25 | ModelStatic, 26 | ModelSetupContext, 27 | Id, 28 | FeathersVuexStoreState, 29 | FeathersVuexGlobalModels, 30 | GlobalModels 31 | } from './service-module/types' 32 | import { initAuth, hydrateApi } from './utils' 33 | import { FeathersVuex } from './vue-plugin/vue-plugin' 34 | import { ServiceState } from './service-module/service-module.state' 35 | import { AuthState } from './auth-module/types' 36 | const events = ['created', 'patched', 'updated', 'removed'] 37 | 38 | const defaults: FeathersVuexOptions = { 39 | autoRemove: false, // Automatically remove records missing from responses (only use with feathers-rest) 40 | addOnUpsert: false, // Add new records pushed by 'updated/patched' socketio events into store, instead of discarding them 41 | enableEvents: true, // Listens to socket.io events when available 42 | idField: 'id', // The field in each record that will contain the id 43 | tempIdField: '__id', 44 | debug: false, // Set to true to enable logging messages. 45 | keepCopiesInStore: false, // Set to true to store cloned copies in the store instead of on the Model. 46 | nameStyle: 'short', // Determines the source of the module name. 'short', 'path', or 'explicit' 47 | 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/ 48 | preferUpdate: false, // When true, calling model.save() will do an update instead of a patch. 49 | replaceItems: false, // Instad of merging in changes in the store, replace the entire record. 50 | serverAlias: 'api', 51 | handleEvents: {} as HandleEvents, 52 | skipRequestIfExists: false, // For get action, if the record already exists in store, skip the remote request 53 | whitelist: [] // Custom query operators that will be allowed in the find getter. 54 | } 55 | 56 | export default function feathersVuex(feathers, options: FeathersVuexOptions) { 57 | if (!feathers || !feathers.service) { 58 | throw new Error( 59 | 'The first argument to feathersVuex must be a feathers client.' 60 | ) 61 | } 62 | 63 | // Setup the event handlers. By default they just return the value of `options.enableEvents` 64 | defaults.handleEvents = events.reduce((obj, eventName) => { 65 | obj[eventName] = () => options.enableEvents || true 66 | return obj 67 | }, {} as HandleEvents) 68 | 69 | options = Object.assign({}, defaults, options) 70 | 71 | if (!options.serverAlias) { 72 | throw new Error( 73 | `You must provide a 'serverAlias' in the options to feathersVuex` 74 | ) 75 | } 76 | 77 | addClient({ client: feathers, serverAlias: options.serverAlias }) 78 | 79 | const BaseModel = makeBaseModel(options) 80 | const makeServicePlugin = prepareMakeServicePlugin(options) 81 | const makeAuthPlugin = prepareMakeAuthPlugin(feathers, options) 82 | 83 | return { 84 | makeServicePlugin, 85 | BaseModel, 86 | makeAuthPlugin, 87 | FeathersVuex, 88 | models: models as GlobalModels, 89 | clients 90 | } 91 | } 92 | 93 | export { 94 | initAuth, 95 | hydrateApi, 96 | FeathersVuexFind, 97 | FeathersVuexGet, 98 | FeathersVuexFormWrapper, 99 | FeathersVuexInputWrapper, 100 | FeathersVuexPagination, 101 | FeathersVuex, 102 | makeFindMixin, 103 | makeGetMixin, 104 | models, 105 | clients, 106 | useFind, 107 | useGet, 108 | AuthState, 109 | Id, 110 | Model, 111 | ModelStatic, 112 | ModelSetupContext, 113 | ServiceState, 114 | FeathersVuexGlobalModels, 115 | FeathersVuexStoreState 116 | } 117 | -------------------------------------------------------------------------------- /src/make-get-mixin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | no-console: 0, 4 | @typescript-eslint/explicit-function-return-type: 0, 5 | @typescript-eslint/no-explicit-any: 0 6 | */ 7 | import inflection from 'inflection' 8 | 9 | export default function makeFindMixin(options) { 10 | const { 11 | service, 12 | params, 13 | fetchParams, 14 | queryWhen, 15 | id, 16 | local = false, 17 | qid = 'default', 18 | item, 19 | debug 20 | } = options 21 | let { name, watch = [] } = options 22 | 23 | if (typeof watch === 'string') { 24 | watch = [watch] 25 | } else if (typeof watch === 'boolean' && watch) { 26 | watch = ['query'] 27 | } 28 | 29 | if ( 30 | !service || 31 | (typeof service !== 'string' && typeof service !== 'function') 32 | ) { 33 | throw new Error( 34 | `The 'service' option is required in the FeathersVuex make-find-mixin and must be a string.` 35 | ) 36 | } 37 | if (typeof service === 'function' && !name) { 38 | name = 'service' 39 | } 40 | 41 | const nameToUse = (name || service).replace(/-/g, '_') 42 | const singularized = inflection.singularize(nameToUse) 43 | const prefix = inflection.camelize(singularized, true) 44 | const capitalized = prefix.charAt(0).toUpperCase() + prefix.slice(1) 45 | const SERVICE_NAME = `${prefix}ServiceName` 46 | let ITEM = item || prefix 47 | if (typeof service === 'function' && name === 'service' && !item) { 48 | ITEM = 'item' 49 | } 50 | const IS_GET_PENDING = `isGet${capitalized}Pending` 51 | const PARAMS = `${prefix}Params` 52 | const FETCH_PARAMS = `${prefix}FetchParams` 53 | const WATCH = `${prefix}Watch` 54 | const QUERY_WHEN = `${prefix}QueryWhen` 55 | const ERROR = `${prefix}Error` 56 | const GET_ACTION = `get${capitalized}` 57 | const GET_GETTER = `get${capitalized}FromStore` 58 | const HAS_ITEM_BEEN_REQUESTED_ONCE = `has${capitalized}BeenRequestedOnce` 59 | const HAS_ITEM_LOADED_ONCE = `has${capitalized}LoadedOnce` 60 | const LOCAL = `${prefix}Local` 61 | const QID = `${prefix}Qid` 62 | const ID = `${prefix}Id` 63 | const data = { 64 | [IS_GET_PENDING]: false, 65 | [HAS_ITEM_BEEN_REQUESTED_ONCE]: false, 66 | [HAS_ITEM_LOADED_ONCE]: false, 67 | [WATCH]: watch, 68 | [QID]: qid, 69 | [ERROR]: null 70 | } 71 | 72 | const mixin = { 73 | data() { 74 | return data 75 | }, 76 | computed: { 77 | [ITEM]() { 78 | return this[ID] 79 | ? this.$store.getters[`${this[SERVICE_NAME]}/get`](this[ID]) 80 | : null 81 | }, 82 | [QUERY_WHEN]() { 83 | return true 84 | }, 85 | // Exposes `getFromStore` 86 | [GET_GETTER]() { 87 | return id => { 88 | const serviceName = this[SERVICE_NAME] 89 | return this.$store.getters[`${serviceName}/get`](id) 90 | } 91 | } 92 | }, 93 | methods: { 94 | [GET_ACTION](id, params) { 95 | const paramsToUse = params || this[FETCH_PARAMS] || this[PARAMS] 96 | const idToUse = id || this[ID] 97 | 98 | if (this[QUERY_WHEN]) { 99 | this[IS_GET_PENDING] = true 100 | this[HAS_ITEM_BEEN_REQUESTED_ONCE] = true 101 | 102 | if (idToUse != null) { 103 | return this.$store 104 | .dispatch(`${this[SERVICE_NAME]}/get`, [idToUse, paramsToUse]) 105 | .then(response => { 106 | // To prevent thrashing, only clear ERROR on response, not on initial request. 107 | this[ERROR] = null 108 | 109 | this[HAS_ITEM_LOADED_ONCE] = true 110 | this[IS_GET_PENDING] = false 111 | return response 112 | }) 113 | .catch(error => { 114 | this[ERROR] = error 115 | return error 116 | }) 117 | } 118 | } 119 | } 120 | }, 121 | // add the created lifecycle hook only if local option is falsy 122 | ...(!local && { 123 | created() { 124 | if (debug) { 125 | console.log( 126 | `running 'created' hook in makeGetMixin for service "${service}" (using name ${nameToUse}")` 127 | ) 128 | console.log(ID, this[ID]) 129 | console.log(PARAMS, this[PARAMS]) 130 | console.log(FETCH_PARAMS, this[FETCH_PARAMS]) 131 | } 132 | 133 | const pType = Object.getPrototypeOf(this) 134 | 135 | if ( 136 | this.hasOwnProperty(ID) || 137 | pType.hasOwnProperty(ID) || 138 | pType.hasOwnProperty(PARAMS) || 139 | pType.hasOwnProperty(FETCH_PARAMS) 140 | ) { 141 | if (!watch.includes(ID)) { 142 | watch.push(ID) 143 | } 144 | 145 | watch.forEach(prop => { 146 | if (typeof prop !== 'string') { 147 | throw new Error(`Values in the 'watch' array must be strings.`) 148 | } 149 | prop = prop.replace('query', PARAMS) 150 | 151 | if (pType.hasOwnProperty(FETCH_PARAMS)) { 152 | if (prop.startsWith(PARAMS)) { 153 | prop.replace(PARAMS, FETCH_PARAMS) 154 | } 155 | } 156 | this.$watch(prop, function() { 157 | return this[GET_ACTION]() 158 | }) 159 | }) 160 | 161 | return this[GET_ACTION]() 162 | } else { 163 | console.log( 164 | `No "${ID}", "${PARAMS}" or "${FETCH_PARAMS}" attribute was found in the makeGetMixin for the "${service}" service (using name "${nameToUse}"). No queries will be made.` 165 | ) 166 | } 167 | } 168 | }) 169 | } 170 | 171 | function hasSomeAttribute(vm, ...attributes) { 172 | return attributes.some(a => { 173 | return vm.hasOwnProperty(a) || Object.getPrototypeOf(vm).hasOwnProperty(a) 174 | }) 175 | } 176 | 177 | function setupAttribute( 178 | NAME, 179 | value, 180 | computedOrMethods = 'computed', 181 | returnTheValue = false 182 | ) { 183 | if (typeof value === 'boolean') { 184 | data[NAME] = !!value 185 | } else if (typeof value === 'string') { 186 | mixin.computed[NAME] = function() { 187 | // If the specified computed prop wasn't found, display an error. 188 | if (!returnTheValue) { 189 | if (!hasSomeAttribute(this, value, NAME)) { 190 | throw new Error( 191 | `Value for ${NAME} was not found on the component at '${value}'.` 192 | ) 193 | } 194 | } 195 | return returnTheValue ? value : this[value] 196 | } 197 | } else if (typeof value === 'function') { 198 | mixin[computedOrMethods][NAME] = value 199 | } 200 | } 201 | 202 | setupAttribute(SERVICE_NAME, service, 'computed', true) 203 | setupAttribute(ID, id) 204 | setupAttribute(PARAMS, params) 205 | setupAttribute(FETCH_PARAMS, fetchParams) 206 | setupAttribute(QUERY_WHEN, queryWhen, 'computed') 207 | setupAttribute(LOCAL, local) 208 | 209 | return mixin 210 | } 211 | -------------------------------------------------------------------------------- /src/service-module/global-clients.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import _get from 'lodash/get' 7 | 8 | /** 9 | * A global object that holds references to all Model Classes in the application. 10 | */ 11 | export const clients: { [k: string]: any } = { 12 | byAlias: {}, 13 | byHost: {} 14 | } 15 | 16 | /** 17 | * prepareAddModel wraps options in a closure around addModel 18 | * @param options 19 | */ 20 | export function addClient({ client, serverAlias }) { 21 | // Save reference to the clients by host uri, if it was available. 22 | let uri = '' 23 | if (client.io) { 24 | uri = _get(client, 'io.io.uri') 25 | } 26 | if (uri) { 27 | clients.byHost[uri] = client 28 | } 29 | // Save reference to clients by serverAlias. 30 | clients.byAlias[serverAlias] = client 31 | } 32 | 33 | export function clearClients() { 34 | function deleteKeys(path) { 35 | Object.keys(clients[path]).forEach(key => { 36 | delete clients[path][key] 37 | }) 38 | } 39 | deleteKeys('byAlias') 40 | deleteKeys('byHost') 41 | } 42 | -------------------------------------------------------------------------------- /src/service-module/global-models.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | no-console: 0, 4 | @typescript-eslint/explicit-function-return-type: 0, 5 | @typescript-eslint/no-explicit-any: 0 6 | */ 7 | import { FeathersVuexOptions } from './types' 8 | 9 | /** 10 | * A global object that holds references to all Model Classes in the application. 11 | */ 12 | export const globalModels: { [k: string]: any } = {} 13 | 14 | /** 15 | * prepareAddModel wraps options in a closure around addModel 16 | * @param options 17 | */ 18 | export function prepareAddModel(options: FeathersVuexOptions) { 19 | const { serverAlias } = options 20 | 21 | return function addModel(Model) { 22 | globalModels[serverAlias] = globalModels[serverAlias] || { 23 | byServicePath: {} 24 | } 25 | const name = Model.modelName || Model.name 26 | if (globalModels[serverAlias][name] && options.debug) { 27 | // eslint-disable-next-line no-console 28 | console.error(`Overwriting Model: models[${serverAlias}][${name}].`) 29 | } 30 | globalModels[serverAlias][name] = Model 31 | globalModels[serverAlias].byServicePath[Model.servicePath] = Model 32 | } 33 | } 34 | 35 | export function clearModels() { 36 | Object.keys(globalModels).forEach(key => { 37 | const serverAliasObj = globalModels[key] 38 | 39 | Object.keys(serverAliasObj).forEach(key => { 40 | delete globalModels[key] 41 | }) 42 | 43 | delete globalModels[key] 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/service-module/make-service-module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import _pick from 'lodash/pick' 7 | import _merge from 'lodash/merge' 8 | import makeDefaultState from './service-module.state' 9 | import makeGetters from './service-module.getters' 10 | import makeMutations from './service-module.mutations' 11 | import makeActions from './service-module.actions' 12 | import { Service } from '@feathersjs/feathers' 13 | import { MakeServicePluginOptions } from './types' 14 | import { Store } from 'vuex' 15 | 16 | export default function makeServiceModule( 17 | service: Service, 18 | options: MakeServicePluginOptions, 19 | store: Store 20 | ) { 21 | const defaults = { 22 | namespaced: true, 23 | state: makeDefaultState(options), 24 | getters: makeGetters(), 25 | mutations: makeMutations(), 26 | actions: makeActions({service, options}) 27 | } 28 | const fromOptions = _pick(options, [ 29 | 'state', 30 | 'getters', 31 | 'mutations', 32 | 'actions' 33 | ]) 34 | const merged = _merge({}, defaults, fromOptions) 35 | const extended = options.extend({ store, module: merged }) 36 | const finalModule = _merge({}, merged, extended) 37 | 38 | return finalModule 39 | } 40 | -------------------------------------------------------------------------------- /src/service-module/make-service-plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { 7 | FeathersVuexOptions, 8 | MakeServicePluginOptions, 9 | ServicePluginExtendOptions 10 | } from './types' 11 | 12 | import makeServiceModule from './make-service-module' 13 | import { globalModels, prepareAddModel } from './global-models' 14 | import enableServiceEvents from './service-module.events' 15 | import { makeNamespace, getServicePath, assignIfNotPresent } from '../utils' 16 | import _get from 'lodash/get' 17 | 18 | interface ServiceOptionsDefaults { 19 | servicePath: string 20 | namespace: string 21 | extend: ( 22 | options: ServicePluginExtendOptions 23 | ) => { 24 | state: any 25 | getters: any 26 | mutations: any 27 | actions: any 28 | } 29 | state: {} 30 | getters: {} 31 | mutations: {} 32 | actions: {} 33 | instanceDefaults: () => {} 34 | setupInstance: (instance: {}) => {} 35 | debounceEventsMaxWait: number 36 | } 37 | 38 | const defaults: ServiceOptionsDefaults = { 39 | 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. 40 | servicePath: '', 41 | extend: ({ module }) => module, // for custom plugin (replaces state, getters, mutations, and actions) 42 | state: {}, // for custom state 43 | getters: {}, // for custom getters 44 | mutations: {}, // for custom mutations 45 | actions: {}, // for custom actions 46 | instanceDefaults: () => ({}), // Default instanceDefaults returns an empty object 47 | setupInstance: instance => instance, // Default setupInstance returns the instance 48 | debounceEventsMaxWait: 1000 49 | } 50 | const events = ['created', 'patched', 'updated', 'removed'] 51 | 52 | /** 53 | * prepare only wraps the makeServicePlugin to provide the globalOptions. 54 | * @param globalOptions 55 | */ 56 | export default function prepareMakeServicePlugin( 57 | globalOptions: FeathersVuexOptions 58 | ) { 59 | const addModel = prepareAddModel(globalOptions) 60 | /** 61 | * (1) Make a Vuex plugin for the provided service. 62 | * (2a) Attach the vuex store to the BaseModel. 63 | * (2b) If the Model does not extend the BaseModel, monkey patch it, too 64 | * (3) Setup real-time events 65 | */ 66 | return function makeServicePlugin(config: MakeServicePluginOptions) { 67 | if (!config.service) { 68 | throw new Error( 69 | '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.' 70 | ) 71 | } 72 | const options = Object.assign({}, defaults, globalOptions, config) 73 | const { 74 | Model, 75 | service, 76 | namespace, 77 | nameStyle, 78 | instanceDefaults, 79 | setupInstance, 80 | preferUpdate 81 | } = options 82 | 83 | if (globalOptions.handleEvents && options.handleEvents) { 84 | options.handleEvents = Object.assign( 85 | {}, 86 | globalOptions.handleEvents, 87 | options.handleEvents 88 | ) 89 | } 90 | 91 | events.forEach(eventName => { 92 | if (!options.handleEvents[eventName]) 93 | options.handleEvents[eventName] = () => options.enableEvents || true 94 | }) 95 | 96 | // Make sure we get a service path from either the service or the options 97 | let { servicePath } = options 98 | if (!servicePath) { 99 | servicePath = getServicePath(service, Model) 100 | } 101 | options.servicePath = servicePath 102 | 103 | service.FeathersVuexModel = Model 104 | 105 | return store => { 106 | // (1^) Create and register the Vuex module 107 | options.namespace = makeNamespace(namespace, servicePath, nameStyle) 108 | const module = makeServiceModule(service, options, store) 109 | // Don't preserve state if reinitialized (prevents state pollution in SSR) 110 | store.registerModule(options.namespace, module, { preserveState: false }) 111 | 112 | // (2a^) Monkey patch the BaseModel in globalModels 113 | const BaseModel = _get(globalModels, [options.serverAlias, 'BaseModel']) 114 | if (BaseModel && !BaseModel.store) { 115 | Object.assign(BaseModel, { 116 | store 117 | }) 118 | } 119 | // (2b^) Monkey patch the Model(s) and add to globalModels 120 | assignIfNotPresent(Model, { 121 | namespace: options.namespace, 122 | servicePath, 123 | instanceDefaults, 124 | setupInstance, 125 | preferUpdate 126 | }) 127 | // As per 1^, don't preserve state on the model either (prevents state pollution in SSR) 128 | Object.assign(Model, { 129 | store 130 | }) 131 | if (!Model.modelName || Model.modelName === 'BaseModel') { 132 | throw new Error( 133 | 'The modelName property is required for Feathers-Vuex Models' 134 | ) 135 | } 136 | addModel(Model) 137 | 138 | // (3^) Setup real-time events 139 | if (options.enableEvents) { 140 | enableServiceEvents({ service, Model, store, options }) 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/service-module/service-module.events.ts: -------------------------------------------------------------------------------- 1 | import { getId } from '../utils' 2 | import _debounce from 'lodash/debounce' 3 | import { globalModels } from './global-models' 4 | 5 | export interface ServiceEventsDebouncedQueue { 6 | addOrUpdateById: {} 7 | removeItemById: {} 8 | enqueueAddOrUpdate(item: any): void 9 | enqueueRemoval(item: any): void 10 | flushAddOrUpdateQueue(): void 11 | flushRemoveItemQueue(): void 12 | } 13 | 14 | export default function enableServiceEvents({ 15 | service, 16 | Model, 17 | store, 18 | options 19 | }): ServiceEventsDebouncedQueue { 20 | const debouncedQueue: ServiceEventsDebouncedQueue = { 21 | addOrUpdateById: {}, 22 | removeItemById: {}, 23 | enqueueAddOrUpdate(item): void { 24 | const id = getId(item, options.idField) 25 | this.addOrUpdateById[id] = item 26 | if (this.removeItemById.hasOwnProperty(id)) { 27 | delete this.removeItemById[id] 28 | } 29 | this.flushAddOrUpdateQueue() 30 | }, 31 | enqueueRemoval(item): void { 32 | const id = getId(item, options.idField) 33 | this.removeItemById[id] = item 34 | if (this.addOrUpdateById.hasOwnProperty(id)) { 35 | delete this.addOrUpdateById[id] 36 | } 37 | this.flushRemoveItemQueue() 38 | }, 39 | flushAddOrUpdateQueue: _debounce( 40 | async function () { 41 | const values = Object.values(this.addOrUpdateById) 42 | if (values.length === 0) return 43 | await store.dispatch(`${options.namespace}/addOrUpdateList`, { 44 | data: values, 45 | disableRemove: true 46 | }) 47 | this.addOrUpdateById = {} 48 | }, 49 | options.debounceEventsTime || 20, 50 | { maxWait: options.debounceEventsMaxWait } 51 | ), 52 | flushRemoveItemQueue: _debounce( 53 | function () { 54 | const values = Object.values(this.removeItemById) 55 | if (values.length === 0) return 56 | store.commit(`${options.namespace}/removeItems`, values) 57 | this.removeItemById = {} 58 | }, 59 | options.debounceEventsTime || 20, 60 | { maxWait: options.debounceEventsMaxWait } 61 | ) 62 | } 63 | 64 | const handleEvent = (eventName, item, mutationName): void => { 65 | const handler = options.handleEvents[eventName] 66 | const confirmOrArray = handler(item, { 67 | model: Model, 68 | models: globalModels 69 | }) 70 | const [affectsStore, modified = item] = Array.isArray(confirmOrArray) 71 | ? confirmOrArray 72 | : [confirmOrArray] 73 | if (affectsStore) { 74 | if (!options.debounceEventsTime) { 75 | eventName === 'removed' 76 | ? store.commit(`${options.namespace}/removeItem`, modified) 77 | : store.dispatch(`${options.namespace}/${mutationName}`, modified) 78 | } else { 79 | eventName === 'removed' 80 | ? debouncedQueue.enqueueRemoval(item) 81 | : debouncedQueue.enqueueAddOrUpdate(item) 82 | } 83 | } 84 | } 85 | 86 | // Listen to socket events when available. 87 | service.on('created', item => { 88 | handleEvent('created', item, 'addOrUpdate') 89 | Model.emit && Model.emit('created', item) 90 | }) 91 | service.on('updated', item => { 92 | handleEvent('updated', item, 'addOrUpdate') 93 | Model.emit && Model.emit('updated', item) 94 | }) 95 | service.on('patched', item => { 96 | handleEvent('patched', item, 'addOrUpdate') 97 | Model.emit && Model.emit('patched', item) 98 | }) 99 | service.on('removed', item => { 100 | handleEvent('removed', item, 'removeItem') 101 | Model.emit && Model.emit('removed', item) 102 | }) 103 | 104 | return debouncedQueue 105 | } 106 | -------------------------------------------------------------------------------- /src/service-module/service-module.getters.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import sift from 'sift' 7 | import { filterQuery, sorter, select } from '@feathersjs/adapter-commons' 8 | import { globalModels as models } from './global-models' 9 | import _omit from 'lodash/omit' 10 | import { unref } from '@vue/composition-api' 11 | import { ServiceState } from '..' 12 | import { Id } from '@feathersjs/feathers' 13 | 14 | const FILTERS = ['$sort', '$limit', '$skip', '$select'] 15 | const additionalOperators = ['$elemMatch'] 16 | 17 | const getCopiesById = ({ 18 | keepCopiesInStore, 19 | servicePath, 20 | serverAlias, 21 | copiesById 22 | }) => { 23 | if (keepCopiesInStore) { 24 | return copiesById 25 | } else { 26 | const Model = models[serverAlias].byServicePath[servicePath] 27 | 28 | return Model.copiesById 29 | } 30 | } 31 | 32 | export default function makeServiceGetters() { 33 | return { 34 | list: state => Object.values(state.keyedById), 35 | find: state => _params => { 36 | const params = unref(_params) || {} 37 | 38 | const { 39 | paramsForServer, 40 | whitelist, 41 | keyedById, 42 | idField, 43 | tempsById 44 | } = state 45 | const q = _omit(params.query || {}, paramsForServer) 46 | 47 | const { query, filters } = filterQuery(q, { 48 | operators: additionalOperators.concat(whitelist) 49 | }) 50 | 51 | let values = Object.values(keyedById) as any 52 | 53 | if (params.temps) { 54 | values.push(...(Object.values(tempsById) as any)) 55 | } 56 | 57 | values = values.filter(sift(query)) 58 | 59 | if (params.copies) { 60 | const copiesById = getCopiesById(state) 61 | // replace keyedById value with existing clone value 62 | values = values.map(value => copiesById[value[idField]] || value) 63 | } 64 | 65 | const total = values.length 66 | 67 | if (filters.$sort !== undefined) { 68 | values.sort(sorter(filters.$sort)) 69 | } 70 | 71 | if (filters.$skip !== undefined && filters.$limit !== undefined) { 72 | values = values.slice(filters.$skip, filters.$limit + filters.$skip) 73 | } else if (filters.$skip !== undefined || filters.$limit !== undefined) { 74 | values = values.slice(filters.$skip, filters.$limit) 75 | } 76 | 77 | if (filters.$select) { 78 | values = select(params)(values) 79 | } 80 | 81 | return { 82 | total, 83 | limit: filters.$limit || 0, 84 | skip: filters.$skip || 0, 85 | data: values 86 | } 87 | }, 88 | count: (state, getters) => _params => { 89 | const params = unref(_params) || {} 90 | 91 | const cleanQuery = _omit(params.query, FILTERS) 92 | params.query = cleanQuery 93 | 94 | return getters.find(params).total 95 | }, 96 | get: ({ keyedById, tempsById, idField, tempIdField }) => ( 97 | _id, 98 | _params = {} 99 | ) => { 100 | const id = unref(_id) 101 | const params = unref(_params) 102 | 103 | const record = keyedById[id] && select(params, idField)(keyedById[id]) 104 | if (record) { 105 | return record 106 | } 107 | const tempRecord = 108 | tempsById[id] && select(params, tempIdField)(tempsById[id]) 109 | 110 | return tempRecord || null 111 | }, 112 | getCopyById: state => id => { 113 | const copiesById = getCopiesById(state) 114 | return copiesById[id] 115 | }, 116 | 117 | isCreatePendingById: ({ isIdCreatePending }: ServiceState) => (id: Id) => 118 | isIdCreatePending.includes(id), 119 | isUpdatePendingById: ({ isIdUpdatePending }: ServiceState) => (id: Id) => 120 | isIdUpdatePending.includes(id), 121 | isPatchPendingById: ({ isIdPatchPending }: ServiceState) => (id: Id) => 122 | isIdPatchPending.includes(id), 123 | isRemovePendingById: ({ isIdRemovePending }: ServiceState) => (id: Id) => 124 | isIdRemovePending.includes(id), 125 | isSavePendingById: (state: ServiceState, getters) => (id: Id) => 126 | getters.isCreatePendingById(id) || 127 | getters.isUpdatePendingById(id) || 128 | getters.isPatchPendingById(id), 129 | isPendingById: (state: ServiceState, getters) => (id: Id) => 130 | getters.isSavePendingById(id) || getters.isRemovePendingById(id) 131 | } 132 | } 133 | 134 | export type GetterName = keyof ReturnType 135 | -------------------------------------------------------------------------------- /src/service-module/service-module.state.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | 7 | import _omit from 'lodash/omit' 8 | 9 | import { MakeServicePluginOptions, Model } from './types' 10 | import { Id } from '@feathersjs/feathers' 11 | 12 | export interface ServiceStateExclusiveDefaults { 13 | ids: string[] 14 | 15 | errorOnFind: any 16 | errorOnGet: any 17 | errorOnCreate: any 18 | errorOnPatch: any 19 | errorOnUpdate: any 20 | errorOnRemove: any 21 | 22 | isFindPending: boolean 23 | isGetPending: boolean 24 | isCreatePending: boolean 25 | isPatchPending: boolean 26 | isUpdatePending: boolean 27 | isRemovePending: boolean 28 | 29 | keyedById: {} 30 | tempsById: {} 31 | copiesById: {} 32 | namespace?: string 33 | pagination?: { 34 | defaultLimit: number 35 | defaultSkip: number 36 | default?: PaginationState 37 | } 38 | paramsForServer: string[] 39 | modelName?: string 40 | debounceEventsTime: number 41 | isIdCreatePending: Id[] 42 | isIdUpdatePending: Id[] 43 | isIdPatchPending: Id[] 44 | isIdRemovePending: Id[] 45 | } 46 | 47 | export interface ServiceState { 48 | options: {} 49 | ids: string[] 50 | autoRemove: boolean 51 | errorOnFind: any 52 | errorOnGet: any 53 | errorOnCreate: any 54 | errorOnPatch: any 55 | errorOnUpdate: any 56 | errorOnRemove: any 57 | isFindPending: boolean 58 | isGetPending: boolean 59 | isCreatePending: boolean 60 | isPatchPending: boolean 61 | isUpdatePending: boolean 62 | isRemovePending: boolean 63 | idField: string 64 | tempIdField: string 65 | keyedById: { 66 | [k: string]: M 67 | [k: number]: M 68 | } 69 | tempsById: { 70 | [k: string]: M 71 | [k: number]: M 72 | } 73 | copiesById: { 74 | [k: string]: M 75 | } 76 | whitelist: string[] 77 | paramsForServer: string[] 78 | namespace: string 79 | nameStyle: string // Should be enum of 'short' or 'path' 80 | pagination?: { 81 | defaultLimit: number 82 | defaultSkip: number 83 | default?: PaginationState 84 | } 85 | modelName?: string 86 | debounceEventsTime: number 87 | debounceEventsMaxWait: number 88 | isIdCreatePending: Id[] 89 | isIdUpdatePending: Id[] 90 | isIdPatchPending: Id[] 91 | isIdRemovePending: Id[] 92 | } 93 | 94 | export interface PaginationState { 95 | ids: any 96 | limit: number 97 | skip: number 98 | ip: number 99 | total: number 100 | mostRecent: any 101 | } 102 | 103 | export default function makeDefaultState(options: MakeServicePluginOptions) { 104 | const nonStateProps = [ 105 | 'Model', 106 | 'service', 107 | 'instanceDefaults', 108 | 'setupInstance', 109 | 'handleEvents', 110 | 'extend', 111 | 'state', 112 | 'getters', 113 | 'mutations', 114 | 'actions' 115 | ] 116 | 117 | const state: ServiceStateExclusiveDefaults = { 118 | ids: [], 119 | keyedById: {}, 120 | copiesById: {}, 121 | tempsById: {}, // Really should be called tempsByTempId 122 | pagination: { 123 | defaultLimit: null, 124 | defaultSkip: null 125 | }, 126 | paramsForServer: ['$populateParams'], 127 | debounceEventsTime: null, 128 | 129 | isFindPending: false, 130 | isGetPending: false, 131 | isCreatePending: false, 132 | isUpdatePending: false, 133 | isPatchPending: false, 134 | isRemovePending: false, 135 | 136 | errorOnFind: null, 137 | errorOnGet: null, 138 | errorOnCreate: null, 139 | errorOnUpdate: null, 140 | errorOnPatch: null, 141 | errorOnRemove: null, 142 | 143 | isIdCreatePending: [], 144 | isIdUpdatePending: [], 145 | isIdPatchPending: [], 146 | isIdRemovePending: [] 147 | } 148 | 149 | if (options.Model) { 150 | state.modelName = options.Model.modelName 151 | } 152 | 153 | const startingState = _omit(options, nonStateProps) 154 | 155 | return Object.assign({}, state, startingState) 156 | } 157 | -------------------------------------------------------------------------------- /src/useFind.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/no-explicit-any: 0 4 | */ 5 | import { 6 | computed, 7 | isRef, 8 | reactive, 9 | Ref, 10 | toRefs, 11 | watch 12 | } from '@vue/composition-api' 13 | import debounce from 'lodash/debounce' 14 | import { getItemsFromQueryInfo, getQueryInfo, Params, Paginated } from './utils' 15 | import { ModelStatic, Model } from './service-module/types' 16 | 17 | interface UseFindOptions { 18 | model: ModelStatic 19 | params: Params | Ref 20 | fetchParams?: Params | Ref 21 | queryWhen?: Ref 22 | qid?: string 23 | local?: boolean 24 | immediate?: boolean 25 | } 26 | interface UseFindState { 27 | debounceTime: null | number 28 | qid: string 29 | isPending: boolean 30 | haveBeenRequested: boolean 31 | haveLoaded: boolean 32 | error: null | Error 33 | latestQuery: null | object 34 | isLocal: boolean 35 | } 36 | interface UseFindData { 37 | items: Ref> 38 | servicePath: Ref 39 | isPending: Ref 40 | haveBeenRequested: Ref 41 | haveLoaded: Ref 42 | isLocal: Ref 43 | qid: Ref 44 | debounceTime: Ref 45 | latestQuery: Ref 46 | paginationData: Ref 47 | error: Ref 48 | find(params?: Params | Ref): Promise> 49 | } 50 | 51 | const unwrapParams = (params: Params | Ref): Params => 52 | isRef(params) ? params.value : params 53 | 54 | export default function find(options: UseFindOptions): UseFindData { 55 | const defaults: UseFindOptions = { 56 | model: null, 57 | params: null, 58 | qid: 'default', 59 | queryWhen: computed((): boolean => true), 60 | local: false, 61 | immediate: true 62 | } 63 | const { model, params, queryWhen, qid, local, immediate } = Object.assign( 64 | {}, 65 | defaults, 66 | options 67 | ) 68 | 69 | if (!model) { 70 | throw new Error( 71 | `No model provided for useFind(). Did you define and register it with FeathersVuex?` 72 | ) 73 | } 74 | 75 | const getFetchParams = (providedParams?: Params | Ref): Params => { 76 | const provided = unwrapParams(providedParams) 77 | 78 | if (provided) { 79 | return provided 80 | } 81 | 82 | const fetchParams = unwrapParams(options.fetchParams) 83 | // Returning null fetchParams allows the query to be skipped. 84 | if (fetchParams || fetchParams === null) { 85 | return fetchParams 86 | } 87 | 88 | const params = unwrapParams(options.params) 89 | return params 90 | } 91 | 92 | const state = reactive({ 93 | qid, 94 | isPending: false, 95 | haveBeenRequested: false, 96 | haveLoaded: local, 97 | error: null, 98 | debounceTime: null, 99 | latestQuery: null, 100 | isLocal: local 101 | }) 102 | const computes = { 103 | // The find getter 104 | items: computed(() => { 105 | const getterParams = unwrapParams(params) 106 | 107 | if (getterParams) { 108 | if (getterParams.paginate) { 109 | const serviceState = model.store.state[model.servicePath] 110 | const { defaultSkip, defaultLimit } = serviceState.pagination 111 | const skip = getterParams.query.$skip || defaultSkip 112 | const limit = getterParams.query.$limit || defaultLimit 113 | const pagination = 114 | computes.paginationData.value[getterParams.qid || state.qid] || {} 115 | const response = skip != null && limit != null ? { limit, skip } : {} 116 | const queryInfo = getQueryInfo(getterParams, response) 117 | const items = getItemsFromQueryInfo( 118 | pagination, 119 | queryInfo, 120 | serviceState.keyedById 121 | ) 122 | return items 123 | } else { 124 | return model.findInStore(getterParams).data 125 | } 126 | } else { 127 | return [] 128 | } 129 | }), 130 | paginationData: computed(() => { 131 | return model.store.state[model.servicePath].pagination 132 | }), 133 | servicePath: computed(() => model.servicePath) 134 | } 135 | 136 | function find(params?: Params | Ref): Promise> { 137 | params = unwrapParams(params) 138 | if (queryWhen.value && !state.isLocal) { 139 | state.isPending = true 140 | state.haveBeenRequested = true 141 | 142 | return model.find(params).then(response => { 143 | // To prevent thrashing, only clear error on response, not on initial request. 144 | state.error = null 145 | state.haveLoaded = true 146 | if(!Array.isArray(response)) { 147 | const queryInfo = getQueryInfo(params, response) 148 | queryInfo.response = response 149 | queryInfo.isOutdated = false 150 | state.latestQuery = queryInfo 151 | } 152 | state.isPending = false 153 | return response 154 | }) 155 | } 156 | } 157 | const methods = { 158 | findDebounced(params?: Params) { 159 | return find(params) 160 | } 161 | } 162 | function findProxy(params?: Params | Ref) { 163 | const paramsToUse = getFetchParams(params) 164 | 165 | if (paramsToUse && paramsToUse.debounce) { 166 | if (paramsToUse.debounce !== state.debounceTime) { 167 | methods.findDebounced = debounce(find, paramsToUse.debounce) 168 | state.debounceTime = paramsToUse.debounce 169 | } 170 | return methods.findDebounced(paramsToUse) 171 | } else if (paramsToUse) { 172 | return find(paramsToUse) 173 | } else { 174 | // Set error 175 | } 176 | } 177 | 178 | watch( 179 | () => getFetchParams(), 180 | () => { 181 | findProxy() 182 | }, 183 | { immediate } 184 | ) 185 | 186 | return { 187 | ...computes, 188 | ...toRefs(state), 189 | find 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/useGet.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/no-explicit-any: 0 4 | */ 5 | import { 6 | reactive, 7 | computed, 8 | toRefs, 9 | isRef, 10 | watch, 11 | Ref 12 | } from '@vue/composition-api' 13 | import { Params } from './utils' 14 | import { ModelStatic, Model, Id } from './service-module/types' 15 | 16 | interface UseGetOptions { 17 | model: ModelStatic 18 | id: null | string | number | Ref | Ref | Ref 19 | params?: Params | Ref 20 | queryWhen?: Ref 21 | local?: boolean 22 | immediate?: boolean 23 | } 24 | interface UseGetState { 25 | isPending: boolean 26 | hasBeenRequested: boolean 27 | hasLoaded: boolean 28 | error: null | Error 29 | isLocal: boolean 30 | } 31 | interface UseGetData { 32 | item: Ref> 33 | servicePath: Ref 34 | isPending: Ref 35 | hasBeenRequested: Ref 36 | hasLoaded: Ref 37 | isLocal: Ref 38 | error: Ref 39 | get(id: Id, params?: Params): Promise 40 | } 41 | 42 | export default function get(options: UseGetOptions): UseGetData { 43 | const defaults: UseGetOptions = { 44 | model: null, 45 | id: null, 46 | params: null, 47 | queryWhen: computed((): boolean => true), 48 | local: false, 49 | immediate: true 50 | } 51 | const { model, id, params, queryWhen, local, immediate } = Object.assign( 52 | {}, 53 | defaults, 54 | options 55 | ) 56 | 57 | if (!model) { 58 | throw new Error( 59 | `No model provided for useGet(). Did you define and register it with FeathersVuex?` 60 | ) 61 | } 62 | 63 | function getId(): null | string | number { 64 | return isRef(id) ? id.value : id || null 65 | } 66 | function getParams(): Params { 67 | return isRef(params) ? params.value : params 68 | } 69 | 70 | const state = reactive({ 71 | isPending: false, 72 | hasBeenRequested: false, 73 | hasLoaded: false, 74 | error: null, 75 | isLocal: local 76 | }) 77 | 78 | const computes = { 79 | item: computed(() => { 80 | const getterId = isRef(id) ? id.value : id 81 | const getterParams = isRef(params) 82 | ? Object.assign({}, params.value) 83 | : params == null 84 | ? params 85 | : { ...params } 86 | if (getterParams != null) { 87 | return model.getFromStore(getterId, getterParams) || null 88 | } else { 89 | return model.getFromStore(getterId) || null 90 | } 91 | }), 92 | servicePath: computed(() => model.servicePath) 93 | } 94 | 95 | 96 | 97 | function get(id: Id, params?: Params): Promise { 98 | const idToUse = isRef(id) ? id.value : id 99 | const paramsToUse = isRef(params) ? params.value : params 100 | 101 | if (idToUse != null && queryWhen.value && !state.isLocal) { 102 | state.isPending = true 103 | state.hasBeenRequested = true 104 | 105 | const promise = 106 | paramsToUse != null 107 | ? model.get(idToUse, paramsToUse) 108 | : model.get(idToUse) 109 | 110 | return promise 111 | .then(response => { 112 | state.isPending = false 113 | state.hasLoaded = true 114 | return response 115 | }) 116 | .catch(error => { 117 | state.isPending = false 118 | state.error = error 119 | return error 120 | }) 121 | } else { 122 | return Promise.resolve(undefined) 123 | } 124 | } 125 | 126 | watch( 127 | [() => getId(), () => getParams()], 128 | ([id, params]) => { 129 | get(id as string | number, params as Params) 130 | }, 131 | { immediate } 132 | ) 133 | 134 | return { 135 | ...toRefs(state), 136 | ...computes, 137 | get 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/vue-plugin/vue-plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import FeathersVuexFind from '../FeathersVuexFind' 7 | import FeathersVuexGet from '../FeathersVuexGet' 8 | import FeathersVuexFormWrapper from '../FeathersVuexFormWrapper' 9 | import FeathersVuexInputWrapper from '../FeathersVuexInputWrapper' 10 | import FeathersVuexPagination from '../FeathersVuexPagination' 11 | import FeathersVuexCount from '../FeathersVuexCount' 12 | import { globalModels } from '../service-module/global-models' 13 | import { GlobalModels } from '../service-module/types' 14 | 15 | // Augment global models onto VueConstructor and instance 16 | declare module 'vue/types/vue' { 17 | interface VueConstructor { 18 | $FeathersVuex: GlobalModels 19 | } 20 | interface Vue { 21 | $FeathersVuex: GlobalModels 22 | } 23 | } 24 | 25 | export const FeathersVuex = { 26 | install(Vue, options = { components: true }) { 27 | const shouldSetupComponents = options.components !== false 28 | 29 | Vue.$FeathersVuex = globalModels 30 | Vue.prototype.$FeathersVuex = globalModels 31 | 32 | if (shouldSetupComponents) { 33 | Vue.component('FeathersVuexFind', FeathersVuexFind) 34 | Vue.component('FeathersVuexGet', FeathersVuexGet) 35 | Vue.component('FeathersVuexFormWrapper', FeathersVuexFormWrapper) 36 | Vue.component('FeathersVuexInputWrapper', FeathersVuexInputWrapper) 37 | Vue.component('FeathersVuexPagination', FeathersVuexPagination) 38 | Vue.component('FeathersVuexCount', FeathersVuexCount) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /stories/.npmignore: -------------------------------------------------------------------------------- 1 | *.stories.js -------------------------------------------------------------------------------- /stories/FeathersVuexFormWrapper.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | import '../../assets/styles/tailwind.postcss' 3 | 4 | import FeathersVuexFormWrapper from '../src/FeathersVuexFormWrapper' 5 | import Readme from './README.md' 6 | 7 | import store from '../../store/store.dev' 8 | import { models } from 'feathers-vuex' 9 | 10 | export default { 11 | title: 'FeathersVuexFormWrapper', 12 | parameters: { 13 | component: FeathersVuexFormWrapper, 14 | readme: { 15 | sidebar: Readme 16 | } 17 | } 18 | } 19 | 20 | export const Basic = () => ({ 21 | components: { FeathersVuexFormWrapper }, 22 | data: () => ({ 23 | date: null, 24 | UserModel: models.api.User 25 | }), 26 | store, 27 | template: `
28 | 32 | 43 | 44 |
` 45 | }) 46 | -------------------------------------------------------------------------------- /stories/FeathersVuexInputWrapper.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | import FeathersVuexInputWrapper from '../src/FeathersVuexInputWrapper.vue' 3 | import { makeModel } from '@rovit/test-model' 4 | 5 | const User = makeModel() 6 | 7 | const user = new User({ 8 | _id: 1, 9 | email: 'marshall@rovit.com', 10 | carColor: '#FFF' 11 | }) 12 | 13 | export default { 14 | title: 'FeathersVuexInputWrapper', 15 | component: FeathersVuexInputWrapper 16 | } 17 | 18 | export const basic = () => ({ 19 | components: { 20 | FeathersVuexInputWrapper 21 | }, 22 | data: () => ({ 23 | user 24 | }), 25 | methods: { 26 | save({ clone, data }) { 27 | const user = clone.commit() 28 | user.patch(data) 29 | } 30 | }, 31 | template: ` 32 |
33 | 34 | 42 | 43 | 44 |
{{user}}
45 |
46 | ` 47 | }) 48 | 49 | export const handlerAsPromise = () => ({ 50 | components: { 51 | FeathersVuexInputWrapper 52 | }, 53 | data: () => ({ 54 | user 55 | }), 56 | methods: { 57 | async save({ clone, data }) { 58 | const user = clone.commit() 59 | return user.patch(data) 60 | } 61 | }, 62 | template: ` 63 |
64 | 65 | 74 | 75 | 76 |
{{user}}
77 |
78 | ` 79 | }) 80 | 81 | export const multipleOnDistinctProperties = () => ({ 82 | components: { 83 | FeathersVuexInputWrapper 84 | }, 85 | data: () => ({ 86 | user 87 | }), 88 | methods: { 89 | async save({ event, clone, prop, data }) { 90 | const user = clone.commit() 91 | return user.patch(data) 92 | } 93 | }, 94 | template: ` 95 |
96 | 97 | 105 | 106 | 107 | 108 | 116 | 117 | 118 |
{{user}}
119 |
120 | ` 121 | }) 122 | 123 | export const noInputInSlot = () => ({ 124 | components: { 125 | FeathersVuexInputWrapper 126 | }, 127 | data: () => ({ 128 | user 129 | }), 130 | methods: { 131 | async save({ clone, data }) { 132 | const user = clone.commit() 133 | user.patch(data) 134 | } 135 | }, 136 | template: ` 137 |
138 | 139 |
140 | ` 141 | }) 142 | -------------------------------------------------------------------------------- /test/auth-module/actions.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'chai/chai' 2 | import setupVuexAuth from '~/src/auth-module/auth-module' 3 | import setupVuexService from '~/src/service-module/service-module' 4 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' 5 | import Vuex, { mapActions } from 'vuex' 6 | import memory from 'feathers-memory' 7 | 8 | const options = {} 9 | const globalModels = {} 10 | 11 | const auth = setupVuexAuth(feathersClient, options, globalModels) 12 | const service = setupVuexService(feathersClient, options, globalModels) 13 | 14 | const accessToken = 15 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjAsImV4cCI6OTk5OTk5OTk5OTk5OX0.zmvEm8w142xGI7CbUsnvVGZk_hrVE1KEjzDt80LSW50' 16 | 17 | describe('Auth Module - Actions', () => { 18 | it('Authenticate', done => { 19 | const store = new Vuex.Store({ 20 | plugins: [auth()] 21 | }) 22 | feathersClient.use('authentication', { 23 | create(data) { 24 | return Promise.resolve({ accessToken }) 25 | } 26 | }) 27 | 28 | const authState = store.state.auth 29 | const actions = mapActions('auth', ['authenticate']) 30 | 31 | assert(authState.accessToken === null) 32 | assert(authState.errorOnAuthenticate === null) 33 | assert(authState.errorOnLogout === null) 34 | assert(authState.isAuthenticatePending === false) 35 | assert(authState.isLogoutPending === false) 36 | assert(authState.payload === null) 37 | 38 | const request = { strategy: 'local', email: 'test', password: 'test' } 39 | actions.authenticate.call({ $store: store }, request).then(response => { 40 | assert(authState.accessToken === response.accessToken) 41 | assert(authState.errorOnAuthenticate === null) 42 | assert(authState.errorOnLogout === null) 43 | assert(authState.isAuthenticatePending === false) 44 | assert(authState.isLogoutPending === false) 45 | const expectedPayload = { 46 | userId: 0, 47 | exp: 9999999999999 48 | } 49 | assert.deepEqual(authState.payload, expectedPayload) 50 | done() 51 | }) 52 | 53 | // Make sure proper state changes occurred before response 54 | assert(authState.accessToken === null) 55 | assert(authState.errorOnAuthenticate === null) 56 | assert(authState.errorOnLogout === null) 57 | assert(authState.isAuthenticatePending === true) 58 | assert(authState.isLogoutPending === false) 59 | assert(authState.payload === null) 60 | }) 61 | 62 | it('Logout', done => { 63 | const store = new Vuex.Store({ 64 | plugins: [auth()] 65 | }) 66 | feathersClient.use('authentication', { 67 | create(data) { 68 | return Promise.resolve({ accessToken }) 69 | } 70 | }) 71 | 72 | const authState = store.state.auth 73 | const actions = mapActions('auth', ['authenticate', 'logout']) 74 | const request = { strategy: 'local', email: 'test', password: 'test' } 75 | 76 | actions.authenticate.call({ $store: store }, request).then(authResponse => { 77 | actions.logout.call({ $store: store }).then(response => { 78 | assert(authState.accessToken === null) 79 | assert(authState.errorOnAuthenticate === null) 80 | assert(authState.errorOnLogout === null) 81 | assert(authState.isAuthenticatePending === false) 82 | assert(authState.isLogoutPending === false) 83 | assert(authState.payload === null) 84 | done() 85 | }) 86 | }) 87 | }) 88 | 89 | it('Authenticate with userService config option', done => { 90 | feathersClient.use('authentication', { 91 | create(data) { 92 | return Promise.resolve({ accessToken }) 93 | } 94 | }) 95 | feathersClient.use( 96 | 'users', 97 | memory({ store: { 0: { id: 0, email: 'test@test.com' } } }) 98 | ) 99 | const store = new Vuex.Store({ 100 | plugins: [auth({ userService: 'users' }), service('users')] 101 | }) 102 | 103 | const authState = store.state.auth 104 | const actions = mapActions('auth', ['authenticate']) 105 | 106 | assert(authState.user === null) 107 | 108 | const request = { strategy: 'local', email: 'test', password: 'test' } 109 | actions.authenticate 110 | .call({ $store: store }, request) 111 | .then(response => { 112 | const expectedUser = { 113 | id: 0, 114 | email: 'test@test.com' 115 | } 116 | assert.deepEqual(authState.user, expectedUser) 117 | done() 118 | }) 119 | .catch(error => { 120 | assert(!error, error) 121 | done() 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /test/auth-module/auth-module.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { assert } from 'chai' 4 | import feathersVuex from '../../src/index' 5 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' 6 | import Vuex from 'vuex' 7 | import { isEmpty } from 'lodash' 8 | 9 | const { makeAuthPlugin, makeServicePlugin, BaseModel } = feathersVuex( 10 | feathersClient, 11 | { 12 | serverAlias: 'api' 13 | } 14 | ) 15 | interface CustomStore { 16 | state: any 17 | auth: any 18 | authentication?: any 19 | users?: any 20 | } 21 | 22 | function makeContext() { 23 | class User extends BaseModel { 24 | constructor(data, options) { 25 | super(data, options) 26 | } 27 | static modelName = 'User' 28 | static instanceDefaults() { 29 | return { 30 | email: '', 31 | password: '' 32 | } 33 | } 34 | } 35 | const servicePath = 'users' 36 | const usersPlugin = makeServicePlugin({ 37 | Model: User, 38 | service: feathersClient.service(servicePath), 39 | servicePath 40 | }) 41 | 42 | const authPlugin = makeAuthPlugin({ userService: 'users' }) 43 | 44 | const store = new Vuex.Store({ 45 | plugins: [authPlugin, usersPlugin] 46 | }) 47 | 48 | return { User, usersPlugin, authPlugin, BaseModel, store } 49 | } 50 | 51 | describe('Auth Module', () => { 52 | describe('Configuration', () => { 53 | it('has default auth namespace', () => { 54 | const { store } = makeContext() 55 | const authState = Object.assign({}, store.state.auth) 56 | const expectedCustomStore = { 57 | accessToken: null, 58 | entityIdField: 'userId', 59 | errorOnAuthenticate: null, 60 | errorOnLogout: null, 61 | isAuthenticatePending: false, 62 | isLogoutPending: false, 63 | payload: null, 64 | responseEntityField: 'user', 65 | serverAlias: 'api', 66 | user: null, 67 | userService: 'users' 68 | } 69 | 70 | assert.deepEqual(authState, expectedCustomStore, 'has the default state') 71 | }) 72 | 73 | it('can customize the namespace', function() { 74 | const store = new Vuex.Store({ 75 | plugins: [makeAuthPlugin({ namespace: 'authentication' })] 76 | }) 77 | 78 | assert(store.state.authentication, 'the custom namespace was used') 79 | }) 80 | }) 81 | 82 | describe('Customizing Auth Store', function() { 83 | it('allows adding custom state', function() { 84 | const customState = { 85 | test: true, 86 | test2: { 87 | test: true 88 | } 89 | } 90 | const store = new Vuex.Store({ 91 | plugins: [makeAuthPlugin({ state: customState })] 92 | }) 93 | 94 | assert(store.state.auth.test === true, 'added custom state') 95 | assert(store.state.auth.test2.test === true, 'added custom state') 96 | }) 97 | 98 | it('allows custom mutations', function() { 99 | const state = { test: true } 100 | const customMutations = { 101 | setTestToFalse(state) { 102 | state.test = false 103 | } 104 | } 105 | const store = new Vuex.Store({ 106 | plugins: [makeAuthPlugin({ state, mutations: customMutations })] 107 | }) 108 | 109 | store.commit('auth/setTestToFalse') 110 | assert( 111 | store.state.auth.test === false, 112 | 'the custom state was modified by the custom mutation' 113 | ) 114 | }) 115 | 116 | it('has a user && isAuthenticated getter when there is a userService attribute', function() { 117 | const store = new Vuex.Store({ 118 | state: { 119 | state: {}, 120 | auth: {}, 121 | users: { 122 | idField: 'id', 123 | keyedById: { 124 | 1: { 125 | id: 1, 126 | name: 'Marshall' 127 | } 128 | } 129 | } 130 | }, 131 | plugins: [ 132 | makeAuthPlugin({ 133 | state: { 134 | user: { 135 | id: 1 136 | } 137 | }, 138 | userService: 'users' 139 | }) 140 | ] 141 | }) 142 | const user = store.getters['auth/user'] 143 | const isAuthenticated = store.getters['auth/isAuthenticated'] 144 | 145 | assert(user.name === 'Marshall', 'Got the user from the users store.') 146 | assert(isAuthenticated, 'isAuthenticated') 147 | }) 148 | 149 | it('getters show not authenticated when there is no user', function() { 150 | const store = new Vuex.Store({ 151 | state: { 152 | state: {}, 153 | auth: {}, 154 | users: { 155 | idField: 'id', 156 | keyedById: {} 157 | } 158 | }, 159 | plugins: [ 160 | makeAuthPlugin({ 161 | state: {}, 162 | userService: 'users' 163 | }) 164 | ] 165 | }) 166 | const user = store.getters['auth/user'] 167 | const isAuthenticated = store.getters['auth/isAuthenticated'] 168 | 169 | assert(user === null, 'user getter returned null as expected') 170 | assert(!isAuthenticated, 'not authenticated') 171 | }) 172 | 173 | it('allows custom getters', function() { 174 | const customGetters = { 175 | oneTwoThree() { 176 | return 123 177 | } 178 | } 179 | const store = new Vuex.Store({ 180 | plugins: [makeAuthPlugin({ getters: customGetters })] 181 | }) 182 | 183 | assert( 184 | store.getters['auth/oneTwoThree'] === 123, 185 | 'the custom getter was available' 186 | ) 187 | }) 188 | 189 | it('allows adding custom actions', function() { 190 | const config = { 191 | state: { 192 | isTrue: false 193 | }, 194 | mutations: { 195 | setToTrue(state) { 196 | state.isTrue = true 197 | } 198 | }, 199 | actions: { 200 | trigger(context) { 201 | context.commit('setToTrue') 202 | } 203 | } 204 | } 205 | const store = new Vuex.Store({ 206 | plugins: [makeAuthPlugin(config)] 207 | }) 208 | 209 | store.dispatch('auth/trigger') 210 | assert(store.state.auth.isTrue === true, 'the custom action was run') 211 | }) 212 | }) 213 | 214 | it('Calls auth service without params', async function() { 215 | let receivedData = null 216 | let receivedParams = null 217 | feathersClient.use('authentication', { 218 | create(data, params) { 219 | receivedData = data 220 | receivedParams = params 221 | return Promise.resolve({ accessToken: 'jg54jh2gj6fgh734j5h4j25jbh' }) 222 | } 223 | }) 224 | 225 | const { store } = makeContext() 226 | 227 | const request = { strategy: 'local', email: 'test', password: 'test' } 228 | await store.dispatch('auth/authenticate', request) 229 | assert(receivedData, 'got data') 230 | assert(receivedData.strategy === 'local', 'got strategy') 231 | assert(receivedData.email === 'test', 'got email') 232 | assert(receivedData.password === 'test', 'got password') 233 | assert(receivedParams && isEmpty(receivedParams), 'empty params') 234 | }) 235 | 236 | it('Calls auth service with params', async function() { 237 | let receivedParams = null 238 | feathersClient.use('authentication', { 239 | create(data, params) { 240 | receivedParams = params 241 | return Promise.resolve({ accessToken: 'jg54jh2gj6fgh734j5h4j25jbh' }) 242 | } 243 | }) 244 | 245 | const { store } = makeContext() 246 | 247 | const request = { strategy: 'local', email: 'test', password: 'test' } 248 | const customParams = { theAnswer: 42 } 249 | await store.dispatch('auth/authenticate', [request, customParams]) 250 | assert(receivedParams && receivedParams.theAnswer === 42, 'got params') 251 | }) 252 | }) 253 | -------------------------------------------------------------------------------- /test/auth.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import feathersVuexAuth, { reducer } from '../src/auth' 3 | import * as actionTypes from '../src/action-types' 4 | import './server' 5 | import { makeFeathersRestClient } from './feathers-client' 6 | 7 | describe('feathers-vuex:auth', () => { 8 | it('is CommonJS compatible', () => { 9 | assert(typeof require('../lib/auth').default === 'function') 10 | }) 11 | 12 | it('basic functionality', () => { 13 | assert(typeof feathersVuexAuth === 'function', 'It worked') 14 | }) 15 | 16 | it('throws an error if the auth plugin is missing', () => { 17 | const app = {} 18 | const store = {} 19 | const plugin = feathersVuexAuth(store).bind(app) 20 | assert.throws( 21 | plugin, 22 | 'You must first register the @feathersjs/authentication-client plugin' 23 | ) 24 | }) 25 | 26 | it('returns the app, is chainable', () => { 27 | const app = { 28 | authenticate() {} 29 | } 30 | const store = {} 31 | const returnValue = feathersVuexAuth(store).bind(app)() 32 | assert(returnValue === app) 33 | }) 34 | 35 | it('replaces the original authenticate function', () => { 36 | const feathersClient = makeFeathersRestClient() 37 | const oldAuthenticate = feathersClient.authenticate 38 | const store = {} 39 | feathersClient.configure(feathersVuexAuth(store)) 40 | assert(oldAuthenticate !== feathersClient.authenticate) 41 | }) 42 | 43 | it('dispatches actions to the store.', done => { 44 | const feathersClient = makeFeathersRestClient() 45 | const fakeStore = { 46 | dispatch(action) { 47 | switch (action.type) { 48 | case actionTypes.FEATHERS_AUTH_REQUEST: 49 | assert(action.payload.test || action.payload.accessToken) 50 | break 51 | case actionTypes.FEATHERS_AUTH_SUCCESS: 52 | assert(action.data) 53 | break 54 | case actionTypes.FEATHERS_AUTH_FAILURE: 55 | assert(action.error) 56 | done() 57 | break 58 | case actionTypes.FEATHERS_AUTH_LOGOUT: 59 | assert(action) 60 | break 61 | } 62 | } 63 | } 64 | 65 | feathersClient.configure(feathersVuexAuth(fakeStore)) 66 | 67 | try { 68 | feathersClient 69 | .authenticate({ test: true }) 70 | .then(response => { 71 | feathersClient.logout() 72 | return response 73 | }) 74 | .catch(error => { 75 | assert(error.className === 'not-authenticated') 76 | }) 77 | } catch (err) {} 78 | try { 79 | feathersClient.authenticate({ 80 | strategy: 'jwt', 81 | accessToken: 'q34twershtdyfhgmj' 82 | }) 83 | } catch (err) { 84 | // eslint-disable-next-line no-console 85 | console.log(err) 86 | } 87 | }) 88 | }) 89 | 90 | describe('feathers-vuex:auth - Reducer', () => { 91 | it('Has defaults', () => { 92 | const state = undefined 93 | const defaultState = { 94 | isPending: false, 95 | isError: false, 96 | isSignedIn: false, 97 | accessToken: null, 98 | error: undefined 99 | } 100 | const newState = reducer(state, {}) 101 | assert.deepEqual(newState, defaultState) 102 | }) 103 | 104 | it(`Responds to ${actionTypes.FEATHERS_AUTH_REQUEST}`, () => { 105 | const state = undefined 106 | const action = { 107 | type: actionTypes.FEATHERS_AUTH_REQUEST, 108 | payload: { 109 | strategy: 'jwt', 110 | accessToken: 'evh8vq2pj' 111 | } 112 | } 113 | const expectedState = { 114 | isPending: true, 115 | isError: false, 116 | isSignedIn: false, 117 | accessToken: null, 118 | error: undefined 119 | } 120 | const newState = reducer(state, action) 121 | assert.deepEqual(newState, expectedState) 122 | }) 123 | 124 | it(`Responds to ${actionTypes.FEATHERS_AUTH_SUCCESS}`, () => { 125 | const state = undefined 126 | const accessToken = 'evh8vq2pj' 127 | const action = { 128 | type: actionTypes.FEATHERS_AUTH_SUCCESS, 129 | data: { accessToken } 130 | } 131 | const expectedState = { 132 | isPending: false, 133 | isError: false, 134 | isSignedIn: true, 135 | accessToken: accessToken, 136 | error: undefined 137 | } 138 | const newState = reducer(state, action) 139 | assert.deepEqual(newState, expectedState) 140 | }) 141 | 142 | it(`Responds to ${actionTypes.FEATHERS_AUTH_FAILURE}`, () => { 143 | const state = undefined 144 | const error = 'Unauthorized' 145 | const action = { 146 | type: actionTypes.FEATHERS_AUTH_FAILURE, 147 | error 148 | } 149 | const expectedState = { 150 | isPending: false, 151 | isError: true, 152 | isSignedIn: false, 153 | accessToken: null, 154 | error 155 | } 156 | const newState = reducer(state, action) 157 | assert.deepEqual(newState, expectedState) 158 | }) 159 | 160 | it(`Responds to ${actionTypes.FEATHERS_AUTH_LOGOUT}`, () => { 161 | const state = undefined 162 | const action = { 163 | type: actionTypes.FEATHERS_AUTH_LOGOUT 164 | } 165 | const expectedState = { 166 | isPending: false, 167 | isError: false, 168 | isSignedIn: false, 169 | accessToken: null, 170 | error: undefined 171 | } 172 | const newState = reducer(state, action) 173 | assert.deepEqual(newState, expectedState) 174 | }) 175 | }) 176 | -------------------------------------------------------------------------------- /test/fixtures/feathers-client.js: -------------------------------------------------------------------------------- 1 | import feathers from '@feathersjs/feathers' 2 | import socketio from '@feathersjs/socketio-client' 3 | import rest from '@feathersjs/rest-client' 4 | import axios from 'axios' 5 | import auth from '@feathersjs/authentication-client' 6 | import io from 'socket.io-client/dist/socket.io' 7 | import fixtureSocket from 'can-fixture-socket' 8 | 9 | const mockServer = new fixtureSocket.Server(io) 10 | const baseUrl = 'http://localhost:3030' 11 | 12 | // These are fixtures used in the service-modulet.test.js under socket events. 13 | let id = 0 14 | mockServer.on('things::create', function (data, params, cb) { 15 | data.id = id 16 | id++ 17 | mockServer.emit('things created', data) 18 | cb(null, data) 19 | }) 20 | mockServer.on('things::patch', function (id, data, params, cb) { 21 | Object.assign(data, { id, test: true }) 22 | mockServer.emit('things patched', data) 23 | cb(null, data) 24 | }) 25 | mockServer.on('things::update', function (id, data, params, cb) { 26 | Object.assign(data, { id, test: true }) 27 | mockServer.emit('things updated', data) 28 | cb(null, data) 29 | }) 30 | mockServer.on('things::remove', function (id, obj, cb) { 31 | const response = { id, test: true } 32 | mockServer.emit('things removed', response) 33 | cb(null, response) 34 | }) 35 | 36 | let idDebounce = 0 37 | 38 | mockServer.on('things-debounced::create', function (data, obj, cb) { 39 | data.id = idDebounce 40 | idDebounce++ 41 | mockServer.emit('things-debounced created', data) 42 | cb(null, data) 43 | }) 44 | mockServer.on('things-debounced::patch', function (id, data, params, cb) { 45 | Object.assign(data, { id, test: true }) 46 | mockServer.emit('things-debounced patched', data) 47 | cb(null, data) 48 | }) 49 | mockServer.on('things-debounced::update', function (id, data, params, cb) { 50 | Object.assign(data, { id, test: true }) 51 | mockServer.emit('things-debounced updated', data) 52 | cb(null, data) 53 | }) 54 | mockServer.on('things-debounced::remove', function (id, params, cb) { 55 | const response = { id, test: true } 56 | mockServer.emit('things-debounced removed', response) 57 | cb(null, response) 58 | }) 59 | 60 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 61 | export function makeFeathersSocketClient(baseUrl) { 62 | const socket = io(baseUrl) 63 | 64 | return feathers().configure(socketio(socket)).configure(auth()) 65 | } 66 | 67 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 68 | export function makeFeathersRestClient(baseUrl) { 69 | return feathers().configure(rest(baseUrl).axios(axios)).configure(auth()) 70 | } 71 | 72 | const sock = io(baseUrl) 73 | 74 | export const feathersSocketioClient = feathers() 75 | .configure(socketio(sock)) 76 | .configure(auth()) 77 | 78 | export const feathersRestClient = feathers() 79 | .configure(rest(baseUrl).axios(axios)) 80 | .configure(auth()) 81 | -------------------------------------------------------------------------------- /test/fixtures/server.js: -------------------------------------------------------------------------------- 1 | import feathers from '@feathersjs/feathers' 2 | import rest from '@feathersjs/express/rest' 3 | import socketio from '@feathersjs/socketio' 4 | import bodyParser from 'body-parser' 5 | import auth from '@feathersjs/authentication' 6 | import jwt from '@feathersjs/authentication-jwt' 7 | import memory from 'feathers-memory' 8 | 9 | const app = feathers() 10 | .use(bodyParser.json()) 11 | .use(bodyParser.urlencoded({ extended: true })) 12 | .configure(rest()) 13 | .configure(socketio()) 14 | .use('/users', memory()) 15 | .use('/todos', memory()) 16 | .use('/errors', memory()) 17 | .configure( 18 | auth({ 19 | secret: 'test', 20 | service: '/users' 21 | }) 22 | ) 23 | .configure(jwt()) 24 | 25 | app.service('/errors').hooks({ 26 | before: { 27 | all: [ 28 | hook => { 29 | throw new Error(`${hook.method} Denied!`) 30 | } 31 | ] 32 | } 33 | }) 34 | 35 | const port = 3030 36 | const server = app.listen(port) 37 | 38 | process.on('unhandledRejection', (reason, p) => 39 | console.log('Unhandled Rejection at: Promise ', p, reason) 40 | ) 41 | 42 | server.on('listening', () => { 43 | console.log(`Feathers application started on localhost:${port}`) 44 | 45 | setTimeout(function() { 46 | server.close() 47 | }, 50000) 48 | }) 49 | -------------------------------------------------------------------------------- /test/fixtures/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default function makeStore() { 7 | return new Vuex.Store({ 8 | state: { 9 | count: 0 10 | }, 11 | mutations: { 12 | increment(state) { 13 | state.count++ 14 | } 15 | } 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/todos.js: -------------------------------------------------------------------------------- 1 | export function makeTodos() { 2 | return { 3 | 1: { _id: 1, description: 'Dishes', isComplete: true }, 4 | 2: { _id: 2, description: 'Laundry', isComplete: true }, 5 | 3: { _id: 3, description: 'Groceries', isComplete: true } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import * as feathersVuex from '../src/index' 3 | import Vue from 'vue' 4 | import Vuex from 'vuex' 5 | 6 | Vue.use(Vuex) 7 | 8 | describe('feathers-vuex', () => { 9 | it('has correct exports', () => { 10 | assert(typeof feathersVuex.default === 'function') 11 | assert( 12 | typeof feathersVuex.FeathersVuex.install === 'function', 13 | 'has Vue Plugin' 14 | ) 15 | assert(feathersVuex.FeathersVuexFind) 16 | assert(feathersVuex.FeathersVuexGet) 17 | assert(feathersVuex.initAuth) 18 | assert(feathersVuex.makeFindMixin) 19 | assert(feathersVuex.makeGetMixin) 20 | assert(feathersVuex.models) 21 | }) 22 | 23 | it('requires a Feathers Client instance', () => { 24 | try { 25 | feathersVuex.default( 26 | {}, 27 | { 28 | serverAlias: 'index-test' 29 | } 30 | ) 31 | } catch (error) { 32 | assert( 33 | error.message === 34 | 'The first argument to feathersVuex must be a feathers client.' 35 | ) 36 | } 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/make-find-mixin.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { assert } from 'chai' 7 | import jsdom from 'jsdom-global' 8 | import Vue from 'vue/dist/vue' 9 | import Vuex from 'vuex' 10 | import feathersVuex, { FeathersVuex } from '../src/index' 11 | import makeFindMixin from '../src/make-find-mixin' 12 | import { feathersRestClient as feathersClient } from './fixtures/feathers-client' 13 | 14 | jsdom() 15 | require('events').EventEmitter.prototype._maxListeners = 100 16 | 17 | function makeContext() { 18 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { 19 | serverAlias: 'make-find-mixin' 20 | }) 21 | 22 | class FindModel extends BaseModel { 23 | public static modelName = 'FindModel' 24 | public static test = true 25 | } 26 | 27 | return { FindModel, BaseModel, makeServicePlugin } 28 | } 29 | 30 | Vue.use(Vuex) 31 | Vue.use(FeathersVuex) 32 | 33 | describe('Find Mixin', function () { 34 | const { makeServicePlugin, FindModel } = makeContext() 35 | const serviceName = 'todos' 36 | const store = new Vuex.Store({ 37 | plugins: [ 38 | makeServicePlugin({ 39 | Model: FindModel, 40 | service: feathersClient.service(serviceName) 41 | }) 42 | ] 43 | }) 44 | 45 | it('correctly forms mixin data', function () { 46 | const todosMixin = makeFindMixin({ service: 'todos' }) 47 | interface TodosComponent { 48 | todos: [] 49 | todosServiceName: string 50 | isFindTodosPending: boolean 51 | haveTodosBeenRequestedOnce: boolean 52 | haveTodosLoadedOnce: boolean 53 | findTodos: Function 54 | todosLocal: boolean 55 | todosQid: string 56 | todosQueryWhen: Function 57 | todosParams: any 58 | todosFetchParams: any 59 | } 60 | 61 | const vm = new Vue({ 62 | name: 'todos-component', 63 | mixins: [todosMixin], 64 | store, 65 | template: `
` 66 | }).$mount() 67 | 68 | assert.deepEqual(vm.todos, [], 'todos prop was empty array') 69 | assert( 70 | vm.hasOwnProperty('todosPaginationData'), 71 | 'pagination data prop was present, even if undefined' 72 | ) 73 | assert(vm.todosServiceName === 'todos', 'service name was correct') 74 | assert(vm.isFindTodosPending === false, 'loading boolean is in place') 75 | assert( 76 | vm.haveTodosBeenRequestedOnce === false, 77 | 'requested once boolean is in place' 78 | ) 79 | assert(vm.haveTodosLoadedOnce === false, 'loaded once boolean is in place') 80 | assert(typeof vm.findTodos === 'function', 'the find action is in place') 81 | assert(vm.todosLocal === false, 'local boolean is false by default') 82 | assert( 83 | typeof vm.$options.created[0] === 'function', 84 | 'created lifecycle hook function is in place given that local is false' 85 | ) 86 | assert( 87 | vm.todosQid === 'default', 88 | 'the default query identifier is in place' 89 | ) 90 | assert(vm.todosQueryWhen === true, 'the default queryWhen is true') 91 | // assert(vm.todosWatch.length === 0, 'the default watch is an empty array') 92 | assert( 93 | vm.todosParams === undefined, 94 | 'no params are in place by default, must be specified by the user' 95 | ) 96 | assert( 97 | vm.todosFetchParams === undefined, 98 | 'no fetch params are in place by default, must be specified by the user' 99 | ) 100 | }) 101 | 102 | it('correctly forms mixin data for dynamic service', function () { 103 | const tasksMixin = makeFindMixin({ 104 | service() { 105 | return this.serviceName 106 | }, 107 | local: true 108 | }) 109 | 110 | interface TasksComponent { 111 | tasks: [] 112 | serviceServiceName: string 113 | isFindTasksPending: boolean 114 | findTasks: Function 115 | tasksLocal: boolean 116 | tasksQid: string 117 | tasksQueryWhen: Function 118 | tasksParams: any 119 | tasksFetchParams: any 120 | } 121 | 122 | const vm = new Vue({ 123 | name: 'tasks-component', 124 | data: () => ({ 125 | serviceName: 'tasks' 126 | }), 127 | mixins: [tasksMixin], 128 | store, 129 | template: `
` 130 | }).$mount() 131 | 132 | assert.deepEqual(vm.items, [], 'items prop was empty array') 133 | assert( 134 | vm.hasOwnProperty('servicePaginationData'), 135 | 'pagination data prop was present, even if undefined' 136 | ) 137 | assert(vm.serviceServiceName === 'tasks', 'service name was correct') 138 | assert(vm.isFindServicePending === false, 'loading boolean is in place') 139 | assert(typeof vm.findService === 'function', 'the find action is in place') 140 | assert(vm.serviceLocal === true, 'local boolean is set to true') 141 | assert( 142 | typeof vm.$options.created === 'undefined', 143 | 'created lifecycle hook function is NOT in place given that local is true' 144 | ) 145 | assert( 146 | vm.serviceQid === 'default', 147 | 'the default query identifier is in place' 148 | ) 149 | assert(vm.serviceQueryWhen === true, 'the default queryWhen is true') 150 | // assert(vm.tasksWatch.length === 0, 'the default watch is an empty array') 151 | assert( 152 | vm.serviceParams === undefined, 153 | 'no params are in place by default, must be specified by the user' 154 | ) 155 | assert( 156 | vm.serviceFetchParams === undefined, 157 | 'no fetch params are in place by default, must be specified by the user' 158 | ) 159 | }) 160 | }) 161 | -------------------------------------------------------------------------------- /test/service-module/misconfigured-client.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/ban-ts-ignore:0 */ 2 | import { assert } from 'chai' 3 | import feathersVuex from '../../src/index' 4 | import feathers from '@feathersjs/client' 5 | import auth from '@feathersjs/authentication-client' 6 | 7 | // @ts-ignore 8 | const feathersClient = feathers().configure(auth()) 9 | 10 | describe('Service Module - Bad Client Setup', () => { 11 | it('throws an error when no client transport plugin is registered', () => { 12 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { 13 | serverAlias: 'misconfigured' 14 | }) 15 | class MisconfiguredTask extends BaseModel { 16 | public static modelName = 'MisconfiguredTask' 17 | public static test = true 18 | } 19 | 20 | try { 21 | makeServicePlugin({ 22 | Model: MisconfiguredTask, 23 | service: feathersClient.service('misconfigured-todos') 24 | }) 25 | } catch (error) { 26 | assert( 27 | error.message.includes( 28 | '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.' 29 | ), 30 | 'got an error with a misconfigured client' 31 | ) 32 | } 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/service-module/model-base.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { assert } from 'chai' 7 | import Vue from 'vue' 8 | import Vuex from 'vuex' 9 | import { clearModels } from '../../src/service-module/global-models' 10 | import { 11 | feathersRestClient as feathers, 12 | makeFeathersRestClient 13 | } from '../fixtures/feathers-client' 14 | import feathersVuex from '../../src/index' 15 | 16 | Vue.use(Vuex) 17 | 18 | process.setMaxListeners(100) 19 | 20 | describe.skip('Model - Standalone', function () { 21 | it.skip('allows using a model without a service', function () {}) 22 | it.skip('rename serverAlias to just `alias` or maybe `groupName`', function () {}) 23 | }) 24 | 25 | describe('makeModel / BaseModel', function () { 26 | before(() => { 27 | clearModels() 28 | }) 29 | 30 | it('properly sets up the BaseModel', function () { 31 | const alias = 'model-base' 32 | const { BaseModel } = feathersVuex(feathers, { serverAlias: alias }) 33 | const { 34 | name, 35 | store, 36 | namespace, 37 | idField, 38 | preferUpdate, 39 | serverAlias, 40 | models, 41 | copiesById 42 | } = BaseModel 43 | 44 | assert(name === 'BaseModel', 'name in place') 45 | 46 | // Monkey patched onto the Model class in `makeServicePlugin()` 47 | assert(!store, 'no store by default') 48 | assert(!namespace, 'no namespace by default') 49 | 50 | assert(idField === 'id', 'default idField is id') 51 | assert(!preferUpdate, 'prefer fetch by default') 52 | 53 | // Readonly props 54 | assert(serverAlias === 'model-base', 'serverAlias') 55 | assert(models, 'models are available') 56 | assert.equal(Object.keys(copiesById).length, 0, 'copiesById is empty') 57 | 58 | // Static Methods 59 | const staticMethods = [ 60 | 'getId', 61 | 'find', 62 | 'findInStore', 63 | 'count', 64 | 'countInStore', 65 | 'get', 66 | 'getFromStore' 67 | ] 68 | staticMethods.forEach(method => { 69 | assert(typeof BaseModel[method] === 'function', `has ${method} method`) 70 | }) 71 | 72 | // Prototype Methods 73 | const prototypeMethods = [ 74 | 'clone', 75 | 'reset', 76 | 'commit', 77 | 'save', 78 | 'create', 79 | 'patch', 80 | 'update', 81 | 'remove' 82 | ] 83 | prototypeMethods.forEach(method => { 84 | assert( 85 | typeof BaseModel.prototype[method] === 'function', 86 | `has ${method} method` 87 | ) 88 | }) 89 | 90 | // Utility Methods 91 | const utilityMethods = ['hydrateAll'] 92 | utilityMethods.forEach(method => { 93 | assert(typeof BaseModel[method] === 'function', `has ${method} method`) 94 | }) 95 | 96 | const eventMethods = [ 97 | 'on', 98 | 'off', 99 | 'once', 100 | 'emit', 101 | 'addListener', 102 | 'removeListener', 103 | 'removeAllListeners' 104 | ] 105 | eventMethods.forEach(method => { 106 | assert(typeof BaseModel[method] === 'function', `has ${method} method`) 107 | }) 108 | 109 | const getterMethods = [ 110 | 'isCreatePending', 111 | 'isUpdatePending', 112 | 'isPatchPending', 113 | 'isRemovePending', 114 | 'isSavePending', 115 | 'isPending' 116 | ] 117 | const m = new BaseModel() 118 | getterMethods.forEach(method => { 119 | assert( 120 | typeof Object.getOwnPropertyDescriptor(Object.getPrototypeOf(m), method).get === 'function', 121 | `has ${method} getter` 122 | ) 123 | }) 124 | }) 125 | 126 | it('allows customization through the FeathersVuexOptions', function () { 127 | const { BaseModel } = feathersVuex(feathers, { 128 | serverAlias: 'myApi', 129 | idField: '_id', 130 | preferUpdate: true 131 | }) 132 | const { idField, preferUpdate, serverAlias } = BaseModel 133 | 134 | assert(idField === '_id', 'idField was set') 135 | assert(preferUpdate, 'turned on preferUpdate') 136 | assert(serverAlias === 'myApi', 'serverAlias was set') 137 | }) 138 | 139 | it('receives store & other props after Vuex plugin is registered', function () { 140 | const { BaseModel, makeServicePlugin } = feathersVuex(feathers, { 141 | serverAlias: 'myApi' 142 | }) 143 | BaseModel.modelName = 'TestModel' 144 | const plugin = makeServicePlugin({ 145 | servicePath: 'todos', 146 | service: feathers.service('todos'), 147 | Model: BaseModel 148 | }) 149 | new Vuex.Store({ 150 | plugins: [plugin] 151 | }) 152 | const { store, namespace, servicePath } = BaseModel 153 | 154 | assert(store, 'store is in place') 155 | assert.equal(namespace, 'todos', 'namespace is in place') 156 | assert.equal(servicePath, 'todos', 'servicePath is in place') 157 | }) 158 | 159 | it('allows access to other models after Vuex plugins are registered', function () { 160 | const serverAlias = 'model-base' 161 | const { makeServicePlugin, BaseModel, models } = feathersVuex(feathers, { 162 | idField: '_id', 163 | serverAlias 164 | }) 165 | 166 | // Create a Todo Model & Plugin 167 | class Todo extends BaseModel { 168 | public static modelName = 'Todo' 169 | public test = true 170 | } 171 | const todosPlugin = makeServicePlugin({ 172 | servicePath: 'todos', 173 | Model: Todo, 174 | service: feathers.service('todos') 175 | }) 176 | 177 | // Create a Task Model & Plugin 178 | class Task extends BaseModel { 179 | public static modelName = 'Task' 180 | public test = true 181 | } 182 | const tasksPlugin = makeServicePlugin({ 183 | servicePath: 'tasks', 184 | Model: Task, 185 | service: feathers.service('tasks') 186 | }) 187 | 188 | // Register the plugins 189 | new Vuex.Store({ 190 | plugins: [todosPlugin, tasksPlugin] 191 | }) 192 | 193 | assert(models[serverAlias][Todo.name] === Todo) 194 | assert.equal(Todo.models, models, 'models available at Model.models') 195 | assert.equal(Task.models, models, 'models available at Model.models') 196 | }) 197 | 198 | it('works with multiple, independent Feathers servers', function () { 199 | // Create a Todo Model & Plugin on myApi 200 | const feathersMyApi = makeFeathersRestClient('https://api.my-api.com') 201 | const myApi = feathersVuex(feathersMyApi, { 202 | idField: '_id', 203 | serverAlias: 'myApi' 204 | }) 205 | class Todo extends myApi.BaseModel { 206 | public static modelName = 'Todo' 207 | public test = true 208 | } 209 | const todosPlugin = myApi.makeServicePlugin({ 210 | Model: Todo, 211 | service: feathersMyApi.service('todos') 212 | }) 213 | 214 | // Create a Task Model & Plugin on theirApi 215 | const feathersTheirApi = makeFeathersRestClient('https://api.their-api.com') 216 | const theirApi = feathersVuex(feathersTheirApi, { 217 | serverAlias: 'theirApi' 218 | }) 219 | class Task extends theirApi.BaseModel { 220 | public static modelName = 'Task' 221 | public test = true 222 | } 223 | const tasksPlugin = theirApi.makeServicePlugin({ 224 | Model: Task, 225 | service: feathersTheirApi.service('tasks') 226 | }) 227 | 228 | // Register the plugins 229 | new Vuex.Store({ 230 | plugins: [todosPlugin, tasksPlugin] 231 | }) 232 | const { models } = myApi 233 | 234 | assert(models.myApi.Todo === Todo) 235 | assert(!models.theirApi.Todo, `Todo stayed out of the 'theirApi' namespace`) 236 | assert(models.theirApi.Task === Task) 237 | assert(!models.myApi.Task, `Task stayed out of the 'myApi' namespace`) 238 | 239 | assert.equal( 240 | models.myApi.byServicePath[Todo.servicePath], 241 | Todo, 242 | 'also registered in models.byServicePath' 243 | ) 244 | assert.equal( 245 | models.theirApi.byServicePath[Task.servicePath], 246 | Task, 247 | 'also registered in models.byServicePath' 248 | ) 249 | }) 250 | }) 251 | -------------------------------------------------------------------------------- /test/service-module/model-serialize.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { assert } from 'chai' 7 | import feathersVuex from '../../src/index' 8 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' 9 | import { clearModels } from '../../src/service-module/global-models' 10 | import _omit from 'lodash/omit' 11 | import Vuex from 'vuex' 12 | 13 | describe('Models - Serialize', function () { 14 | beforeEach(() => { 15 | clearModels() 16 | }) 17 | 18 | it('allows customizing toJSON', function () { 19 | const { BaseModel, makeServicePlugin } = feathersVuex(feathersClient, { 20 | serverAlias: 'myApi' 21 | }) 22 | 23 | class Task extends BaseModel { 24 | public static modelName = 'Task' 25 | public static instanceDefaults() { 26 | return { 27 | id: null, 28 | description: '', 29 | isComplete: false 30 | } 31 | } 32 | public toJSON() { 33 | return _omit(this, ['isComplete']) 34 | } 35 | public constructor(data, options?) { 36 | super(data, options) 37 | } 38 | } 39 | 40 | const servicePath = 'thingies' 41 | const plugin = makeServicePlugin({ 42 | servicePath: 'thingies', 43 | Model: Task, 44 | service: feathersClient.service(servicePath) 45 | }) 46 | 47 | new Vuex.Store({ plugins: [plugin] }) 48 | 49 | const task = new Task({ 50 | description: 'Hello, World!', 51 | isComplete: true 52 | }) 53 | 54 | assert(!task.toJSON().hasOwnProperty('isComplete'), 'custom toJSON worked') 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/service-module/model-tests.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { assert } from 'chai' 7 | 8 | interface ModelOptions { 9 | servicePath: string 10 | } 11 | 12 | describe('TypeScript Class Inheritance', () => { 13 | it('Can access static instanceDefaults from BaseModel', () => { 14 | abstract class BaseModel { 15 | public static instanceDefaults 16 | public constructor(data, options?) { 17 | const { instanceDefaults } = this.constructor as typeof BaseModel 18 | const defaults = instanceDefaults(data, options) 19 | assert( 20 | defaults.description === 'default description', 21 | 'We get defaults in the BaseModel constructor' 22 | ) 23 | Object.assign(this, defaults, data) 24 | } 25 | } 26 | class Todo extends BaseModel { 27 | public static modelName = 'Todo' 28 | 29 | public description: string 30 | public static instanceDefaults = (data, options) => ({ 31 | description: 'default description' 32 | }) 33 | 34 | public constructor(data, options?) { 35 | super(data, options) 36 | const { instanceDefaults } = this.constructor as typeof BaseModel 37 | const defaults = instanceDefaults(data, options) 38 | assert( 39 | defaults.description === 'default description', 40 | 'We get defaults in the Todo constructor, too' 41 | ) 42 | } 43 | } 44 | 45 | const todo = new Todo({ 46 | test: true 47 | }) 48 | 49 | assert( 50 | todo.description === 'default description', 51 | 'got default description' 52 | ) 53 | }) 54 | 55 | it('Can access static instanceDefaults from two levels of inheritance', () => { 56 | abstract class BaseModel { 57 | public static instanceDefaults 58 | public constructor(data, options?) { 59 | const { instanceDefaults } = this.constructor as typeof BaseModel 60 | const defaults = instanceDefaults(data, options) 61 | assert( 62 | defaults.description === 'default description', 63 | 'We get defaults in the BaseModel constructor' 64 | ) 65 | Object.assign(this, defaults, data) 66 | } 67 | } 68 | 69 | function makeServiceModel(options) { 70 | const { servicePath } = options 71 | 72 | class ServiceModel extends BaseModel { 73 | public static modelName = 'ServiceModel' 74 | public constructor(data, options: ModelOptions = { servicePath: '' }) { 75 | options.servicePath = servicePath 76 | super(data, options) 77 | } 78 | } 79 | return ServiceModel 80 | } 81 | 82 | class Todo extends makeServiceModel({ servicePath: 'todos' }) { 83 | public static modelName = 'Todo' 84 | public description: string 85 | 86 | public static instanceDefaults = (data, options) => ({ 87 | description: 'default description' 88 | }) 89 | } 90 | 91 | const todo = new Todo({ 92 | test: true 93 | }) 94 | 95 | assert( 96 | todo.description === 'default description', 97 | 'got default description' 98 | ) 99 | }) 100 | 101 | it('Can access static servicePath from Todo in BaseModel', () => { 102 | abstract class BaseModel { 103 | public static instanceDefaults 104 | public static servicePath 105 | public static namespace 106 | 107 | public constructor(data, options?) { 108 | const { instanceDefaults, servicePath, namespace } = this 109 | .constructor as typeof BaseModel 110 | const defaults = instanceDefaults(data, options) 111 | assert( 112 | defaults.description === 'default description', 113 | 'We get defaults in the BaseModel constructor' 114 | ) 115 | Object.assign(this, defaults, data, { 116 | _options: { namespace, servicePath } 117 | }) 118 | } 119 | } 120 | 121 | class Todo extends BaseModel { 122 | public static modelName = 'Todo' 123 | public static namespace: string = 'todos' 124 | public static servicePath: string = 'v1/todos' 125 | 126 | public description: string 127 | public _options 128 | 129 | public static instanceDefaults = (data, models) => ({ 130 | description: 'default description' 131 | }) 132 | } 133 | 134 | const todo = new Todo({ 135 | test: true 136 | }) 137 | 138 | assert(todo._options.servicePath === 'v1/todos', 'got static servicePath') 139 | }) 140 | 141 | it('cannot serialize instance methods', () => { 142 | class BaseModel { 143 | public clone() { 144 | return this 145 | } 146 | 147 | public constructor(data) { 148 | Object.assign(this, data) 149 | } 150 | } 151 | 152 | class Todo extends BaseModel { 153 | public static modelName = 'Todo' 154 | public serialize() { 155 | return Object.assign({}, this, { serialized: true }) 156 | } 157 | } 158 | 159 | const todo = new Todo({ name: 'test' }) 160 | const json = JSON.parse(JSON.stringify(todo)) 161 | 162 | assert(!json.clone) 163 | assert(!json.serialize) 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /test/service-module/service-module.reinitialization.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Vuex from 'vuex' 3 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' 4 | import feathersVuex from '../../src/index' 5 | 6 | interface RootState { 7 | todos: any 8 | } 9 | 10 | function makeContext() { 11 | const todoService = feathersClient.service('todos') 12 | const serverAlias = 'reinitialization' 13 | const { makeServicePlugin, BaseModel, models } = feathersVuex( 14 | feathersClient, 15 | { 16 | serverAlias 17 | } 18 | ) 19 | class Todo extends BaseModel { 20 | public static modelName = 'Todo' 21 | } 22 | return { 23 | makeServicePlugin, 24 | BaseModel, 25 | todoService, 26 | Todo, 27 | models, 28 | serverAlias 29 | } 30 | } 31 | 32 | describe('Service Module - Reinitialization', function () { 33 | /** 34 | * Tests that when the make service plugin is reinitialized state 35 | * is reset in the vuex module/model. 36 | * This prevents state pollution in SSR setups. 37 | */ 38 | it('does not preserve module/model state when reinitialized', function () { 39 | const { 40 | makeServicePlugin, 41 | todoService, 42 | Todo, 43 | models, 44 | serverAlias 45 | } = makeContext() 46 | const todosPlugin = makeServicePlugin({ 47 | servicePath: 'todos', 48 | Model: Todo, 49 | service: todoService 50 | }) 51 | let store = new Vuex.Store({ 52 | plugins: [todosPlugin] 53 | }) 54 | let todoState = store.state['todos'] 55 | const virginState = { 56 | addOnUpsert: false, 57 | autoRemove: false, 58 | debug: false, 59 | copiesById: {}, 60 | enableEvents: true, 61 | errorOnCreate: null, 62 | errorOnFind: null, 63 | errorOnGet: null, 64 | errorOnPatch: null, 65 | errorOnRemove: null, 66 | errorOnUpdate: null, 67 | idField: 'id', 68 | tempIdField: '__id', 69 | ids: [], 70 | isCreatePending: false, 71 | isFindPending: false, 72 | isGetPending: false, 73 | isPatchPending: false, 74 | isRemovePending: false, 75 | isUpdatePending: false, 76 | keepCopiesInStore: false, 77 | debounceEventsTime: null, 78 | debounceEventsMaxWait: 1000, 79 | keyedById: {}, 80 | modelName: 'Todo', 81 | nameStyle: 'short', 82 | namespace: 'todos', 83 | pagination: { 84 | defaultLimit: null, 85 | defaultSkip: null 86 | }, 87 | paramsForServer: ['$populateParams'], 88 | preferUpdate: false, 89 | replaceItems: false, 90 | serverAlias, 91 | servicePath: 'todos', 92 | skipRequestIfExists: false, 93 | tempsById: {}, 94 | whitelist: [], 95 | isIdCreatePending: [], 96 | isIdUpdatePending: [], 97 | isIdPatchPending: [], 98 | isIdRemovePending: [], 99 | } 100 | 101 | assert.deepEqual( 102 | todoState, 103 | virginState, 104 | 'vuex module state is correct on first initialization' 105 | ) 106 | assert.deepEqual( 107 | models[serverAlias][Todo.name].store.state[Todo.namespace], 108 | todoState, 109 | 'model state is the same as vuex module state on first initialization' 110 | ) 111 | 112 | // Simulate some mutations on the store. 113 | const todo = { 114 | id: 1, 115 | testProp: true 116 | } 117 | 118 | store.commit('todos/addItem', todo) 119 | const serviceTodo = store.state['todos'].keyedById[1] 120 | 121 | assert.equal( 122 | todo.testProp, 123 | serviceTodo.testProp, 124 | 'todo is added to the store' 125 | ) 126 | 127 | assert.deepEqual( 128 | models[serverAlias][Todo.name].store.state[Todo.namespace], 129 | todoState, 130 | 'model state is the same as vuex module state when store is mutated' 131 | ) 132 | 133 | // Here we are going to simulate the make service plugin being reinitialized. 134 | // This is the default behaviour in SSR setups, e.g. nuxt universal mode, 135 | // although unlikely in SPAs. 136 | store = new Vuex.Store({ 137 | plugins: [todosPlugin] 138 | }) 139 | 140 | todoState = store.state['todos'] 141 | 142 | // We expect vuex module state for this service to be reset. 143 | assert.deepEqual( 144 | todoState, 145 | virginState, 146 | 'store state in vuex module is not preserved on reinitialization' 147 | ) 148 | // We also expect model store state for this service to be reset. 149 | assert.deepEqual( 150 | models[serverAlias][Todo.name].store.state[Todo.namespace], 151 | virginState, 152 | 'store state in service model is not preserved on reinitialization' 153 | ) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /test/service-module/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | export interface ServiceState { 7 | options: {} 8 | ids: (string | number)[] 9 | autoRemove: boolean 10 | errorOnFind: any 11 | errorOnGet: any 12 | errorOnCreate: any 13 | errorOnPatch: any 14 | errorOnUpdate: any 15 | errorOnRemove: any 16 | isFindPending: boolean 17 | isGetPending: boolean 18 | isCreatePending: boolean 19 | isPatchPending: boolean 20 | isUpdatePending: boolean 21 | isRemovePending: boolean 22 | idField: string 23 | keyedById: {} 24 | tempsById: {} 25 | tempsByNewId: {} 26 | whitelist: string[] 27 | paramsForServer: string[] 28 | namespace: string 29 | nameStyle: string // Should be enum of 'short' or 'path' 30 | pagination?: { 31 | default: PaginationState 32 | } 33 | modelName: string 34 | } 35 | 36 | export interface PaginationState { 37 | ids: any 38 | limit: number 39 | skip: number 40 | ip: number 41 | total: number 42 | mostRecent: any 43 | } 44 | 45 | export interface Location { 46 | coordinates: number[] 47 | } 48 | -------------------------------------------------------------------------------- /test/test-utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { assert } from 'chai' 7 | 8 | export function assertGetter(item, prop, value) { 9 | assert( 10 | typeof Object.getOwnPropertyDescriptor(item, prop).get === 'function', 11 | 'getter in place' 12 | ) 13 | assert.equal(item[prop], value, 'returned value matches') 14 | } 15 | 16 | export const makeStore = () => { 17 | return { 18 | 0: { id: 0, description: 'Do the first', isComplete: false }, 19 | 1: { id: 1, description: 'Do the second', isComplete: false }, 20 | 2: { id: 2, description: 'Do the third', isComplete: false }, 21 | 3: { id: 3, description: 'Do the fourth', isComplete: false }, 22 | 4: { id: 4, description: 'Do the fifth', isComplete: false }, 23 | 5: { id: 5, description: 'Do the sixth', isComplete: false }, 24 | 6: { id: 6, description: 'Do the seventh', isComplete: false }, 25 | 7: { id: 7, description: 'Do the eighth', isComplete: false }, 26 | 8: { id: 8, description: 'Do the ninth', isComplete: false }, 27 | 9: { id: 9, description: 'Do the tenth', isComplete: false } 28 | } 29 | } 30 | 31 | export const makeStoreWithAtypicalIds = () => { 32 | return { 33 | 0: { someId: 0, description: 'Do the first', isComplete: false }, 34 | 1: { someId: 1, description: 'Do the second', isComplete: false }, 35 | 2: { someId: 2, description: 'Do the third', isComplete: false }, 36 | 3: { someId: 3, description: 'Do the fourth', isComplete: false }, 37 | 4: { someId: 4, description: 'Do the fifth', isComplete: false }, 38 | 5: { someId: 5, description: 'Do the sixth', isComplete: false }, 39 | 6: { someId: 6, description: 'Do the seventh', isComplete: false }, 40 | 7: { someId: 7, description: 'Do the eighth', isComplete: false }, 41 | 8: { someId: 8, description: 'Do the ninth', isComplete: false }, 42 | 9: { someId: 9, description: 'Do the tenth', isComplete: false } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/use/InstrumentComponent.js: -------------------------------------------------------------------------------- 1 | import useGet from '../../src/useGet' 2 | 3 | export default { 4 | name: 'InstrumentComponent', 5 | template: '
{{ instrument }}
', 6 | props: { 7 | id: { 8 | type: String, 9 | default: '' 10 | } 11 | }, 12 | setup(props, context) { 13 | const { Instrument } = context.root.$FeathersVuex 14 | 15 | const instrumentData = useGet({ model: Instrument, id: props.id }) 16 | 17 | return { 18 | instrument: instrumentData.item 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/use/find.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0, 5 | @typescript-eslint/no-empty-function: 0 6 | */ 7 | import Vue from 'vue' 8 | import VueCompositionApi from '@vue/composition-api' 9 | Vue.use(VueCompositionApi) 10 | 11 | import jsdom from 'jsdom-global' 12 | import { assert } from 'chai' 13 | import feathersVuex, { FeathersVuex } from '../../src/index' 14 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' 15 | import useFind from '../../src/useFind' 16 | import Vuex from 'vuex' 17 | // import { shallowMount } from '@vue/test-utils' 18 | import { computed, isRef } from '@vue/composition-api' 19 | jsdom() 20 | require('events').EventEmitter.prototype._maxListeners = 100 21 | 22 | Vue.use(Vuex) 23 | Vue.use(FeathersVuex) 24 | 25 | function makeContext() { 26 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { 27 | serverAlias: 'useFind' 28 | }) 29 | 30 | class Instrument extends BaseModel { 31 | public static modelName = 'Instrument' 32 | } 33 | 34 | const serviceName = 'things' 35 | const store = new Vuex.Store({ 36 | plugins: [ 37 | makeServicePlugin({ 38 | Model: Instrument, 39 | service: feathersClient.service(serviceName) 40 | }) 41 | ] 42 | }) 43 | return { store, Instrument, BaseModel, makeServicePlugin } 44 | } 45 | 46 | describe('use/find', function () { 47 | it('returns correct default data', function () { 48 | const { Instrument } = makeContext() 49 | 50 | const instrumentParams = computed(() => { 51 | return { 52 | query: {}, 53 | paginate: false 54 | } 55 | }) 56 | const instrumentsData = useFind({ 57 | model: Instrument, 58 | params: instrumentParams 59 | }) 60 | 61 | const { 62 | debounceTime, 63 | error, 64 | haveBeenRequested, 65 | haveLoaded, 66 | isPending, 67 | isLocal, 68 | items, 69 | latestQuery, 70 | paginationData, 71 | qid 72 | } = instrumentsData 73 | 74 | assert(isRef(debounceTime)) 75 | assert(debounceTime.value === null) 76 | 77 | assert(isRef(error)) 78 | assert(error.value === null) 79 | 80 | assert(isRef(haveBeenRequested)) 81 | assert(haveBeenRequested.value === true) 82 | 83 | assert(isRef(haveLoaded)) 84 | assert(haveLoaded.value === false) 85 | 86 | assert(isRef(isPending)) 87 | assert(isPending.value === true) 88 | 89 | assert(isRef(isLocal)) 90 | assert(isLocal.value === false) 91 | 92 | assert(isRef(items)) 93 | assert(Array.isArray(items.value)) 94 | assert(items.value.length === 0) 95 | 96 | assert(isRef(latestQuery)) 97 | assert(latestQuery.value === null) 98 | 99 | assert(isRef(paginationData)) 100 | assert.deepStrictEqual(paginationData.value, { 101 | defaultLimit: null, 102 | defaultSkip: null 103 | }) 104 | 105 | assert(isRef(qid)) 106 | assert(qid.value === 'default') 107 | }) 108 | 109 | it.skip('returns correct default data even when params is not reactive', function () { 110 | const { Instrument } = makeContext() 111 | 112 | const instrumentsData = useFind({ 113 | model: Instrument, 114 | params: { 115 | query: {}, 116 | paginate: false 117 | } 118 | }) 119 | 120 | const { 121 | debounceTime, 122 | error, 123 | haveBeenRequested, 124 | haveLoaded, 125 | isPending, 126 | isLocal, 127 | items, 128 | latestQuery, 129 | paginationData, 130 | qid 131 | } = instrumentsData 132 | 133 | assert(isRef(debounceTime)) 134 | assert(debounceTime.value === null) 135 | 136 | assert(isRef(error)) 137 | assert(error.value === null) 138 | 139 | assert(isRef(haveBeenRequested)) 140 | assert(haveBeenRequested.value === true) 141 | 142 | assert(isRef(haveLoaded)) 143 | assert(haveLoaded.value === false) 144 | 145 | assert(isRef(isPending)) 146 | assert(isPending.value === true) 147 | 148 | assert(isRef(isLocal)) 149 | assert(isLocal.value === false) 150 | 151 | assert(isRef(items)) 152 | assert(Array.isArray(items.value)) 153 | assert(items.value.length === 0) 154 | 155 | assert(isRef(latestQuery)) 156 | assert(latestQuery.value === null) 157 | 158 | assert(isRef(paginationData)) 159 | assert.deepStrictEqual(paginationData.value, { 160 | defaultLimit: null, 161 | defaultSkip: null 162 | }) 163 | 164 | assert(isRef(qid)) 165 | assert(qid.value === 'default') 166 | }) 167 | 168 | it('allows passing {immediate:false} to not query immediately', function () { 169 | const { Instrument } = makeContext() 170 | 171 | const instrumentParams = computed(() => { 172 | return { 173 | query: {}, 174 | paginate: false 175 | } 176 | }) 177 | const instrumentsData = useFind({ 178 | model: Instrument, 179 | params: instrumentParams, 180 | immediate: false 181 | }) 182 | const { haveBeenRequested } = instrumentsData 183 | 184 | assert(isRef(haveBeenRequested)) 185 | assert(haveBeenRequested.value === false) 186 | }) 187 | 188 | it('params can return null to prevent the query', function () { 189 | const { Instrument } = makeContext() 190 | 191 | const instrumentParams = computed(() => { 192 | return null 193 | }) 194 | const instrumentsData = useFind({ 195 | model: Instrument, 196 | params: instrumentParams, 197 | immediate: true 198 | }) 199 | const { haveBeenRequested } = instrumentsData 200 | 201 | assert(isRef(haveBeenRequested)) 202 | assert(haveBeenRequested.value === false) 203 | }) 204 | 205 | it('allows using `local: true` to prevent API calls from being made', function () { 206 | const { Instrument } = makeContext() 207 | 208 | const instrumentParams = computed(() => { 209 | return { 210 | query: {} 211 | } 212 | }) 213 | const instrumentsData = useFind({ 214 | model: Instrument, 215 | params: instrumentParams, 216 | local: true 217 | }) 218 | const { haveBeenRequested, find } = instrumentsData 219 | 220 | assert(isRef(haveBeenRequested)) 221 | assert(haveBeenRequested.value === false, 'no request during init') 222 | 223 | find() 224 | 225 | assert(haveBeenRequested.value === false, 'no request after find') 226 | }) 227 | }) 228 | -------------------------------------------------------------------------------- /test/use/get.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0, 5 | @typescript-eslint/no-empty-function: 0 6 | */ 7 | import Vue from 'vue' 8 | import VueCompositionApi from '@vue/composition-api' 9 | Vue.use(VueCompositionApi) 10 | 11 | import jsdom from 'jsdom-global' 12 | import { assert } from 'chai' 13 | import feathersVuex, { FeathersVuex } from '../../src/index' 14 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client' 15 | import useGet from '../../src/useGet' 16 | import memory from 'feathers-memory' 17 | import Vuex from 'vuex' 18 | // import { mount, shallowMount } from '@vue/test-utils' 19 | // import InstrumentComponent from './InstrumentComponent' 20 | import { isRef } from '@vue/composition-api' 21 | import { HookContext } from '@feathersjs/feathers' 22 | jsdom() 23 | require('events').EventEmitter.prototype._maxListeners = 100 24 | 25 | Vue.use(Vuex) 26 | Vue.use(FeathersVuex) 27 | 28 | // function timeoutPromise(wait = 0) { 29 | // return new Promise(resolve => { 30 | // setTimeout(() => { 31 | // resolve() 32 | // }, wait) 33 | // }) 34 | // } 35 | 36 | function makeContext() { 37 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { 38 | serverAlias: 'useGet' 39 | }) 40 | 41 | class Instrument extends BaseModel { 42 | public constructor(data, options?) { 43 | super(data, options) 44 | } 45 | public static modelName = 'Instrument' 46 | public static instanceDefaults(data) { 47 | return { 48 | name: '' 49 | } 50 | } 51 | } 52 | 53 | feathersClient.use( 54 | 'things', 55 | memory({ 56 | store: { 57 | 0: { id: 0, name: 'trumpet' }, 58 | 1: { id: 1, name: 'trombone' } 59 | }, 60 | paginate: { 61 | default: 10, 62 | max: 50 63 | } 64 | }) 65 | ) 66 | 67 | const servicePath = 'instruments' 68 | const store = new Vuex.Store({ 69 | plugins: [ 70 | makeServicePlugin({ 71 | Model: Instrument, 72 | servicePath, 73 | service: feathersClient.service(servicePath) 74 | }) 75 | ] 76 | }) 77 | return { store, Instrument, BaseModel, makeServicePlugin } 78 | } 79 | 80 | describe('use/get', function () { 81 | it('returns correct default data', function () { 82 | const { Instrument } = makeContext() 83 | 84 | const id = 1 85 | 86 | const existing = Instrument.getFromStore(id) 87 | assert(!existing, 'the current instrument is not in the store.') 88 | 89 | const instrumentData = useGet({ model: Instrument, id }) 90 | 91 | const { 92 | error, 93 | hasBeenRequested, 94 | hasLoaded, 95 | isPending, 96 | isLocal, 97 | item 98 | } = instrumentData 99 | 100 | assert(isRef(error)) 101 | assert(error.value === null) 102 | 103 | assert(isRef(hasBeenRequested)) 104 | assert(hasBeenRequested.value === true) 105 | 106 | assert(isRef(hasLoaded)) 107 | assert(hasLoaded.value === false) 108 | 109 | assert(isRef(isPending)) 110 | assert(isPending.value === true) 111 | 112 | assert(isRef(isLocal)) 113 | assert(isLocal.value === false) 114 | 115 | assert(isRef(item)) 116 | assert(item.value === null) 117 | }) 118 | 119 | it('allows passing {immediate:false} to not query immediately', function () { 120 | const { Instrument } = makeContext() 121 | 122 | const id = 1 123 | const instrumentData = useGet({ model: Instrument, id, immediate: false }) 124 | const { hasBeenRequested } = instrumentData 125 | 126 | assert(isRef(hasBeenRequested)) 127 | assert(hasBeenRequested.value === false) 128 | }) 129 | 130 | it('id can return null id to prevent the query', function () { 131 | const { Instrument } = makeContext() 132 | 133 | const id = null 134 | const instrumentData = useGet({ model: Instrument, id }) 135 | const { hasBeenRequested } = instrumentData 136 | 137 | assert(isRef(hasBeenRequested)) 138 | assert(hasBeenRequested.value === false) 139 | }) 140 | 141 | it('allows using `local: true` to prevent API calls from being made', function () { 142 | const { Instrument } = makeContext() 143 | 144 | const id = 1 145 | const instrumentData = useGet({ model: Instrument, id, local: true }) 146 | const { hasBeenRequested, get } = instrumentData 147 | 148 | assert(isRef(hasBeenRequested)) 149 | assert(hasBeenRequested.value === false, 'no request during init') 150 | 151 | get(id) 152 | 153 | assert(hasBeenRequested.value === false, 'no request after get') 154 | }) 155 | 156 | it('API only hit once on initial render', async function () { 157 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { 158 | serverAlias: 'useGet' 159 | }) 160 | 161 | class Dohickey extends BaseModel { 162 | public static modelName = 'Dohickey' 163 | } 164 | 165 | const servicePath = 'dohickies' 166 | const store = new Vuex.Store({ 167 | plugins: [ 168 | makeServicePlugin({ 169 | Model: Dohickey, 170 | servicePath, 171 | service: feathersClient.service(servicePath) 172 | }) 173 | ] 174 | }) 175 | 176 | let getCalls = 0 177 | feathersClient.service(servicePath).hooks({ 178 | before: { 179 | get: [ 180 | (ctx: HookContext) => { 181 | getCalls += 1 182 | ctx.result = { id: ctx.id } 183 | } 184 | ] 185 | } 186 | }) 187 | 188 | useGet({ model: Dohickey, id: 42 }) 189 | await new Promise((resolve) => setTimeout(resolve, 100)) 190 | 191 | assert(getCalls === 1, '`get` called once') 192 | }) 193 | }) 194 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import { AuthState } from '../src/auth-module/types' 3 | import { ServiceState } from './service-module/types' 4 | import { isNode, isBrowser } from '../src/utils' 5 | import { diff as deepDiff } from 'deep-object-diff' 6 | import { 7 | getId, 8 | initAuth, 9 | hydrateApi, 10 | getServicePrefix, 11 | getServiceCapitalization, 12 | getQueryInfo 13 | } from '../src/utils' 14 | import feathersVuex from '../src/index' 15 | import { feathersSocketioClient as feathersClient } from './fixtures/feathers-client' 16 | import Vue from 'vue' 17 | import Vuex from 'vuex' 18 | 19 | Vue.use(Vuex) 20 | 21 | interface RootState { 22 | auth: AuthState 23 | users: ServiceState 24 | } 25 | 26 | describe('Utils', function () { 27 | describe('getId', () => { 28 | const idField = '_id' 29 | it('converts objects to strings', () => { 30 | const _id = { test: true } 31 | const id = getId({ _id }, idField) 32 | assert.strictEqual(typeof id, 'string') 33 | assert.strictEqual(id, _id.toString()) 34 | }) 35 | it('does not convert number ids', () => { 36 | const _id = 1 37 | const id = getId({ _id }, idField) 38 | assert.strictEqual(typeof id, 'number') 39 | assert.strictEqual(id, _id) 40 | }) 41 | it('automatically finds _id', () => { 42 | const _id = 1 43 | const id = getId({ _id }) 44 | assert.strictEqual(id, _id) 45 | }) 46 | it('automatically finds id', () => { 47 | const referenceId = 1 48 | const id = getId({ id: referenceId }) 49 | assert.strictEqual(id, referenceId) 50 | }) 51 | it('prefers id over _id (only due to their order in the code)', () => { 52 | const _id = 1 53 | const referenceId = 2 54 | const id = getId({ _id, id: referenceId }) 55 | assert.strictEqual(id, referenceId) 56 | }) 57 | }) 58 | 59 | describe('Auth & SSR', () => { 60 | before(function () { 61 | const { 62 | makeServicePlugin, 63 | makeAuthPlugin, 64 | BaseModel 65 | } = feathersVuex(feathersClient, { serverAlias: 'utils' }) 66 | 67 | class User extends BaseModel { 68 | public static modelName = 'User' 69 | public static test = true 70 | } 71 | 72 | Object.assign(this, { 73 | makeServicePlugin, 74 | makeAuthPlugin, 75 | BaseModel, 76 | User 77 | }) 78 | }) 79 | it('properly populates auth', function () { 80 | const store = new Vuex.Store({ 81 | plugins: [ 82 | this.makeServicePlugin({ 83 | Model: this.User, 84 | servicePath: 'users', 85 | service: feathersClient.service('users') 86 | }), 87 | this.makeAuthPlugin({}) 88 | ] 89 | }) 90 | const accessToken = 91 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoiOTk5OTk5OTk5OTkiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.lUlEd3xH-TnlNRbKM3jnDVTNoIg10zgzaS6QyFZE-6g' 92 | const req = { 93 | headers: { 94 | cookie: 'feathers-jwt=' + accessToken 95 | } 96 | } 97 | return initAuth({ 98 | commit: store.commit, 99 | req, 100 | moduleName: 'auth', 101 | cookieName: 'feathers-jwt', 102 | feathersClient 103 | }) 104 | .then(() => { 105 | assert( 106 | store.state.auth.accessToken === accessToken, 107 | 'the token was in place' 108 | ) 109 | assert(store.state.auth.payload, 'the payload was set') 110 | return feathersClient.authentication.getAccessToken() 111 | }) 112 | .then(token => { 113 | assert.isDefined(token, 'the feathers client storage was set') 114 | }) 115 | }) 116 | 117 | it('properly hydrate SSR store', function () { 118 | const { 119 | makeServicePlugin, 120 | BaseModel, 121 | models 122 | } = feathersVuex(feathersClient, { serverAlias: 'hydrate' }) 123 | 124 | class User extends BaseModel { 125 | public static modelName = 'User' 126 | public static test = true 127 | } 128 | 129 | const store = new Vuex.Store({ 130 | plugins: [ 131 | makeServicePlugin({ 132 | Model: User, 133 | servicePath: 'users', 134 | service: feathersClient.service('users'), 135 | mutations: { 136 | addServerItem(state) { 137 | state.keyedById['abcdefg'] = { id: 'abcdefg', name: 'Guzz' } 138 | } 139 | } 140 | }) 141 | ] 142 | }) 143 | store.commit('users/addServerItem') 144 | assert(store.state.users.keyedById['abcdefg'], 'server document added') 145 | assert( 146 | store.state.users.keyedById['abcdefg'] instanceof Object, 147 | 'server document is pure javascript object' 148 | ) 149 | hydrateApi({ api: models.hydrate }) 150 | assert( 151 | store.state.users.keyedById['abcdefg'] instanceof User, 152 | 'document hydrated' 153 | ) 154 | }) 155 | }) 156 | 157 | describe('Inflections', function () { 158 | it('properly inflects the service prefix', function () { 159 | const decisionTable = [ 160 | ['todos', 'todos'], 161 | ['TODOS', 'tODOS'], 162 | ['environment-Panos', 'environmentPanos'], 163 | ['env-panos', 'envPanos'], 164 | ['envPanos', 'envPanos'], 165 | ['api/v1/env-panos', 'envPanos'], 166 | ['very-long-service', 'veryLongService'] 167 | ] 168 | decisionTable.forEach(([path, prefix]) => { 169 | assert( 170 | getServicePrefix(path) === prefix, 171 | `The service prefix for path "${path}" was "${getServicePrefix( 172 | path 173 | )}", expected "${prefix}"` 174 | ) 175 | }) 176 | }) 177 | 178 | it('properly inflects the service capitalization', function () { 179 | const decisionTable = [ 180 | ['todos', 'Todos'], 181 | ['TODOS', 'TODOS'], 182 | ['environment-Panos', 'EnvironmentPanos'], 183 | ['env-panos', 'EnvPanos'], 184 | ['envPanos', 'EnvPanos'], 185 | ['api/v1/env-panos', 'EnvPanos'], 186 | ['very-long-service', 'VeryLongService'] 187 | ] 188 | decisionTable.forEach(([path, prefix]) => { 189 | assert( 190 | getServiceCapitalization(path) === prefix, 191 | `The service prefix for path "${path}" was "${getServiceCapitalization( 192 | path 193 | )}", expected "${prefix}"` 194 | ) 195 | }) 196 | }) 197 | }) 198 | 199 | describe('Environments', () => { 200 | it('sets isNode to true', () => { 201 | assert(isNode, 'isNode was true') 202 | }) 203 | 204 | it('sets isBrowser to false', () => { 205 | assert(!isBrowser, 'isBrowser was false') 206 | }) 207 | }) 208 | }) 209 | 210 | describe('Pagination', function () { 211 | it('getQueryInfo', function () { 212 | const params = { 213 | qid: 'main-list', 214 | query: { 215 | test: true, 216 | $limit: 10, 217 | $skip: 0 218 | } 219 | } 220 | const response = { 221 | data: [], 222 | limit: 10, 223 | skip: 0, 224 | total: 500 225 | } 226 | const info = getQueryInfo(params, response) 227 | const expected = { 228 | isOutdated: undefined, 229 | qid: 'main-list', 230 | query: { 231 | test: true, 232 | $limit: 10, 233 | $skip: 0 234 | }, 235 | queryId: '{"test":true}', 236 | queryParams: { 237 | test: true 238 | }, 239 | pageParams: { 240 | $limit: 10, 241 | $skip: 0 242 | }, 243 | pageId: '{"$limit":10,"$skip":0}', 244 | response: undefined 245 | } 246 | const diff = deepDiff(info, expected) 247 | 248 | assert.deepEqual(info, expected, 'query info formatted correctly') 249 | }) 250 | 251 | it('getQueryInfo no limit or skip', function () { 252 | const params = { 253 | qid: 'main-list', 254 | query: { 255 | test: true 256 | } 257 | } 258 | const response = { 259 | data: [], 260 | limit: 10, 261 | skip: 0, 262 | total: 500 263 | } 264 | const info = getQueryInfo(params, response) 265 | const expected = { 266 | isOutdated: undefined, 267 | qid: 'main-list', 268 | query: { 269 | test: true 270 | }, 271 | queryId: '{"test":true}', 272 | queryParams: { 273 | test: true 274 | }, 275 | pageParams: { 276 | $limit: 10, 277 | $skip: 0 278 | }, 279 | pageId: '{"$limit":10,"$skip":0}', 280 | response: undefined 281 | } 282 | const diff = deepDiff(info, expected) 283 | 284 | assert.deepEqual(info, expected, 'query info formatted correctly') 285 | }) 286 | }) 287 | -------------------------------------------------------------------------------- /test/vue-plugin.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | eslint 3 | @typescript-eslint/explicit-function-return-type: 0, 4 | @typescript-eslint/no-explicit-any: 0 5 | */ 6 | import { assert } from 'chai' 7 | import feathersVuex, { FeathersVuex } from '../src/index' 8 | import { feathersRestClient as feathersClient } from './fixtures/feathers-client' 9 | import Vue from 'vue/dist/vue' 10 | import Vuex from 'vuex' 11 | 12 | // @ts-ignore 13 | Vue.use(Vuex) 14 | // @ts-ignore 15 | Vue.use(FeathersVuex) 16 | 17 | interface VueWithFeathers { 18 | $FeathersVuex: {} 19 | } 20 | 21 | function makeContext() { 22 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, { 23 | serverAlias: 'make-find-mixin' 24 | }) 25 | class FindModel extends BaseModel { 26 | public static modelName = 'FindModel' 27 | public static test: boolean = true 28 | } 29 | 30 | const serviceName = 'todos' 31 | const store = new Vuex.Store({ 32 | plugins: [ 33 | makeServicePlugin({ 34 | Model: FindModel, 35 | service: feathersClient.service(serviceName) 36 | }) 37 | ] 38 | }) 39 | return { 40 | store 41 | } 42 | } 43 | 44 | describe('Vue Plugin', function () { 45 | it('Adds the `$FeathersVuex` object to components', function () { 46 | const { store } = makeContext() 47 | const vm = new Vue({ 48 | name: 'todos-component', 49 | store, 50 | template: `
` 51 | }).$mount() 52 | 53 | assert(vm.$FeathersVuex, 'registeredPlugin correctly') 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "outDir": "dist", 6 | "moduleResolution": "node", 7 | "target": "es6", 8 | "sourceMap": false, 9 | "declaration": true 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.test.js"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "outDir": "dist", 6 | "moduleResolution": "node", 7 | "target": "esnext", 8 | "sourceMap": true, 9 | "allowJs": true 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.test.js"] 13 | } 14 | --------------------------------------------------------------------------------