├── stories
├── .npmignore
├── FeathersVuexFormWrapper.stories.js
└── FeathersVuexInputWrapper.stories.js
├── mocha.opts
├── .github
├── FUNDING.yml
├── issue_template.md
├── pull_request_template.md
└── contributing.md
├── service-logo.png
├── .npmignore
├── docs
├── .vuepress
│ ├── public
│ │ ├── favicon.ico
│ │ └── img
│ │ │ └── devtools.jpg
│ └── config.js
├── index.md
├── feathervuex-in-vuejs3-setup.md
├── api-overview.md
├── auth-plugin.md
├── example-applications.md
├── vue-plugin.md
└── 3.0-major-release.md
├── .babelrc
├── .travis.yml
├── test
├── fixtures
│ ├── todos.js
│ ├── store.js
│ ├── server.js
│ └── feathers-client.js
├── use
│ ├── InstrumentComponent.js
│ ├── get.test.ts
│ └── find.test.ts
├── index.test.ts
├── service-module
│ ├── types.ts
│ ├── misconfigured-client.test.ts
│ ├── model-serialize.test.ts
│ ├── service-module.reinitialization.test.ts
│ ├── model-tests.test.ts
│ └── model-base.test.ts
├── vue-plugin.test.ts
├── test-utils.ts
├── auth-module
│ ├── actions.test.js
│ └── auth-module.test.ts
├── auth.test.js
├── make-find-mixin.test.ts
└── utils.test.ts
├── .editorconfig
├── .istanbul.yml
├── tsconfig.json
├── tsconfig.test.json
├── src
├── auth-module
│ ├── types.ts
│ ├── auth-module.getters.ts
│ ├── auth-module.state.ts
│ ├── auth-module.mutations.ts
│ ├── make-auth-plugin.ts
│ └── auth-module.actions.ts
├── service-module
│ ├── global-clients.ts
│ ├── make-service-module.ts
│ ├── global-models.ts
│ ├── service-module.events.ts
│ ├── service-module.state.ts
│ ├── service-module.getters.ts
│ └── make-service-plugin.ts
├── vue-plugin
│ └── vue-plugin.ts
├── FeathersVuexInputWrapper.ts
├── FeathersVuexFormWrapper.ts
├── FeathersVuexCount.ts
├── FeathersVuexPagination.ts
├── useGet.ts
├── index.ts
├── FeathersVuexFind.ts
├── useFind.ts
├── FeathersVuexGet.ts
└── make-get-mixin.ts
├── .vscode
├── settings.json
└── launch.json
├── .gitignore
├── LICENSE
├── README.md
├── notes.old.md
├── package.json
└── CHANGELOG.md
/stories/.npmignore:
--------------------------------------------------------------------------------
1 | *.stories.js
--------------------------------------------------------------------------------
/mocha.opts:
--------------------------------------------------------------------------------
1 | --compilers js:babel-core/register
2 | test/node.test.js
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: marshallswain
4 |
--------------------------------------------------------------------------------
/service-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-vuex/HEAD/service-logo.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .jshintrc
3 | .travis.yml
4 | .istanbul.yml
5 | .babelrc
6 | .idea/
7 | test/
8 | !lib/
9 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-vuex/HEAD/docs/.vuepress/public/favicon.ico
--------------------------------------------------------------------------------
/docs/.vuepress/public/img/devtools.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-vuex/HEAD/docs/.vuepress/public/img/devtools.jpg
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [ "add-module-exports" ],
3 | "presets": [
4 | [ "env", { "modules": false } ],
5 | "es2015",
6 | "stage-2"
7 | ]
8 | }
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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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]
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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**:
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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:
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
33 |
41 |
42 |
43 |
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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Feathers-Vuex
2 |
3 | [](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex)
4 | [](https://david-dm.org/feathersjs-ecosystem/feathers-vuex)
5 | [](https://www.npmjs.com/package/feathers-vuex)
6 | [](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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/docs/api-overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: API Overview
3 | sidebarDepth: 3
4 | ---
5 |
6 |
7 | [](https://travis-ci.org/feathersjs-ecosystem/feathers-vuex)
8 | [](https://david-dm.org/feathersjs-ecosystem/feathers-vuex)
9 | [](https://www.npmjs.com/package/feathers-vuex)
10 |
11 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
35 | handler(e, save)"
40 | />
41 |
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 |
66 | handler(e, save)"
71 | class="bg-gray-200 rounded"
72 | />
73 |
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 |
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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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