(id) ? id.value : id
99 | const paramsToUse = isRef(params) ? params.value : params
100 |
101 | if (idToUse != null && queryWhen.value && !state.isLocal) {
102 | state.isPending = true
103 | state.hasBeenRequested = true
104 |
105 | const promise =
106 | paramsToUse != null
107 | ? model.get(idToUse, paramsToUse)
108 | : model.get(idToUse)
109 |
110 | return promise
111 | .then(response => {
112 | state.isPending = false
113 | state.hasLoaded = true
114 | return response
115 | })
116 | .catch(error => {
117 | state.isPending = false
118 | state.error = error
119 | return error
120 | })
121 | } else {
122 | return Promise.resolve(undefined)
123 | }
124 | }
125 |
126 | watch(
127 | [() => getId(), () => getParams()],
128 | ([id, params]) => {
129 | get(id as string | number, params as Params)
130 | },
131 | { immediate }
132 | )
133 |
134 | return {
135 | ...toRefs(state),
136 | ...computes,
137 | get
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/vue-plugin/vue-plugin.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0
5 | */
6 | import FeathersVuexFind from '../FeathersVuexFind'
7 | import FeathersVuexGet from '../FeathersVuexGet'
8 | import FeathersVuexFormWrapper from '../FeathersVuexFormWrapper'
9 | import FeathersVuexInputWrapper from '../FeathersVuexInputWrapper'
10 | import FeathersVuexPagination from '../FeathersVuexPagination'
11 | import FeathersVuexCount from '../FeathersVuexCount'
12 | import { globalModels } from '../service-module/global-models'
13 | import { GlobalModels } from '../service-module/types'
14 |
15 | // Augment global models onto VueConstructor and instance
16 | declare module 'vue/types/vue' {
17 | interface VueConstructor {
18 | $FeathersVuex: GlobalModels
19 | }
20 | interface Vue {
21 | $FeathersVuex: GlobalModels
22 | }
23 | }
24 |
25 | export const FeathersVuex = {
26 | install(Vue, options = { components: true }) {
27 | const shouldSetupComponents = options.components !== false
28 |
29 | Vue.$FeathersVuex = globalModels
30 | Vue.prototype.$FeathersVuex = globalModels
31 |
32 | if (shouldSetupComponents) {
33 | Vue.component('FeathersVuexFind', FeathersVuexFind)
34 | Vue.component('FeathersVuexGet', FeathersVuexGet)
35 | Vue.component('FeathersVuexFormWrapper', FeathersVuexFormWrapper)
36 | Vue.component('FeathersVuexInputWrapper', FeathersVuexInputWrapper)
37 | Vue.component('FeathersVuexPagination', FeathersVuexPagination)
38 | Vue.component('FeathersVuexCount', FeathersVuexCount)
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/stories/.npmignore:
--------------------------------------------------------------------------------
1 | *.stories.js
--------------------------------------------------------------------------------
/stories/FeathersVuexFormWrapper.stories.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */
2 | import '../../assets/styles/tailwind.postcss'
3 |
4 | import FeathersVuexFormWrapper from '../src/FeathersVuexFormWrapper'
5 | import Readme from './README.md'
6 |
7 | import store from '../../store/store.dev'
8 | import { models } from 'feathers-vuex'
9 |
10 | export default {
11 | title: 'FeathersVuexFormWrapper',
12 | parameters: {
13 | component: FeathersVuexFormWrapper,
14 | readme: {
15 | sidebar: Readme
16 | }
17 | }
18 | }
19 |
20 | export const Basic = () => ({
21 | components: { FeathersVuexFormWrapper },
22 | data: () => ({
23 | date: null,
24 | UserModel: models.api.User
25 | }),
26 | store,
27 | template: `
28 |
32 |
33 |
41 |
42 |
43 |
44 |
`
45 | })
46 |
--------------------------------------------------------------------------------
/stories/FeathersVuexInputWrapper.stories.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */
2 | import FeathersVuexInputWrapper from '../src/FeathersVuexInputWrapper.vue'
3 | import { makeModel } from '@rovit/test-model'
4 |
5 | const User = makeModel()
6 |
7 | const user = new User({
8 | _id: 1,
9 | email: 'marshall@rovit.com',
10 | carColor: '#FFF'
11 | })
12 |
13 | export default {
14 | title: 'FeathersVuexInputWrapper',
15 | component: FeathersVuexInputWrapper
16 | }
17 |
18 | export const basic = () => ({
19 | components: {
20 | FeathersVuexInputWrapper
21 | },
22 | data: () => ({
23 | user
24 | }),
25 | methods: {
26 | save({ clone, data }) {
27 | const user = clone.commit()
28 | user.patch(data)
29 | }
30 | },
31 | template: `
32 |
33 |
34 |
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 |
--------------------------------------------------------------------------------
/test/auth-module/actions.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'chai/chai'
2 | import setupVuexAuth from '~/src/auth-module/auth-module'
3 | import setupVuexService from '~/src/service-module/service-module'
4 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client'
5 | import Vuex, { mapActions } from 'vuex'
6 | import memory from 'feathers-memory'
7 |
8 | const options = {}
9 | const globalModels = {}
10 |
11 | const auth = setupVuexAuth(feathersClient, options, globalModels)
12 | const service = setupVuexService(feathersClient, options, globalModels)
13 |
14 | const accessToken =
15 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjAsImV4cCI6OTk5OTk5OTk5OTk5OX0.zmvEm8w142xGI7CbUsnvVGZk_hrVE1KEjzDt80LSW50'
16 |
17 | describe('Auth Module - Actions', () => {
18 | it('Authenticate', done => {
19 | const store = new Vuex.Store({
20 | plugins: [auth()]
21 | })
22 | feathersClient.use('authentication', {
23 | create(data) {
24 | return Promise.resolve({ accessToken })
25 | }
26 | })
27 |
28 | const authState = store.state.auth
29 | const actions = mapActions('auth', ['authenticate'])
30 |
31 | assert(authState.accessToken === null)
32 | assert(authState.errorOnAuthenticate === null)
33 | assert(authState.errorOnLogout === null)
34 | assert(authState.isAuthenticatePending === false)
35 | assert(authState.isLogoutPending === false)
36 | assert(authState.payload === null)
37 |
38 | const request = { strategy: 'local', email: 'test', password: 'test' }
39 | actions.authenticate.call({ $store: store }, request).then(response => {
40 | assert(authState.accessToken === response.accessToken)
41 | assert(authState.errorOnAuthenticate === null)
42 | assert(authState.errorOnLogout === null)
43 | assert(authState.isAuthenticatePending === false)
44 | assert(authState.isLogoutPending === false)
45 | const expectedPayload = {
46 | userId: 0,
47 | exp: 9999999999999
48 | }
49 | assert.deepEqual(authState.payload, expectedPayload)
50 | done()
51 | })
52 |
53 | // Make sure proper state changes occurred before response
54 | assert(authState.accessToken === null)
55 | assert(authState.errorOnAuthenticate === null)
56 | assert(authState.errorOnLogout === null)
57 | assert(authState.isAuthenticatePending === true)
58 | assert(authState.isLogoutPending === false)
59 | assert(authState.payload === null)
60 | })
61 |
62 | it('Logout', done => {
63 | const store = new Vuex.Store({
64 | plugins: [auth()]
65 | })
66 | feathersClient.use('authentication', {
67 | create(data) {
68 | return Promise.resolve({ accessToken })
69 | }
70 | })
71 |
72 | const authState = store.state.auth
73 | const actions = mapActions('auth', ['authenticate', 'logout'])
74 | const request = { strategy: 'local', email: 'test', password: 'test' }
75 |
76 | actions.authenticate.call({ $store: store }, request).then(authResponse => {
77 | actions.logout.call({ $store: store }).then(response => {
78 | assert(authState.accessToken === null)
79 | assert(authState.errorOnAuthenticate === null)
80 | assert(authState.errorOnLogout === null)
81 | assert(authState.isAuthenticatePending === false)
82 | assert(authState.isLogoutPending === false)
83 | assert(authState.payload === null)
84 | done()
85 | })
86 | })
87 | })
88 |
89 | it('Authenticate with userService config option', done => {
90 | feathersClient.use('authentication', {
91 | create(data) {
92 | return Promise.resolve({ accessToken })
93 | }
94 | })
95 | feathersClient.use(
96 | 'users',
97 | memory({ store: { 0: { id: 0, email: 'test@test.com' } } })
98 | )
99 | const store = new Vuex.Store({
100 | plugins: [auth({ userService: 'users' }), service('users')]
101 | })
102 |
103 | const authState = store.state.auth
104 | const actions = mapActions('auth', ['authenticate'])
105 |
106 | assert(authState.user === null)
107 |
108 | const request = { strategy: 'local', email: 'test', password: 'test' }
109 | actions.authenticate
110 | .call({ $store: store }, request)
111 | .then(response => {
112 | const expectedUser = {
113 | id: 0,
114 | email: 'test@test.com'
115 | }
116 | assert.deepEqual(authState.user, expectedUser)
117 | done()
118 | })
119 | .catch(error => {
120 | assert(!error, error)
121 | done()
122 | })
123 | })
124 | })
125 |
--------------------------------------------------------------------------------
/test/auth-module/auth-module.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | import { assert } from 'chai'
4 | import feathersVuex from '../../src/index'
5 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client'
6 | import Vuex from 'vuex'
7 | import { isEmpty } from 'lodash'
8 |
9 | const { makeAuthPlugin, makeServicePlugin, BaseModel } = feathersVuex(
10 | feathersClient,
11 | {
12 | serverAlias: 'api'
13 | }
14 | )
15 | interface CustomStore {
16 | state: any
17 | auth: any
18 | authentication?: any
19 | users?: any
20 | }
21 |
22 | function makeContext() {
23 | class User extends BaseModel {
24 | constructor(data, options) {
25 | super(data, options)
26 | }
27 | static modelName = 'User'
28 | static instanceDefaults() {
29 | return {
30 | email: '',
31 | password: ''
32 | }
33 | }
34 | }
35 | const servicePath = 'users'
36 | const usersPlugin = makeServicePlugin({
37 | Model: User,
38 | service: feathersClient.service(servicePath),
39 | servicePath
40 | })
41 |
42 | const authPlugin = makeAuthPlugin({ userService: 'users' })
43 |
44 | const store = new Vuex.Store({
45 | plugins: [authPlugin, usersPlugin]
46 | })
47 |
48 | return { User, usersPlugin, authPlugin, BaseModel, store }
49 | }
50 |
51 | describe('Auth Module', () => {
52 | describe('Configuration', () => {
53 | it('has default auth namespace', () => {
54 | const { store } = makeContext()
55 | const authState = Object.assign({}, store.state.auth)
56 | const expectedCustomStore = {
57 | accessToken: null,
58 | entityIdField: 'userId',
59 | errorOnAuthenticate: null,
60 | errorOnLogout: null,
61 | isAuthenticatePending: false,
62 | isLogoutPending: false,
63 | payload: null,
64 | responseEntityField: 'user',
65 | serverAlias: 'api',
66 | user: null,
67 | userService: 'users'
68 | }
69 |
70 | assert.deepEqual(authState, expectedCustomStore, 'has the default state')
71 | })
72 |
73 | it('can customize the namespace', function() {
74 | const store = new Vuex.Store({
75 | plugins: [makeAuthPlugin({ namespace: 'authentication' })]
76 | })
77 |
78 | assert(store.state.authentication, 'the custom namespace was used')
79 | })
80 | })
81 |
82 | describe('Customizing Auth Store', function() {
83 | it('allows adding custom state', function() {
84 | const customState = {
85 | test: true,
86 | test2: {
87 | test: true
88 | }
89 | }
90 | const store = new Vuex.Store({
91 | plugins: [makeAuthPlugin({ state: customState })]
92 | })
93 |
94 | assert(store.state.auth.test === true, 'added custom state')
95 | assert(store.state.auth.test2.test === true, 'added custom state')
96 | })
97 |
98 | it('allows custom mutations', function() {
99 | const state = { test: true }
100 | const customMutations = {
101 | setTestToFalse(state) {
102 | state.test = false
103 | }
104 | }
105 | const store = new Vuex.Store({
106 | plugins: [makeAuthPlugin({ state, mutations: customMutations })]
107 | })
108 |
109 | store.commit('auth/setTestToFalse')
110 | assert(
111 | store.state.auth.test === false,
112 | 'the custom state was modified by the custom mutation'
113 | )
114 | })
115 |
116 | it('has a user && isAuthenticated getter when there is a userService attribute', function() {
117 | const store = new Vuex.Store({
118 | state: {
119 | state: {},
120 | auth: {},
121 | users: {
122 | idField: 'id',
123 | keyedById: {
124 | 1: {
125 | id: 1,
126 | name: 'Marshall'
127 | }
128 | }
129 | }
130 | },
131 | plugins: [
132 | makeAuthPlugin({
133 | state: {
134 | user: {
135 | id: 1
136 | }
137 | },
138 | userService: 'users'
139 | })
140 | ]
141 | })
142 | const user = store.getters['auth/user']
143 | const isAuthenticated = store.getters['auth/isAuthenticated']
144 |
145 | assert(user.name === 'Marshall', 'Got the user from the users store.')
146 | assert(isAuthenticated, 'isAuthenticated')
147 | })
148 |
149 | it('getters show not authenticated when there is no user', function() {
150 | const store = new Vuex.Store({
151 | state: {
152 | state: {},
153 | auth: {},
154 | users: {
155 | idField: 'id',
156 | keyedById: {}
157 | }
158 | },
159 | plugins: [
160 | makeAuthPlugin({
161 | state: {},
162 | userService: 'users'
163 | })
164 | ]
165 | })
166 | const user = store.getters['auth/user']
167 | const isAuthenticated = store.getters['auth/isAuthenticated']
168 |
169 | assert(user === null, 'user getter returned null as expected')
170 | assert(!isAuthenticated, 'not authenticated')
171 | })
172 |
173 | it('allows custom getters', function() {
174 | const customGetters = {
175 | oneTwoThree() {
176 | return 123
177 | }
178 | }
179 | const store = new Vuex.Store({
180 | plugins: [makeAuthPlugin({ getters: customGetters })]
181 | })
182 |
183 | assert(
184 | store.getters['auth/oneTwoThree'] === 123,
185 | 'the custom getter was available'
186 | )
187 | })
188 |
189 | it('allows adding custom actions', function() {
190 | const config = {
191 | state: {
192 | isTrue: false
193 | },
194 | mutations: {
195 | setToTrue(state) {
196 | state.isTrue = true
197 | }
198 | },
199 | actions: {
200 | trigger(context) {
201 | context.commit('setToTrue')
202 | }
203 | }
204 | }
205 | const store = new Vuex.Store({
206 | plugins: [makeAuthPlugin(config)]
207 | })
208 |
209 | store.dispatch('auth/trigger')
210 | assert(store.state.auth.isTrue === true, 'the custom action was run')
211 | })
212 | })
213 |
214 | it('Calls auth service without params', async function() {
215 | let receivedData = null
216 | let receivedParams = null
217 | feathersClient.use('authentication', {
218 | create(data, params) {
219 | receivedData = data
220 | receivedParams = params
221 | return Promise.resolve({ accessToken: 'jg54jh2gj6fgh734j5h4j25jbh' })
222 | }
223 | })
224 |
225 | const { store } = makeContext()
226 |
227 | const request = { strategy: 'local', email: 'test', password: 'test' }
228 | await store.dispatch('auth/authenticate', request)
229 | assert(receivedData, 'got data')
230 | assert(receivedData.strategy === 'local', 'got strategy')
231 | assert(receivedData.email === 'test', 'got email')
232 | assert(receivedData.password === 'test', 'got password')
233 | assert(receivedParams && isEmpty(receivedParams), 'empty params')
234 | })
235 |
236 | it('Calls auth service with params', async function() {
237 | let receivedParams = null
238 | feathersClient.use('authentication', {
239 | create(data, params) {
240 | receivedParams = params
241 | return Promise.resolve({ accessToken: 'jg54jh2gj6fgh734j5h4j25jbh' })
242 | }
243 | })
244 |
245 | const { store } = makeContext()
246 |
247 | const request = { strategy: 'local', email: 'test', password: 'test' }
248 | const customParams = { theAnswer: 42 }
249 | await store.dispatch('auth/authenticate', [request, customParams])
250 | assert(receivedParams && receivedParams.theAnswer === 42, 'got params')
251 | })
252 | })
253 |
--------------------------------------------------------------------------------
/test/auth.test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai'
2 | import feathersVuexAuth, { reducer } from '../src/auth'
3 | import * as actionTypes from '../src/action-types'
4 | import './server'
5 | import { makeFeathersRestClient } from './feathers-client'
6 |
7 | describe('feathers-vuex:auth', () => {
8 | it('is CommonJS compatible', () => {
9 | assert(typeof require('../lib/auth').default === 'function')
10 | })
11 |
12 | it('basic functionality', () => {
13 | assert(typeof feathersVuexAuth === 'function', 'It worked')
14 | })
15 |
16 | it('throws an error if the auth plugin is missing', () => {
17 | const app = {}
18 | const store = {}
19 | const plugin = feathersVuexAuth(store).bind(app)
20 | assert.throws(
21 | plugin,
22 | 'You must first register the @feathersjs/authentication-client plugin'
23 | )
24 | })
25 |
26 | it('returns the app, is chainable', () => {
27 | const app = {
28 | authenticate() {}
29 | }
30 | const store = {}
31 | const returnValue = feathersVuexAuth(store).bind(app)()
32 | assert(returnValue === app)
33 | })
34 |
35 | it('replaces the original authenticate function', () => {
36 | const feathersClient = makeFeathersRestClient()
37 | const oldAuthenticate = feathersClient.authenticate
38 | const store = {}
39 | feathersClient.configure(feathersVuexAuth(store))
40 | assert(oldAuthenticate !== feathersClient.authenticate)
41 | })
42 |
43 | it('dispatches actions to the store.', done => {
44 | const feathersClient = makeFeathersRestClient()
45 | const fakeStore = {
46 | dispatch(action) {
47 | switch (action.type) {
48 | case actionTypes.FEATHERS_AUTH_REQUEST:
49 | assert(action.payload.test || action.payload.accessToken)
50 | break
51 | case actionTypes.FEATHERS_AUTH_SUCCESS:
52 | assert(action.data)
53 | break
54 | case actionTypes.FEATHERS_AUTH_FAILURE:
55 | assert(action.error)
56 | done()
57 | break
58 | case actionTypes.FEATHERS_AUTH_LOGOUT:
59 | assert(action)
60 | break
61 | }
62 | }
63 | }
64 |
65 | feathersClient.configure(feathersVuexAuth(fakeStore))
66 |
67 | try {
68 | feathersClient
69 | .authenticate({ test: true })
70 | .then(response => {
71 | feathersClient.logout()
72 | return response
73 | })
74 | .catch(error => {
75 | assert(error.className === 'not-authenticated')
76 | })
77 | } catch (err) {}
78 | try {
79 | feathersClient.authenticate({
80 | strategy: 'jwt',
81 | accessToken: 'q34twershtdyfhgmj'
82 | })
83 | } catch (err) {
84 | // eslint-disable-next-line no-console
85 | console.log(err)
86 | }
87 | })
88 | })
89 |
90 | describe('feathers-vuex:auth - Reducer', () => {
91 | it('Has defaults', () => {
92 | const state = undefined
93 | const defaultState = {
94 | isPending: false,
95 | isError: false,
96 | isSignedIn: false,
97 | accessToken: null,
98 | error: undefined
99 | }
100 | const newState = reducer(state, {})
101 | assert.deepEqual(newState, defaultState)
102 | })
103 |
104 | it(`Responds to ${actionTypes.FEATHERS_AUTH_REQUEST}`, () => {
105 | const state = undefined
106 | const action = {
107 | type: actionTypes.FEATHERS_AUTH_REQUEST,
108 | payload: {
109 | strategy: 'jwt',
110 | accessToken: 'evh8vq2pj'
111 | }
112 | }
113 | const expectedState = {
114 | isPending: true,
115 | isError: false,
116 | isSignedIn: false,
117 | accessToken: null,
118 | error: undefined
119 | }
120 | const newState = reducer(state, action)
121 | assert.deepEqual(newState, expectedState)
122 | })
123 |
124 | it(`Responds to ${actionTypes.FEATHERS_AUTH_SUCCESS}`, () => {
125 | const state = undefined
126 | const accessToken = 'evh8vq2pj'
127 | const action = {
128 | type: actionTypes.FEATHERS_AUTH_SUCCESS,
129 | data: { accessToken }
130 | }
131 | const expectedState = {
132 | isPending: false,
133 | isError: false,
134 | isSignedIn: true,
135 | accessToken: accessToken,
136 | error: undefined
137 | }
138 | const newState = reducer(state, action)
139 | assert.deepEqual(newState, expectedState)
140 | })
141 |
142 | it(`Responds to ${actionTypes.FEATHERS_AUTH_FAILURE}`, () => {
143 | const state = undefined
144 | const error = 'Unauthorized'
145 | const action = {
146 | type: actionTypes.FEATHERS_AUTH_FAILURE,
147 | error
148 | }
149 | const expectedState = {
150 | isPending: false,
151 | isError: true,
152 | isSignedIn: false,
153 | accessToken: null,
154 | error
155 | }
156 | const newState = reducer(state, action)
157 | assert.deepEqual(newState, expectedState)
158 | })
159 |
160 | it(`Responds to ${actionTypes.FEATHERS_AUTH_LOGOUT}`, () => {
161 | const state = undefined
162 | const action = {
163 | type: actionTypes.FEATHERS_AUTH_LOGOUT
164 | }
165 | const expectedState = {
166 | isPending: false,
167 | isError: false,
168 | isSignedIn: false,
169 | accessToken: null,
170 | error: undefined
171 | }
172 | const newState = reducer(state, action)
173 | assert.deepEqual(newState, expectedState)
174 | })
175 | })
176 |
--------------------------------------------------------------------------------
/test/fixtures/feathers-client.js:
--------------------------------------------------------------------------------
1 | import feathers from '@feathersjs/feathers'
2 | import socketio from '@feathersjs/socketio-client'
3 | import rest from '@feathersjs/rest-client'
4 | import axios from 'axios'
5 | import auth from '@feathersjs/authentication-client'
6 | import io from 'socket.io-client/dist/socket.io'
7 | import fixtureSocket from 'can-fixture-socket'
8 |
9 | const mockServer = new fixtureSocket.Server(io)
10 | const baseUrl = 'http://localhost:3030'
11 |
12 | // These are fixtures used in the service-modulet.test.js under socket events.
13 | let id = 0
14 | mockServer.on('things::create', function (data, params, cb) {
15 | data.id = id
16 | id++
17 | mockServer.emit('things created', data)
18 | cb(null, data)
19 | })
20 | mockServer.on('things::patch', function (id, data, params, cb) {
21 | Object.assign(data, { id, test: true })
22 | mockServer.emit('things patched', data)
23 | cb(null, data)
24 | })
25 | mockServer.on('things::update', function (id, data, params, cb) {
26 | Object.assign(data, { id, test: true })
27 | mockServer.emit('things updated', data)
28 | cb(null, data)
29 | })
30 | mockServer.on('things::remove', function (id, obj, cb) {
31 | const response = { id, test: true }
32 | mockServer.emit('things removed', response)
33 | cb(null, response)
34 | })
35 |
36 | let idDebounce = 0
37 |
38 | mockServer.on('things-debounced::create', function (data, obj, cb) {
39 | data.id = idDebounce
40 | idDebounce++
41 | mockServer.emit('things-debounced created', data)
42 | cb(null, data)
43 | })
44 | mockServer.on('things-debounced::patch', function (id, data, params, cb) {
45 | Object.assign(data, { id, test: true })
46 | mockServer.emit('things-debounced patched', data)
47 | cb(null, data)
48 | })
49 | mockServer.on('things-debounced::update', function (id, data, params, cb) {
50 | Object.assign(data, { id, test: true })
51 | mockServer.emit('things-debounced updated', data)
52 | cb(null, data)
53 | })
54 | mockServer.on('things-debounced::remove', function (id, params, cb) {
55 | const response = { id, test: true }
56 | mockServer.emit('things-debounced removed', response)
57 | cb(null, response)
58 | })
59 |
60 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
61 | export function makeFeathersSocketClient(baseUrl) {
62 | const socket = io(baseUrl)
63 |
64 | return feathers().configure(socketio(socket)).configure(auth())
65 | }
66 |
67 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
68 | export function makeFeathersRestClient(baseUrl) {
69 | return feathers().configure(rest(baseUrl).axios(axios)).configure(auth())
70 | }
71 |
72 | const sock = io(baseUrl)
73 |
74 | export const feathersSocketioClient = feathers()
75 | .configure(socketio(sock))
76 | .configure(auth())
77 |
78 | export const feathersRestClient = feathers()
79 | .configure(rest(baseUrl).axios(axios))
80 | .configure(auth())
81 |
--------------------------------------------------------------------------------
/test/fixtures/server.js:
--------------------------------------------------------------------------------
1 | import feathers from '@feathersjs/feathers'
2 | import rest from '@feathersjs/express/rest'
3 | import socketio from '@feathersjs/socketio'
4 | import bodyParser from 'body-parser'
5 | import auth from '@feathersjs/authentication'
6 | import jwt from '@feathersjs/authentication-jwt'
7 | import memory from 'feathers-memory'
8 |
9 | const app = feathers()
10 | .use(bodyParser.json())
11 | .use(bodyParser.urlencoded({ extended: true }))
12 | .configure(rest())
13 | .configure(socketio())
14 | .use('/users', memory())
15 | .use('/todos', memory())
16 | .use('/errors', memory())
17 | .configure(
18 | auth({
19 | secret: 'test',
20 | service: '/users'
21 | })
22 | )
23 | .configure(jwt())
24 |
25 | app.service('/errors').hooks({
26 | before: {
27 | all: [
28 | hook => {
29 | throw new Error(`${hook.method} Denied!`)
30 | }
31 | ]
32 | }
33 | })
34 |
35 | const port = 3030
36 | const server = app.listen(port)
37 |
38 | process.on('unhandledRejection', (reason, p) =>
39 | console.log('Unhandled Rejection at: Promise ', p, reason)
40 | )
41 |
42 | server.on('listening', () => {
43 | console.log(`Feathers application started on localhost:${port}`)
44 |
45 | setTimeout(function() {
46 | server.close()
47 | }, 50000)
48 | })
49 |
--------------------------------------------------------------------------------
/test/fixtures/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | Vue.use(Vuex)
5 |
6 | export default function makeStore() {
7 | return new Vuex.Store({
8 | state: {
9 | count: 0
10 | },
11 | mutations: {
12 | increment(state) {
13 | state.count++
14 | }
15 | }
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/test/fixtures/todos.js:
--------------------------------------------------------------------------------
1 | export function makeTodos() {
2 | return {
3 | 1: { _id: 1, description: 'Dishes', isComplete: true },
4 | 2: { _id: 2, description: 'Laundry', isComplete: true },
5 | 3: { _id: 3, description: 'Groceries', isComplete: true }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai'
2 | import * as feathersVuex from '../src/index'
3 | import Vue from 'vue'
4 | import Vuex from 'vuex'
5 |
6 | Vue.use(Vuex)
7 |
8 | describe('feathers-vuex', () => {
9 | it('has correct exports', () => {
10 | assert(typeof feathersVuex.default === 'function')
11 | assert(
12 | typeof feathersVuex.FeathersVuex.install === 'function',
13 | 'has Vue Plugin'
14 | )
15 | assert(feathersVuex.FeathersVuexFind)
16 | assert(feathersVuex.FeathersVuexGet)
17 | assert(feathersVuex.initAuth)
18 | assert(feathersVuex.makeFindMixin)
19 | assert(feathersVuex.makeGetMixin)
20 | assert(feathersVuex.models)
21 | })
22 |
23 | it('requires a Feathers Client instance', () => {
24 | try {
25 | feathersVuex.default(
26 | {},
27 | {
28 | serverAlias: 'index-test'
29 | }
30 | )
31 | } catch (error) {
32 | assert(
33 | error.message ===
34 | 'The first argument to feathersVuex must be a feathers client.'
35 | )
36 | }
37 | })
38 | })
39 |
--------------------------------------------------------------------------------
/test/make-find-mixin.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0
5 | */
6 | import { assert } from 'chai'
7 | import jsdom from 'jsdom-global'
8 | import Vue from 'vue/dist/vue'
9 | import Vuex from 'vuex'
10 | import feathersVuex, { FeathersVuex } from '../src/index'
11 | import makeFindMixin from '../src/make-find-mixin'
12 | import { feathersRestClient as feathersClient } from './fixtures/feathers-client'
13 |
14 | jsdom()
15 | require('events').EventEmitter.prototype._maxListeners = 100
16 |
17 | function makeContext() {
18 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, {
19 | serverAlias: 'make-find-mixin'
20 | })
21 |
22 | class FindModel extends BaseModel {
23 | public static modelName = 'FindModel'
24 | public static test = true
25 | }
26 |
27 | return { FindModel, BaseModel, makeServicePlugin }
28 | }
29 |
30 | Vue.use(Vuex)
31 | Vue.use(FeathersVuex)
32 |
33 | describe('Find Mixin', function () {
34 | const { makeServicePlugin, FindModel } = makeContext()
35 | const serviceName = 'todos'
36 | const store = new Vuex.Store({
37 | plugins: [
38 | makeServicePlugin({
39 | Model: FindModel,
40 | service: feathersClient.service(serviceName)
41 | })
42 | ]
43 | })
44 |
45 | it('correctly forms mixin data', function () {
46 | const todosMixin = makeFindMixin({ service: 'todos' })
47 | interface TodosComponent {
48 | todos: []
49 | todosServiceName: string
50 | isFindTodosPending: boolean
51 | haveTodosBeenRequestedOnce: boolean
52 | haveTodosLoadedOnce: boolean
53 | findTodos: Function
54 | todosLocal: boolean
55 | todosQid: string
56 | todosQueryWhen: Function
57 | todosParams: any
58 | todosFetchParams: any
59 | }
60 |
61 | const vm = new Vue({
62 | name: 'todos-component',
63 | mixins: [todosMixin],
64 | store,
65 | template: ``
66 | }).$mount()
67 |
68 | assert.deepEqual(vm.todos, [], 'todos prop was empty array')
69 | assert(
70 | vm.hasOwnProperty('todosPaginationData'),
71 | 'pagination data prop was present, even if undefined'
72 | )
73 | assert(vm.todosServiceName === 'todos', 'service name was correct')
74 | assert(vm.isFindTodosPending === false, 'loading boolean is in place')
75 | assert(
76 | vm.haveTodosBeenRequestedOnce === false,
77 | 'requested once boolean is in place'
78 | )
79 | assert(vm.haveTodosLoadedOnce === false, 'loaded once boolean is in place')
80 | assert(typeof vm.findTodos === 'function', 'the find action is in place')
81 | assert(vm.todosLocal === false, 'local boolean is false by default')
82 | assert(
83 | typeof vm.$options.created[0] === 'function',
84 | 'created lifecycle hook function is in place given that local is false'
85 | )
86 | assert(
87 | vm.todosQid === 'default',
88 | 'the default query identifier is in place'
89 | )
90 | assert(vm.todosQueryWhen === true, 'the default queryWhen is true')
91 | // assert(vm.todosWatch.length === 0, 'the default watch is an empty array')
92 | assert(
93 | vm.todosParams === undefined,
94 | 'no params are in place by default, must be specified by the user'
95 | )
96 | assert(
97 | vm.todosFetchParams === undefined,
98 | 'no fetch params are in place by default, must be specified by the user'
99 | )
100 | })
101 |
102 | it('correctly forms mixin data for dynamic service', function () {
103 | const tasksMixin = makeFindMixin({
104 | service() {
105 | return this.serviceName
106 | },
107 | local: true
108 | })
109 |
110 | interface TasksComponent {
111 | tasks: []
112 | serviceServiceName: string
113 | isFindTasksPending: boolean
114 | findTasks: Function
115 | tasksLocal: boolean
116 | tasksQid: string
117 | tasksQueryWhen: Function
118 | tasksParams: any
119 | tasksFetchParams: any
120 | }
121 |
122 | const vm = new Vue({
123 | name: 'tasks-component',
124 | data: () => ({
125 | serviceName: 'tasks'
126 | }),
127 | mixins: [tasksMixin],
128 | store,
129 | template: ``
130 | }).$mount()
131 |
132 | assert.deepEqual(vm.items, [], 'items prop was empty array')
133 | assert(
134 | vm.hasOwnProperty('servicePaginationData'),
135 | 'pagination data prop was present, even if undefined'
136 | )
137 | assert(vm.serviceServiceName === 'tasks', 'service name was correct')
138 | assert(vm.isFindServicePending === false, 'loading boolean is in place')
139 | assert(typeof vm.findService === 'function', 'the find action is in place')
140 | assert(vm.serviceLocal === true, 'local boolean is set to true')
141 | assert(
142 | typeof vm.$options.created === 'undefined',
143 | 'created lifecycle hook function is NOT in place given that local is true'
144 | )
145 | assert(
146 | vm.serviceQid === 'default',
147 | 'the default query identifier is in place'
148 | )
149 | assert(vm.serviceQueryWhen === true, 'the default queryWhen is true')
150 | // assert(vm.tasksWatch.length === 0, 'the default watch is an empty array')
151 | assert(
152 | vm.serviceParams === undefined,
153 | 'no params are in place by default, must be specified by the user'
154 | )
155 | assert(
156 | vm.serviceFetchParams === undefined,
157 | 'no fetch params are in place by default, must be specified by the user'
158 | )
159 | })
160 | })
161 |
--------------------------------------------------------------------------------
/test/service-module/misconfigured-client.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint @typescript-eslint/ban-ts-ignore:0 */
2 | import { assert } from 'chai'
3 | import feathersVuex from '../../src/index'
4 | import feathers from '@feathersjs/client'
5 | import auth from '@feathersjs/authentication-client'
6 |
7 | // @ts-ignore
8 | const feathersClient = feathers().configure(auth())
9 |
10 | describe('Service Module - Bad Client Setup', () => {
11 | it('throws an error when no client transport plugin is registered', () => {
12 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, {
13 | serverAlias: 'misconfigured'
14 | })
15 | class MisconfiguredTask extends BaseModel {
16 | public static modelName = 'MisconfiguredTask'
17 | public static test = true
18 | }
19 |
20 | try {
21 | makeServicePlugin({
22 | Model: MisconfiguredTask,
23 | service: feathersClient.service('misconfigured-todos')
24 | })
25 | } catch (error) {
26 | assert(
27 | error.message.includes(
28 | 'No service was provided. If you passed one in, check that you have configured a transport plugin on the Feathers Client. Make sure you use the client version of the transport.'
29 | ),
30 | 'got an error with a misconfigured client'
31 | )
32 | }
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/test/service-module/model-base.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0
5 | */
6 | import { assert } from 'chai'
7 | import Vue from 'vue'
8 | import Vuex from 'vuex'
9 | import { clearModels } from '../../src/service-module/global-models'
10 | import {
11 | feathersRestClient as feathers,
12 | makeFeathersRestClient
13 | } from '../fixtures/feathers-client'
14 | import feathersVuex from '../../src/index'
15 |
16 | Vue.use(Vuex)
17 |
18 | process.setMaxListeners(100)
19 |
20 | describe.skip('Model - Standalone', function () {
21 | it.skip('allows using a model without a service', function () {})
22 | it.skip('rename serverAlias to just `alias` or maybe `groupName`', function () {})
23 | })
24 |
25 | describe('makeModel / BaseModel', function () {
26 | before(() => {
27 | clearModels()
28 | })
29 |
30 | it('properly sets up the BaseModel', function () {
31 | const alias = 'model-base'
32 | const { BaseModel } = feathersVuex(feathers, { serverAlias: alias })
33 | const {
34 | name,
35 | store,
36 | namespace,
37 | idField,
38 | preferUpdate,
39 | serverAlias,
40 | models,
41 | copiesById
42 | } = BaseModel
43 |
44 | assert(name === 'BaseModel', 'name in place')
45 |
46 | // Monkey patched onto the Model class in `makeServicePlugin()`
47 | assert(!store, 'no store by default')
48 | assert(!namespace, 'no namespace by default')
49 |
50 | assert(idField === 'id', 'default idField is id')
51 | assert(!preferUpdate, 'prefer fetch by default')
52 |
53 | // Readonly props
54 | assert(serverAlias === 'model-base', 'serverAlias')
55 | assert(models, 'models are available')
56 | assert.equal(Object.keys(copiesById).length, 0, 'copiesById is empty')
57 |
58 | // Static Methods
59 | const staticMethods = [
60 | 'getId',
61 | 'find',
62 | 'findInStore',
63 | 'count',
64 | 'countInStore',
65 | 'get',
66 | 'getFromStore'
67 | ]
68 | staticMethods.forEach(method => {
69 | assert(typeof BaseModel[method] === 'function', `has ${method} method`)
70 | })
71 |
72 | // Prototype Methods
73 | const prototypeMethods = [
74 | 'clone',
75 | 'reset',
76 | 'commit',
77 | 'save',
78 | 'create',
79 | 'patch',
80 | 'update',
81 | 'remove'
82 | ]
83 | prototypeMethods.forEach(method => {
84 | assert(
85 | typeof BaseModel.prototype[method] === 'function',
86 | `has ${method} method`
87 | )
88 | })
89 |
90 | // Utility Methods
91 | const utilityMethods = ['hydrateAll']
92 | utilityMethods.forEach(method => {
93 | assert(typeof BaseModel[method] === 'function', `has ${method} method`)
94 | })
95 |
96 | const eventMethods = [
97 | 'on',
98 | 'off',
99 | 'once',
100 | 'emit',
101 | 'addListener',
102 | 'removeListener',
103 | 'removeAllListeners'
104 | ]
105 | eventMethods.forEach(method => {
106 | assert(typeof BaseModel[method] === 'function', `has ${method} method`)
107 | })
108 |
109 | const getterMethods = [
110 | 'isCreatePending',
111 | 'isUpdatePending',
112 | 'isPatchPending',
113 | 'isRemovePending',
114 | 'isSavePending',
115 | 'isPending'
116 | ]
117 | const m = new BaseModel()
118 | getterMethods.forEach(method => {
119 | assert(
120 | typeof Object.getOwnPropertyDescriptor(Object.getPrototypeOf(m), method).get === 'function',
121 | `has ${method} getter`
122 | )
123 | })
124 | })
125 |
126 | it('allows customization through the FeathersVuexOptions', function () {
127 | const { BaseModel } = feathersVuex(feathers, {
128 | serverAlias: 'myApi',
129 | idField: '_id',
130 | preferUpdate: true
131 | })
132 | const { idField, preferUpdate, serverAlias } = BaseModel
133 |
134 | assert(idField === '_id', 'idField was set')
135 | assert(preferUpdate, 'turned on preferUpdate')
136 | assert(serverAlias === 'myApi', 'serverAlias was set')
137 | })
138 |
139 | it('receives store & other props after Vuex plugin is registered', function () {
140 | const { BaseModel, makeServicePlugin } = feathersVuex(feathers, {
141 | serverAlias: 'myApi'
142 | })
143 | BaseModel.modelName = 'TestModel'
144 | const plugin = makeServicePlugin({
145 | servicePath: 'todos',
146 | service: feathers.service('todos'),
147 | Model: BaseModel
148 | })
149 | new Vuex.Store({
150 | plugins: [plugin]
151 | })
152 | const { store, namespace, servicePath } = BaseModel
153 |
154 | assert(store, 'store is in place')
155 | assert.equal(namespace, 'todos', 'namespace is in place')
156 | assert.equal(servicePath, 'todos', 'servicePath is in place')
157 | })
158 |
159 | it('allows access to other models after Vuex plugins are registered', function () {
160 | const serverAlias = 'model-base'
161 | const { makeServicePlugin, BaseModel, models } = feathersVuex(feathers, {
162 | idField: '_id',
163 | serverAlias
164 | })
165 |
166 | // Create a Todo Model & Plugin
167 | class Todo extends BaseModel {
168 | public static modelName = 'Todo'
169 | public test = true
170 | }
171 | const todosPlugin = makeServicePlugin({
172 | servicePath: 'todos',
173 | Model: Todo,
174 | service: feathers.service('todos')
175 | })
176 |
177 | // Create a Task Model & Plugin
178 | class Task extends BaseModel {
179 | public static modelName = 'Task'
180 | public test = true
181 | }
182 | const tasksPlugin = makeServicePlugin({
183 | servicePath: 'tasks',
184 | Model: Task,
185 | service: feathers.service('tasks')
186 | })
187 |
188 | // Register the plugins
189 | new Vuex.Store({
190 | plugins: [todosPlugin, tasksPlugin]
191 | })
192 |
193 | assert(models[serverAlias][Todo.name] === Todo)
194 | assert.equal(Todo.models, models, 'models available at Model.models')
195 | assert.equal(Task.models, models, 'models available at Model.models')
196 | })
197 |
198 | it('works with multiple, independent Feathers servers', function () {
199 | // Create a Todo Model & Plugin on myApi
200 | const feathersMyApi = makeFeathersRestClient('https://api.my-api.com')
201 | const myApi = feathersVuex(feathersMyApi, {
202 | idField: '_id',
203 | serverAlias: 'myApi'
204 | })
205 | class Todo extends myApi.BaseModel {
206 | public static modelName = 'Todo'
207 | public test = true
208 | }
209 | const todosPlugin = myApi.makeServicePlugin({
210 | Model: Todo,
211 | service: feathersMyApi.service('todos')
212 | })
213 |
214 | // Create a Task Model & Plugin on theirApi
215 | const feathersTheirApi = makeFeathersRestClient('https://api.their-api.com')
216 | const theirApi = feathersVuex(feathersTheirApi, {
217 | serverAlias: 'theirApi'
218 | })
219 | class Task extends theirApi.BaseModel {
220 | public static modelName = 'Task'
221 | public test = true
222 | }
223 | const tasksPlugin = theirApi.makeServicePlugin({
224 | Model: Task,
225 | service: feathersTheirApi.service('tasks')
226 | })
227 |
228 | // Register the plugins
229 | new Vuex.Store({
230 | plugins: [todosPlugin, tasksPlugin]
231 | })
232 | const { models } = myApi
233 |
234 | assert(models.myApi.Todo === Todo)
235 | assert(!models.theirApi.Todo, `Todo stayed out of the 'theirApi' namespace`)
236 | assert(models.theirApi.Task === Task)
237 | assert(!models.myApi.Task, `Task stayed out of the 'myApi' namespace`)
238 |
239 | assert.equal(
240 | models.myApi.byServicePath[Todo.servicePath],
241 | Todo,
242 | 'also registered in models.byServicePath'
243 | )
244 | assert.equal(
245 | models.theirApi.byServicePath[Task.servicePath],
246 | Task,
247 | 'also registered in models.byServicePath'
248 | )
249 | })
250 | })
251 |
--------------------------------------------------------------------------------
/test/service-module/model-serialize.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0
5 | */
6 | import { assert } from 'chai'
7 | import feathersVuex from '../../src/index'
8 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client'
9 | import { clearModels } from '../../src/service-module/global-models'
10 | import _omit from 'lodash/omit'
11 | import Vuex from 'vuex'
12 |
13 | describe('Models - Serialize', function () {
14 | beforeEach(() => {
15 | clearModels()
16 | })
17 |
18 | it('allows customizing toJSON', function () {
19 | const { BaseModel, makeServicePlugin } = feathersVuex(feathersClient, {
20 | serverAlias: 'myApi'
21 | })
22 |
23 | class Task extends BaseModel {
24 | public static modelName = 'Task'
25 | public static instanceDefaults() {
26 | return {
27 | id: null,
28 | description: '',
29 | isComplete: false
30 | }
31 | }
32 | public toJSON() {
33 | return _omit(this, ['isComplete'])
34 | }
35 | public constructor(data, options?) {
36 | super(data, options)
37 | }
38 | }
39 |
40 | const servicePath = 'thingies'
41 | const plugin = makeServicePlugin({
42 | servicePath: 'thingies',
43 | Model: Task,
44 | service: feathersClient.service(servicePath)
45 | })
46 |
47 | new Vuex.Store({ plugins: [plugin] })
48 |
49 | const task = new Task({
50 | description: 'Hello, World!',
51 | isComplete: true
52 | })
53 |
54 | assert(!task.toJSON().hasOwnProperty('isComplete'), 'custom toJSON worked')
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/test/service-module/model-tests.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0
5 | */
6 | import { assert } from 'chai'
7 |
8 | interface ModelOptions {
9 | servicePath: string
10 | }
11 |
12 | describe('TypeScript Class Inheritance', () => {
13 | it('Can access static instanceDefaults from BaseModel', () => {
14 | abstract class BaseModel {
15 | public static instanceDefaults
16 | public constructor(data, options?) {
17 | const { instanceDefaults } = this.constructor as typeof BaseModel
18 | const defaults = instanceDefaults(data, options)
19 | assert(
20 | defaults.description === 'default description',
21 | 'We get defaults in the BaseModel constructor'
22 | )
23 | Object.assign(this, defaults, data)
24 | }
25 | }
26 | class Todo extends BaseModel {
27 | public static modelName = 'Todo'
28 |
29 | public description: string
30 | public static instanceDefaults = (data, options) => ({
31 | description: 'default description'
32 | })
33 |
34 | public constructor(data, options?) {
35 | super(data, options)
36 | const { instanceDefaults } = this.constructor as typeof BaseModel
37 | const defaults = instanceDefaults(data, options)
38 | assert(
39 | defaults.description === 'default description',
40 | 'We get defaults in the Todo constructor, too'
41 | )
42 | }
43 | }
44 |
45 | const todo = new Todo({
46 | test: true
47 | })
48 |
49 | assert(
50 | todo.description === 'default description',
51 | 'got default description'
52 | )
53 | })
54 |
55 | it('Can access static instanceDefaults from two levels of inheritance', () => {
56 | abstract class BaseModel {
57 | public static instanceDefaults
58 | public constructor(data, options?) {
59 | const { instanceDefaults } = this.constructor as typeof BaseModel
60 | const defaults = instanceDefaults(data, options)
61 | assert(
62 | defaults.description === 'default description',
63 | 'We get defaults in the BaseModel constructor'
64 | )
65 | Object.assign(this, defaults, data)
66 | }
67 | }
68 |
69 | function makeServiceModel(options) {
70 | const { servicePath } = options
71 |
72 | class ServiceModel extends BaseModel {
73 | public static modelName = 'ServiceModel'
74 | public constructor(data, options: ModelOptions = { servicePath: '' }) {
75 | options.servicePath = servicePath
76 | super(data, options)
77 | }
78 | }
79 | return ServiceModel
80 | }
81 |
82 | class Todo extends makeServiceModel({ servicePath: 'todos' }) {
83 | public static modelName = 'Todo'
84 | public description: string
85 |
86 | public static instanceDefaults = (data, options) => ({
87 | description: 'default description'
88 | })
89 | }
90 |
91 | const todo = new Todo({
92 | test: true
93 | })
94 |
95 | assert(
96 | todo.description === 'default description',
97 | 'got default description'
98 | )
99 | })
100 |
101 | it('Can access static servicePath from Todo in BaseModel', () => {
102 | abstract class BaseModel {
103 | public static instanceDefaults
104 | public static servicePath
105 | public static namespace
106 |
107 | public constructor(data, options?) {
108 | const { instanceDefaults, servicePath, namespace } = this
109 | .constructor as typeof BaseModel
110 | const defaults = instanceDefaults(data, options)
111 | assert(
112 | defaults.description === 'default description',
113 | 'We get defaults in the BaseModel constructor'
114 | )
115 | Object.assign(this, defaults, data, {
116 | _options: { namespace, servicePath }
117 | })
118 | }
119 | }
120 |
121 | class Todo extends BaseModel {
122 | public static modelName = 'Todo'
123 | public static namespace: string = 'todos'
124 | public static servicePath: string = 'v1/todos'
125 |
126 | public description: string
127 | public _options
128 |
129 | public static instanceDefaults = (data, models) => ({
130 | description: 'default description'
131 | })
132 | }
133 |
134 | const todo = new Todo({
135 | test: true
136 | })
137 |
138 | assert(todo._options.servicePath === 'v1/todos', 'got static servicePath')
139 | })
140 |
141 | it('cannot serialize instance methods', () => {
142 | class BaseModel {
143 | public clone() {
144 | return this
145 | }
146 |
147 | public constructor(data) {
148 | Object.assign(this, data)
149 | }
150 | }
151 |
152 | class Todo extends BaseModel {
153 | public static modelName = 'Todo'
154 | public serialize() {
155 | return Object.assign({}, this, { serialized: true })
156 | }
157 | }
158 |
159 | const todo = new Todo({ name: 'test' })
160 | const json = JSON.parse(JSON.stringify(todo))
161 |
162 | assert(!json.clone)
163 | assert(!json.serialize)
164 | })
165 | })
166 |
--------------------------------------------------------------------------------
/test/service-module/service-module.reinitialization.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai'
2 | import Vuex from 'vuex'
3 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client'
4 | import feathersVuex from '../../src/index'
5 |
6 | interface RootState {
7 | todos: any
8 | }
9 |
10 | function makeContext() {
11 | const todoService = feathersClient.service('todos')
12 | const serverAlias = 'reinitialization'
13 | const { makeServicePlugin, BaseModel, models } = feathersVuex(
14 | feathersClient,
15 | {
16 | serverAlias
17 | }
18 | )
19 | class Todo extends BaseModel {
20 | public static modelName = 'Todo'
21 | }
22 | return {
23 | makeServicePlugin,
24 | BaseModel,
25 | todoService,
26 | Todo,
27 | models,
28 | serverAlias
29 | }
30 | }
31 |
32 | describe('Service Module - Reinitialization', function () {
33 | /**
34 | * Tests that when the make service plugin is reinitialized state
35 | * is reset in the vuex module/model.
36 | * This prevents state pollution in SSR setups.
37 | */
38 | it('does not preserve module/model state when reinitialized', function () {
39 | const {
40 | makeServicePlugin,
41 | todoService,
42 | Todo,
43 | models,
44 | serverAlias
45 | } = makeContext()
46 | const todosPlugin = makeServicePlugin({
47 | servicePath: 'todos',
48 | Model: Todo,
49 | service: todoService
50 | })
51 | let store = new Vuex.Store({
52 | plugins: [todosPlugin]
53 | })
54 | let todoState = store.state['todos']
55 | const virginState = {
56 | addOnUpsert: false,
57 | autoRemove: false,
58 | debug: false,
59 | copiesById: {},
60 | enableEvents: true,
61 | errorOnCreate: null,
62 | errorOnFind: null,
63 | errorOnGet: null,
64 | errorOnPatch: null,
65 | errorOnRemove: null,
66 | errorOnUpdate: null,
67 | idField: 'id',
68 | tempIdField: '__id',
69 | ids: [],
70 | isCreatePending: false,
71 | isFindPending: false,
72 | isGetPending: false,
73 | isPatchPending: false,
74 | isRemovePending: false,
75 | isUpdatePending: false,
76 | keepCopiesInStore: false,
77 | debounceEventsTime: null,
78 | debounceEventsMaxWait: 1000,
79 | keyedById: {},
80 | modelName: 'Todo',
81 | nameStyle: 'short',
82 | namespace: 'todos',
83 | pagination: {
84 | defaultLimit: null,
85 | defaultSkip: null
86 | },
87 | paramsForServer: ['$populateParams'],
88 | preferUpdate: false,
89 | replaceItems: false,
90 | serverAlias,
91 | servicePath: 'todos',
92 | skipRequestIfExists: false,
93 | tempsById: {},
94 | whitelist: [],
95 | isIdCreatePending: [],
96 | isIdUpdatePending: [],
97 | isIdPatchPending: [],
98 | isIdRemovePending: [],
99 | }
100 |
101 | assert.deepEqual(
102 | todoState,
103 | virginState,
104 | 'vuex module state is correct on first initialization'
105 | )
106 | assert.deepEqual(
107 | models[serverAlias][Todo.name].store.state[Todo.namespace],
108 | todoState,
109 | 'model state is the same as vuex module state on first initialization'
110 | )
111 |
112 | // Simulate some mutations on the store.
113 | const todo = {
114 | id: 1,
115 | testProp: true
116 | }
117 |
118 | store.commit('todos/addItem', todo)
119 | const serviceTodo = store.state['todos'].keyedById[1]
120 |
121 | assert.equal(
122 | todo.testProp,
123 | serviceTodo.testProp,
124 | 'todo is added to the store'
125 | )
126 |
127 | assert.deepEqual(
128 | models[serverAlias][Todo.name].store.state[Todo.namespace],
129 | todoState,
130 | 'model state is the same as vuex module state when store is mutated'
131 | )
132 |
133 | // Here we are going to simulate the make service plugin being reinitialized.
134 | // This is the default behaviour in SSR setups, e.g. nuxt universal mode,
135 | // although unlikely in SPAs.
136 | store = new Vuex.Store({
137 | plugins: [todosPlugin]
138 | })
139 |
140 | todoState = store.state['todos']
141 |
142 | // We expect vuex module state for this service to be reset.
143 | assert.deepEqual(
144 | todoState,
145 | virginState,
146 | 'store state in vuex module is not preserved on reinitialization'
147 | )
148 | // We also expect model store state for this service to be reset.
149 | assert.deepEqual(
150 | models[serverAlias][Todo.name].store.state[Todo.namespace],
151 | virginState,
152 | 'store state in service model is not preserved on reinitialization'
153 | )
154 | })
155 | })
156 |
--------------------------------------------------------------------------------
/test/service-module/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0
5 | */
6 | export interface ServiceState {
7 | options: {}
8 | ids: (string | number)[]
9 | autoRemove: boolean
10 | errorOnFind: any
11 | errorOnGet: any
12 | errorOnCreate: any
13 | errorOnPatch: any
14 | errorOnUpdate: any
15 | errorOnRemove: any
16 | isFindPending: boolean
17 | isGetPending: boolean
18 | isCreatePending: boolean
19 | isPatchPending: boolean
20 | isUpdatePending: boolean
21 | isRemovePending: boolean
22 | idField: string
23 | keyedById: {}
24 | tempsById: {}
25 | tempsByNewId: {}
26 | whitelist: string[]
27 | paramsForServer: string[]
28 | namespace: string
29 | nameStyle: string // Should be enum of 'short' or 'path'
30 | pagination?: {
31 | default: PaginationState
32 | }
33 | modelName: string
34 | }
35 |
36 | export interface PaginationState {
37 | ids: any
38 | limit: number
39 | skip: number
40 | ip: number
41 | total: number
42 | mostRecent: any
43 | }
44 |
45 | export interface Location {
46 | coordinates: number[]
47 | }
48 |
--------------------------------------------------------------------------------
/test/test-utils.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0
5 | */
6 | import { assert } from 'chai'
7 |
8 | export function assertGetter(item, prop, value) {
9 | assert(
10 | typeof Object.getOwnPropertyDescriptor(item, prop).get === 'function',
11 | 'getter in place'
12 | )
13 | assert.equal(item[prop], value, 'returned value matches')
14 | }
15 |
16 | export const makeStore = () => {
17 | return {
18 | 0: { id: 0, description: 'Do the first', isComplete: false },
19 | 1: { id: 1, description: 'Do the second', isComplete: false },
20 | 2: { id: 2, description: 'Do the third', isComplete: false },
21 | 3: { id: 3, description: 'Do the fourth', isComplete: false },
22 | 4: { id: 4, description: 'Do the fifth', isComplete: false },
23 | 5: { id: 5, description: 'Do the sixth', isComplete: false },
24 | 6: { id: 6, description: 'Do the seventh', isComplete: false },
25 | 7: { id: 7, description: 'Do the eighth', isComplete: false },
26 | 8: { id: 8, description: 'Do the ninth', isComplete: false },
27 | 9: { id: 9, description: 'Do the tenth', isComplete: false }
28 | }
29 | }
30 |
31 | export const makeStoreWithAtypicalIds = () => {
32 | return {
33 | 0: { someId: 0, description: 'Do the first', isComplete: false },
34 | 1: { someId: 1, description: 'Do the second', isComplete: false },
35 | 2: { someId: 2, description: 'Do the third', isComplete: false },
36 | 3: { someId: 3, description: 'Do the fourth', isComplete: false },
37 | 4: { someId: 4, description: 'Do the fifth', isComplete: false },
38 | 5: { someId: 5, description: 'Do the sixth', isComplete: false },
39 | 6: { someId: 6, description: 'Do the seventh', isComplete: false },
40 | 7: { someId: 7, description: 'Do the eighth', isComplete: false },
41 | 8: { someId: 8, description: 'Do the ninth', isComplete: false },
42 | 9: { someId: 9, description: 'Do the tenth', isComplete: false }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/test/use/InstrumentComponent.js:
--------------------------------------------------------------------------------
1 | import useGet from '../../src/useGet'
2 |
3 | export default {
4 | name: 'InstrumentComponent',
5 | template: ' {{ instrument }}
',
6 | props: {
7 | id: {
8 | type: String,
9 | default: ''
10 | }
11 | },
12 | setup(props, context) {
13 | const { Instrument } = context.root.$FeathersVuex
14 |
15 | const instrumentData = useGet({ model: Instrument, id: props.id })
16 |
17 | return {
18 | instrument: instrumentData.item
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/test/use/find.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0,
5 | @typescript-eslint/no-empty-function: 0
6 | */
7 | import Vue from 'vue'
8 | import VueCompositionApi from '@vue/composition-api'
9 | Vue.use(VueCompositionApi)
10 |
11 | import jsdom from 'jsdom-global'
12 | import { assert } from 'chai'
13 | import feathersVuex, { FeathersVuex } from '../../src/index'
14 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client'
15 | import useFind from '../../src/useFind'
16 | import Vuex from 'vuex'
17 | // import { shallowMount } from '@vue/test-utils'
18 | import { computed, isRef } from '@vue/composition-api'
19 | jsdom()
20 | require('events').EventEmitter.prototype._maxListeners = 100
21 |
22 | Vue.use(Vuex)
23 | Vue.use(FeathersVuex)
24 |
25 | function makeContext() {
26 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, {
27 | serverAlias: 'useFind'
28 | })
29 |
30 | class Instrument extends BaseModel {
31 | public static modelName = 'Instrument'
32 | }
33 |
34 | const serviceName = 'things'
35 | const store = new Vuex.Store({
36 | plugins: [
37 | makeServicePlugin({
38 | Model: Instrument,
39 | service: feathersClient.service(serviceName)
40 | })
41 | ]
42 | })
43 | return { store, Instrument, BaseModel, makeServicePlugin }
44 | }
45 |
46 | describe('use/find', function () {
47 | it('returns correct default data', function () {
48 | const { Instrument } = makeContext()
49 |
50 | const instrumentParams = computed(() => {
51 | return {
52 | query: {},
53 | paginate: false
54 | }
55 | })
56 | const instrumentsData = useFind({
57 | model: Instrument,
58 | params: instrumentParams
59 | })
60 |
61 | const {
62 | debounceTime,
63 | error,
64 | haveBeenRequested,
65 | haveLoaded,
66 | isPending,
67 | isLocal,
68 | items,
69 | latestQuery,
70 | paginationData,
71 | qid
72 | } = instrumentsData
73 |
74 | assert(isRef(debounceTime))
75 | assert(debounceTime.value === null)
76 |
77 | assert(isRef(error))
78 | assert(error.value === null)
79 |
80 | assert(isRef(haveBeenRequested))
81 | assert(haveBeenRequested.value === true)
82 |
83 | assert(isRef(haveLoaded))
84 | assert(haveLoaded.value === false)
85 |
86 | assert(isRef(isPending))
87 | assert(isPending.value === true)
88 |
89 | assert(isRef(isLocal))
90 | assert(isLocal.value === false)
91 |
92 | assert(isRef(items))
93 | assert(Array.isArray(items.value))
94 | assert(items.value.length === 0)
95 |
96 | assert(isRef(latestQuery))
97 | assert(latestQuery.value === null)
98 |
99 | assert(isRef(paginationData))
100 | assert.deepStrictEqual(paginationData.value, {
101 | defaultLimit: null,
102 | defaultSkip: null
103 | })
104 |
105 | assert(isRef(qid))
106 | assert(qid.value === 'default')
107 | })
108 |
109 | it.skip('returns correct default data even when params is not reactive', function () {
110 | const { Instrument } = makeContext()
111 |
112 | const instrumentsData = useFind({
113 | model: Instrument,
114 | params: {
115 | query: {},
116 | paginate: false
117 | }
118 | })
119 |
120 | const {
121 | debounceTime,
122 | error,
123 | haveBeenRequested,
124 | haveLoaded,
125 | isPending,
126 | isLocal,
127 | items,
128 | latestQuery,
129 | paginationData,
130 | qid
131 | } = instrumentsData
132 |
133 | assert(isRef(debounceTime))
134 | assert(debounceTime.value === null)
135 |
136 | assert(isRef(error))
137 | assert(error.value === null)
138 |
139 | assert(isRef(haveBeenRequested))
140 | assert(haveBeenRequested.value === true)
141 |
142 | assert(isRef(haveLoaded))
143 | assert(haveLoaded.value === false)
144 |
145 | assert(isRef(isPending))
146 | assert(isPending.value === true)
147 |
148 | assert(isRef(isLocal))
149 | assert(isLocal.value === false)
150 |
151 | assert(isRef(items))
152 | assert(Array.isArray(items.value))
153 | assert(items.value.length === 0)
154 |
155 | assert(isRef(latestQuery))
156 | assert(latestQuery.value === null)
157 |
158 | assert(isRef(paginationData))
159 | assert.deepStrictEqual(paginationData.value, {
160 | defaultLimit: null,
161 | defaultSkip: null
162 | })
163 |
164 | assert(isRef(qid))
165 | assert(qid.value === 'default')
166 | })
167 |
168 | it('allows passing {immediate:false} to not query immediately', function () {
169 | const { Instrument } = makeContext()
170 |
171 | const instrumentParams = computed(() => {
172 | return {
173 | query: {},
174 | paginate: false
175 | }
176 | })
177 | const instrumentsData = useFind({
178 | model: Instrument,
179 | params: instrumentParams,
180 | immediate: false
181 | })
182 | const { haveBeenRequested } = instrumentsData
183 |
184 | assert(isRef(haveBeenRequested))
185 | assert(haveBeenRequested.value === false)
186 | })
187 |
188 | it('params can return null to prevent the query', function () {
189 | const { Instrument } = makeContext()
190 |
191 | const instrumentParams = computed(() => {
192 | return null
193 | })
194 | const instrumentsData = useFind({
195 | model: Instrument,
196 | params: instrumentParams,
197 | immediate: true
198 | })
199 | const { haveBeenRequested } = instrumentsData
200 |
201 | assert(isRef(haveBeenRequested))
202 | assert(haveBeenRequested.value === false)
203 | })
204 |
205 | it('allows using `local: true` to prevent API calls from being made', function () {
206 | const { Instrument } = makeContext()
207 |
208 | const instrumentParams = computed(() => {
209 | return {
210 | query: {}
211 | }
212 | })
213 | const instrumentsData = useFind({
214 | model: Instrument,
215 | params: instrumentParams,
216 | local: true
217 | })
218 | const { haveBeenRequested, find } = instrumentsData
219 |
220 | assert(isRef(haveBeenRequested))
221 | assert(haveBeenRequested.value === false, 'no request during init')
222 |
223 | find()
224 |
225 | assert(haveBeenRequested.value === false, 'no request after find')
226 | })
227 | })
228 |
--------------------------------------------------------------------------------
/test/use/get.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0,
5 | @typescript-eslint/no-empty-function: 0
6 | */
7 | import Vue from 'vue'
8 | import VueCompositionApi from '@vue/composition-api'
9 | Vue.use(VueCompositionApi)
10 |
11 | import jsdom from 'jsdom-global'
12 | import { assert } from 'chai'
13 | import feathersVuex, { FeathersVuex } from '../../src/index'
14 | import { feathersRestClient as feathersClient } from '../fixtures/feathers-client'
15 | import useGet from '../../src/useGet'
16 | import memory from 'feathers-memory'
17 | import Vuex from 'vuex'
18 | // import { mount, shallowMount } from '@vue/test-utils'
19 | // import InstrumentComponent from './InstrumentComponent'
20 | import { isRef } from '@vue/composition-api'
21 | import { HookContext } from '@feathersjs/feathers'
22 | jsdom()
23 | require('events').EventEmitter.prototype._maxListeners = 100
24 |
25 | Vue.use(Vuex)
26 | Vue.use(FeathersVuex)
27 |
28 | // function timeoutPromise(wait = 0) {
29 | // return new Promise(resolve => {
30 | // setTimeout(() => {
31 | // resolve()
32 | // }, wait)
33 | // })
34 | // }
35 |
36 | function makeContext() {
37 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, {
38 | serverAlias: 'useGet'
39 | })
40 |
41 | class Instrument extends BaseModel {
42 | public constructor(data, options?) {
43 | super(data, options)
44 | }
45 | public static modelName = 'Instrument'
46 | public static instanceDefaults(data) {
47 | return {
48 | name: ''
49 | }
50 | }
51 | }
52 |
53 | feathersClient.use(
54 | 'things',
55 | memory({
56 | store: {
57 | 0: { id: 0, name: 'trumpet' },
58 | 1: { id: 1, name: 'trombone' }
59 | },
60 | paginate: {
61 | default: 10,
62 | max: 50
63 | }
64 | })
65 | )
66 |
67 | const servicePath = 'instruments'
68 | const store = new Vuex.Store({
69 | plugins: [
70 | makeServicePlugin({
71 | Model: Instrument,
72 | servicePath,
73 | service: feathersClient.service(servicePath)
74 | })
75 | ]
76 | })
77 | return { store, Instrument, BaseModel, makeServicePlugin }
78 | }
79 |
80 | describe('use/get', function () {
81 | it('returns correct default data', function () {
82 | const { Instrument } = makeContext()
83 |
84 | const id = 1
85 |
86 | const existing = Instrument.getFromStore(id)
87 | assert(!existing, 'the current instrument is not in the store.')
88 |
89 | const instrumentData = useGet({ model: Instrument, id })
90 |
91 | const {
92 | error,
93 | hasBeenRequested,
94 | hasLoaded,
95 | isPending,
96 | isLocal,
97 | item
98 | } = instrumentData
99 |
100 | assert(isRef(error))
101 | assert(error.value === null)
102 |
103 | assert(isRef(hasBeenRequested))
104 | assert(hasBeenRequested.value === true)
105 |
106 | assert(isRef(hasLoaded))
107 | assert(hasLoaded.value === false)
108 |
109 | assert(isRef(isPending))
110 | assert(isPending.value === true)
111 |
112 | assert(isRef(isLocal))
113 | assert(isLocal.value === false)
114 |
115 | assert(isRef(item))
116 | assert(item.value === null)
117 | })
118 |
119 | it('allows passing {immediate:false} to not query immediately', function () {
120 | const { Instrument } = makeContext()
121 |
122 | const id = 1
123 | const instrumentData = useGet({ model: Instrument, id, immediate: false })
124 | const { hasBeenRequested } = instrumentData
125 |
126 | assert(isRef(hasBeenRequested))
127 | assert(hasBeenRequested.value === false)
128 | })
129 |
130 | it('id can return null id to prevent the query', function () {
131 | const { Instrument } = makeContext()
132 |
133 | const id = null
134 | const instrumentData = useGet({ model: Instrument, id })
135 | const { hasBeenRequested } = instrumentData
136 |
137 | assert(isRef(hasBeenRequested))
138 | assert(hasBeenRequested.value === false)
139 | })
140 |
141 | it('allows using `local: true` to prevent API calls from being made', function () {
142 | const { Instrument } = makeContext()
143 |
144 | const id = 1
145 | const instrumentData = useGet({ model: Instrument, id, local: true })
146 | const { hasBeenRequested, get } = instrumentData
147 |
148 | assert(isRef(hasBeenRequested))
149 | assert(hasBeenRequested.value === false, 'no request during init')
150 |
151 | get(id)
152 |
153 | assert(hasBeenRequested.value === false, 'no request after get')
154 | })
155 |
156 | it('API only hit once on initial render', async function () {
157 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, {
158 | serverAlias: 'useGet'
159 | })
160 |
161 | class Dohickey extends BaseModel {
162 | public static modelName = 'Dohickey'
163 | }
164 |
165 | const servicePath = 'dohickies'
166 | const store = new Vuex.Store({
167 | plugins: [
168 | makeServicePlugin({
169 | Model: Dohickey,
170 | servicePath,
171 | service: feathersClient.service(servicePath)
172 | })
173 | ]
174 | })
175 |
176 | let getCalls = 0
177 | feathersClient.service(servicePath).hooks({
178 | before: {
179 | get: [
180 | (ctx: HookContext) => {
181 | getCalls += 1
182 | ctx.result = { id: ctx.id }
183 | }
184 | ]
185 | }
186 | })
187 |
188 | useGet({ model: Dohickey, id: 42 })
189 | await new Promise((resolve) => setTimeout(resolve, 100))
190 |
191 | assert(getCalls === 1, '`get` called once')
192 | })
193 | })
194 |
--------------------------------------------------------------------------------
/test/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai'
2 | import { AuthState } from '../src/auth-module/types'
3 | import { ServiceState } from './service-module/types'
4 | import { isNode, isBrowser } from '../src/utils'
5 | import { diff as deepDiff } from 'deep-object-diff'
6 | import {
7 | getId,
8 | initAuth,
9 | hydrateApi,
10 | getServicePrefix,
11 | getServiceCapitalization,
12 | getQueryInfo
13 | } from '../src/utils'
14 | import feathersVuex from '../src/index'
15 | import { feathersSocketioClient as feathersClient } from './fixtures/feathers-client'
16 | import Vue from 'vue'
17 | import Vuex from 'vuex'
18 |
19 | Vue.use(Vuex)
20 |
21 | interface RootState {
22 | auth: AuthState
23 | users: ServiceState
24 | }
25 |
26 | describe('Utils', function () {
27 | describe('getId', () => {
28 | const idField = '_id'
29 | it('converts objects to strings', () => {
30 | const _id = { test: true }
31 | const id = getId({ _id }, idField)
32 | assert.strictEqual(typeof id, 'string')
33 | assert.strictEqual(id, _id.toString())
34 | })
35 | it('does not convert number ids', () => {
36 | const _id = 1
37 | const id = getId({ _id }, idField)
38 | assert.strictEqual(typeof id, 'number')
39 | assert.strictEqual(id, _id)
40 | })
41 | it('automatically finds _id', () => {
42 | const _id = 1
43 | const id = getId({ _id })
44 | assert.strictEqual(id, _id)
45 | })
46 | it('automatically finds id', () => {
47 | const referenceId = 1
48 | const id = getId({ id: referenceId })
49 | assert.strictEqual(id, referenceId)
50 | })
51 | it('prefers id over _id (only due to their order in the code)', () => {
52 | const _id = 1
53 | const referenceId = 2
54 | const id = getId({ _id, id: referenceId })
55 | assert.strictEqual(id, referenceId)
56 | })
57 | })
58 |
59 | describe('Auth & SSR', () => {
60 | before(function () {
61 | const {
62 | makeServicePlugin,
63 | makeAuthPlugin,
64 | BaseModel
65 | } = feathersVuex(feathersClient, { serverAlias: 'utils' })
66 |
67 | class User extends BaseModel {
68 | public static modelName = 'User'
69 | public static test = true
70 | }
71 |
72 | Object.assign(this, {
73 | makeServicePlugin,
74 | makeAuthPlugin,
75 | BaseModel,
76 | User
77 | })
78 | })
79 | it('properly populates auth', function () {
80 | const store = new Vuex.Store({
81 | plugins: [
82 | this.makeServicePlugin({
83 | Model: this.User,
84 | servicePath: 'users',
85 | service: feathersClient.service('users')
86 | }),
87 | this.makeAuthPlugin({})
88 | ]
89 | })
90 | const accessToken =
91 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoiOTk5OTk5OTk5OTkiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.lUlEd3xH-TnlNRbKM3jnDVTNoIg10zgzaS6QyFZE-6g'
92 | const req = {
93 | headers: {
94 | cookie: 'feathers-jwt=' + accessToken
95 | }
96 | }
97 | return initAuth({
98 | commit: store.commit,
99 | req,
100 | moduleName: 'auth',
101 | cookieName: 'feathers-jwt',
102 | feathersClient
103 | })
104 | .then(() => {
105 | assert(
106 | store.state.auth.accessToken === accessToken,
107 | 'the token was in place'
108 | )
109 | assert(store.state.auth.payload, 'the payload was set')
110 | return feathersClient.authentication.getAccessToken()
111 | })
112 | .then(token => {
113 | assert.isDefined(token, 'the feathers client storage was set')
114 | })
115 | })
116 |
117 | it('properly hydrate SSR store', function () {
118 | const {
119 | makeServicePlugin,
120 | BaseModel,
121 | models
122 | } = feathersVuex(feathersClient, { serverAlias: 'hydrate' })
123 |
124 | class User extends BaseModel {
125 | public static modelName = 'User'
126 | public static test = true
127 | }
128 |
129 | const store = new Vuex.Store({
130 | plugins: [
131 | makeServicePlugin({
132 | Model: User,
133 | servicePath: 'users',
134 | service: feathersClient.service('users'),
135 | mutations: {
136 | addServerItem(state) {
137 | state.keyedById['abcdefg'] = { id: 'abcdefg', name: 'Guzz' }
138 | }
139 | }
140 | })
141 | ]
142 | })
143 | store.commit('users/addServerItem')
144 | assert(store.state.users.keyedById['abcdefg'], 'server document added')
145 | assert(
146 | store.state.users.keyedById['abcdefg'] instanceof Object,
147 | 'server document is pure javascript object'
148 | )
149 | hydrateApi({ api: models.hydrate })
150 | assert(
151 | store.state.users.keyedById['abcdefg'] instanceof User,
152 | 'document hydrated'
153 | )
154 | })
155 | })
156 |
157 | describe('Inflections', function () {
158 | it('properly inflects the service prefix', function () {
159 | const decisionTable = [
160 | ['todos', 'todos'],
161 | ['TODOS', 'tODOS'],
162 | ['environment-Panos', 'environmentPanos'],
163 | ['env-panos', 'envPanos'],
164 | ['envPanos', 'envPanos'],
165 | ['api/v1/env-panos', 'envPanos'],
166 | ['very-long-service', 'veryLongService']
167 | ]
168 | decisionTable.forEach(([path, prefix]) => {
169 | assert(
170 | getServicePrefix(path) === prefix,
171 | `The service prefix for path "${path}" was "${getServicePrefix(
172 | path
173 | )}", expected "${prefix}"`
174 | )
175 | })
176 | })
177 |
178 | it('properly inflects the service capitalization', function () {
179 | const decisionTable = [
180 | ['todos', 'Todos'],
181 | ['TODOS', 'TODOS'],
182 | ['environment-Panos', 'EnvironmentPanos'],
183 | ['env-panos', 'EnvPanos'],
184 | ['envPanos', 'EnvPanos'],
185 | ['api/v1/env-panos', 'EnvPanos'],
186 | ['very-long-service', 'VeryLongService']
187 | ]
188 | decisionTable.forEach(([path, prefix]) => {
189 | assert(
190 | getServiceCapitalization(path) === prefix,
191 | `The service prefix for path "${path}" was "${getServiceCapitalization(
192 | path
193 | )}", expected "${prefix}"`
194 | )
195 | })
196 | })
197 | })
198 |
199 | describe('Environments', () => {
200 | it('sets isNode to true', () => {
201 | assert(isNode, 'isNode was true')
202 | })
203 |
204 | it('sets isBrowser to false', () => {
205 | assert(!isBrowser, 'isBrowser was false')
206 | })
207 | })
208 | })
209 |
210 | describe('Pagination', function () {
211 | it('getQueryInfo', function () {
212 | const params = {
213 | qid: 'main-list',
214 | query: {
215 | test: true,
216 | $limit: 10,
217 | $skip: 0
218 | }
219 | }
220 | const response = {
221 | data: [],
222 | limit: 10,
223 | skip: 0,
224 | total: 500
225 | }
226 | const info = getQueryInfo(params, response)
227 | const expected = {
228 | isOutdated: undefined,
229 | qid: 'main-list',
230 | query: {
231 | test: true,
232 | $limit: 10,
233 | $skip: 0
234 | },
235 | queryId: '{"test":true}',
236 | queryParams: {
237 | test: true
238 | },
239 | pageParams: {
240 | $limit: 10,
241 | $skip: 0
242 | },
243 | pageId: '{"$limit":10,"$skip":0}',
244 | response: undefined
245 | }
246 | const diff = deepDiff(info, expected)
247 |
248 | assert.deepEqual(info, expected, 'query info formatted correctly')
249 | })
250 |
251 | it('getQueryInfo no limit or skip', function () {
252 | const params = {
253 | qid: 'main-list',
254 | query: {
255 | test: true
256 | }
257 | }
258 | const response = {
259 | data: [],
260 | limit: 10,
261 | skip: 0,
262 | total: 500
263 | }
264 | const info = getQueryInfo(params, response)
265 | const expected = {
266 | isOutdated: undefined,
267 | qid: 'main-list',
268 | query: {
269 | test: true
270 | },
271 | queryId: '{"test":true}',
272 | queryParams: {
273 | test: true
274 | },
275 | pageParams: {
276 | $limit: 10,
277 | $skip: 0
278 | },
279 | pageId: '{"$limit":10,"$skip":0}',
280 | response: undefined
281 | }
282 | const diff = deepDiff(info, expected)
283 |
284 | assert.deepEqual(info, expected, 'query info formatted correctly')
285 | })
286 | })
287 |
--------------------------------------------------------------------------------
/test/vue-plugin.test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | eslint
3 | @typescript-eslint/explicit-function-return-type: 0,
4 | @typescript-eslint/no-explicit-any: 0
5 | */
6 | import { assert } from 'chai'
7 | import feathersVuex, { FeathersVuex } from '../src/index'
8 | import { feathersRestClient as feathersClient } from './fixtures/feathers-client'
9 | import Vue from 'vue/dist/vue'
10 | import Vuex from 'vuex'
11 |
12 | // @ts-ignore
13 | Vue.use(Vuex)
14 | // @ts-ignore
15 | Vue.use(FeathersVuex)
16 |
17 | interface VueWithFeathers {
18 | $FeathersVuex: {}
19 | }
20 |
21 | function makeContext() {
22 | const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, {
23 | serverAlias: 'make-find-mixin'
24 | })
25 | class FindModel extends BaseModel {
26 | public static modelName = 'FindModel'
27 | public static test: boolean = true
28 | }
29 |
30 | const serviceName = 'todos'
31 | const store = new Vuex.Store({
32 | plugins: [
33 | makeServicePlugin({
34 | Model: FindModel,
35 | service: feathersClient.service(serviceName)
36 | })
37 | ]
38 | })
39 | return {
40 | store
41 | }
42 | }
43 |
44 | describe('Vue Plugin', function () {
45 | it('Adds the `$FeathersVuex` object to components', function () {
46 | const { store } = makeContext()
47 | const vm = new Vue({
48 | name: 'todos-component',
49 | store,
50 | template: ``
51 | }).$mount()
52 |
53 | assert(vm.$FeathersVuex, 'registeredPlugin correctly')
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "esModuleInterop": true,
5 | "outDir": "dist",
6 | "moduleResolution": "node",
7 | "target": "es6",
8 | "sourceMap": false,
9 | "declaration": true
10 | },
11 | "include": ["src/**/*"],
12 | "exclude": ["node_modules", "**/*.test.js"]
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "esModuleInterop": true,
5 | "outDir": "dist",
6 | "moduleResolution": "node",
7 | "target": "esnext",
8 | "sourceMap": true,
9 | "allowJs": true
10 | },
11 | "include": ["src/**/*"],
12 | "exclude": ["node_modules", "**/*.test.js"]
13 | }
14 |
--------------------------------------------------------------------------------