├── .eslintignore ├── test ├── helpers │ ├── userConfig.js │ ├── config.js │ ├── index.js │ ├── store │ │ ├── friendsList.js │ │ ├── user.js │ │ ├── dex.js │ │ ├── locationJournal.js │ │ └── index.js │ ├── pathUtils.cjs.js │ └── makeSetters.cjs.js ├── ignoreProps.js ├── storeSetup.js ├── pathUtils.js ├── defaultDeletor.js ├── firestore.js ├── defaultSetter.js └── actions.js ├── .gitattributes ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── types ├── errors.d.ts ├── declarations.d.ts ├── defaultConfig.d.ts ├── makeGetters.d.ts ├── makeMutations.d.ts ├── index.d.ts ├── makeSetters.d.ts └── pathUtils.d.ts ├── src ├── defaultConfig.ts ├── declarations.ts ├── makeGetters.ts ├── index.ts ├── errors.ts ├── pathUtils.ts ├── makeMutations.ts └── makeSetters.ts ├── wallaby.conf.js ├── docs ├── .vuepress │ ├── deploy.sh │ ├── config.js │ └── styles │ │ └── index.styl ├── setup.md ├── feedback.md ├── reference.md ├── README.md ├── hooks.md ├── advanced.md └── guide.md ├── .github └── FUNDING.yml ├── LICENSE ├── .eslintrc.js ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /test/helpers/userConfig.js: -------------------------------------------------------------------------------- 1 | // Make tests for the user config object -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | docs/.vuepress/dist 3 | .rpt2_cache 4 | .vscode 5 | .cache 6 | -------------------------------------------------------------------------------- /test/helpers/config.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | ignoreProps: ['user/importedData', 'user/user.secretProp', 'wallet'], 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "declaration": true, 5 | "declarationDir": "./types/", 6 | "target": "es5", 7 | "lib": ["es5", "es2015", "es2016", "es2017", "dom"] 8 | }, 9 | "include": [ 10 | "src/**/*", 11 | "test/**/*" 12 | ] 13 | } -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import storeObj from './store' 4 | 5 | // create store 6 | Vue.use(Vuex) 7 | const store = new Vuex.Store(storeObj) 8 | // console.log('store.set → ', store.set) 9 | // console.log('store.get → ', store.get) 10 | export default store 11 | -------------------------------------------------------------------------------- /types/errors.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error logging 3 | * 4 | * @export 5 | * @param {string} error the error code 6 | * @param {object} conf the user config 7 | * @param {string} [path] (optional) the path the error occured in 8 | * @param {string} [props] (optional) the props the error occured with 9 | * @returns {string} the error code 10 | */ 11 | export default function (error: string, conf?: object, path?: string, props?: string): string; 12 | -------------------------------------------------------------------------------- /test/helpers/store/friendsList.js: -------------------------------------------------------------------------------- 1 | import { defaultMutations } from '../../../dist/index.cjs' 2 | import config from '../config' 3 | // MODULE: dex 4 | 5 | const state = { 6 | '*': { 7 | name: '', 8 | online: false, 9 | tags: { 10 | '*': true 11 | } 12 | } 13 | } 14 | 15 | export const friendsList = { 16 | namespaced: true, 17 | state: state, 18 | mutations: defaultMutations(state, config, {moduleNamespace: 'friendsList/'}) 19 | } 20 | -------------------------------------------------------------------------------- /src/defaultConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IDefaultConfig { 3 | setter?: string 4 | getter?: string 5 | deletor?: string 6 | vuexEasyFirestore?: boolean 7 | ignorePrivateProps?: boolean 8 | ignoreProps?: string[] 9 | pattern?: string 10 | } 11 | 12 | export default { 13 | setter: 'set', 14 | getter: 'get', 15 | deletor: 'delete', 16 | vuexEasyFirestore: false, 17 | ignorePrivateProps: true, 18 | ignoreProps: [], 19 | pattern: 'simple' 20 | } 21 | -------------------------------------------------------------------------------- /src/declarations.ts: -------------------------------------------------------------------------------- 1 | 2 | export { IDefaultConfig } from './defaultConfig' 3 | 4 | export type AnyObject = {[key: string]: any} 5 | 6 | export interface IInitialisedStore { 7 | state: AnyObject 8 | getters: AnyObject 9 | dispatch: (path: string, payload: any) => any 10 | commit: (path: string, payload: any) => any | void 11 | _actions: AnyObject 12 | _mutations: AnyObject 13 | _modulesNamespaceMap: AnyObject 14 | registerModule: (name: string | string[], module: any) => any 15 | } 16 | -------------------------------------------------------------------------------- /types/declarations.d.ts: -------------------------------------------------------------------------------- 1 | export { IDefaultConfig } from './defaultConfig'; 2 | export declare type AnyObject = { 3 | [key: string]: any; 4 | }; 5 | export interface IInitialisedStore { 6 | state: AnyObject; 7 | getters: AnyObject; 8 | dispatch: (path: string, payload: any) => any; 9 | commit: (path: string, payload: any) => any | void; 10 | _actions: AnyObject; 11 | _mutations: AnyObject; 12 | _modulesNamespaceMap: AnyObject; 13 | registerModule: (name: string | string[], module: any) => any; 14 | } 15 | -------------------------------------------------------------------------------- /types/defaultConfig.d.ts: -------------------------------------------------------------------------------- 1 | export interface IDefaultConfig { 2 | setter?: string; 3 | getter?: string; 4 | deletor?: string; 5 | vuexEasyFirestore?: boolean; 6 | ignorePrivateProps?: boolean; 7 | ignoreProps?: string[]; 8 | pattern?: string; 9 | } 10 | declare const _default: { 11 | setter: string; 12 | getter: string; 13 | deletor: string; 14 | vuexEasyFirestore: boolean; 15 | ignorePrivateProps: boolean; 16 | ignoreProps: any[]; 17 | pattern: string; 18 | }; 19 | export default _default; 20 | -------------------------------------------------------------------------------- /wallaby.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | return { 3 | files: [ 4 | 'src/**/*.ts', 5 | 'dist/**/*.js' 6 | ], 7 | tests: [ 8 | 'test/**/*.js' 9 | ], 10 | env: { 11 | type: 'node', 12 | runner: 'node' 13 | }, 14 | compilers: { 15 | '**/*.+(js|ts)': wallaby.compilers.typeScript({allowJs: true, outDir: './bin'}) 16 | }, 17 | preprocessors: { 18 | '**/*.jsts': file => file.changeExt('js').content 19 | }, 20 | testFramework: 'ava', 21 | debug: true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/.vuepress/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # abort on errors 4 | set -e 5 | 6 | # build 7 | npm run docs:build 8 | 9 | # navigate into the build output directory 10 | cd docs/.vuepress/dist 11 | 12 | # if you are deploying to a custom domain 13 | # echo 'www.example.com' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # if you are deploying to https://.github.io 20 | # git push -f git@github.com:/.github.io.git master 21 | 22 | # if you are deploying to https://.github.io/ 23 | git push -f git@github.com:mesqueeb/vuex-easy-access.git master:gh-pages 24 | 25 | cd - -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mesqueeb 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /types/makeGetters.d.ts: -------------------------------------------------------------------------------- 1 | import { IInitialisedStore } from './declarations'; 2 | /** 3 | * Creates a getter function in the store to set any state value. 4 | * Usage: 5 | * `get('module/path/path.to.prop')` 6 | * it will check first for existence of: `getters['module/path/path.to.prop']` 7 | * if non existant it will return: `state.module.path.path.to.prop` 8 | * Import method: 9 | * `store.get = (path) => { return defaultGetter(path, store) }` 10 | * 11 | * @param {string} path the path of the prop to get eg. 'info/user/favColours.primary' 12 | * @param {IInitialisedStore} store the store to attach 13 | * @returns {*} getter or state 14 | */ 15 | declare function defaultGetter(path: string, store: IInitialisedStore): any; 16 | export { defaultGetter }; 17 | -------------------------------------------------------------------------------- /types/makeMutations.d.ts: -------------------------------------------------------------------------------- 1 | import { IDefaultConfig, AnyObject } from './declarations'; 2 | interface IInfoNS { 3 | moduleNamespace: string; 4 | } 5 | /** 6 | * Creates all mutations for the state of a module. 7 | * Usage: 8 | * commit('module/path/SET_PATH.TO.PROP', newValue) 9 | * Import method: 10 | * mutations { 11 | * ...defaultMutations(initialState) 12 | * } 13 | * 14 | * @param {object} initialState the initial state of a module 15 | * @param {IDefaultConfig} [userConf={}] (optional) user config 16 | * @param {IInfoNS} [infoNS] (optional) info on the module namespace 17 | * @returns {AnyObject} all mutations for the state 18 | */ 19 | export declare function defaultMutations(initialState: object, userConf?: IDefaultConfig, infoNS?: IInfoNS): AnyObject; 20 | export {}; 21 | -------------------------------------------------------------------------------- /test/helpers/store/user.js: -------------------------------------------------------------------------------- 1 | import { defaultMutations } from '../../../dist/index.cjs' 2 | import config from '../config' 3 | 4 | // MODULE: user 5 | const userState = { 6 | user: {secretProp: []}, 7 | importedData: [], 8 | wallet: [] 9 | } 10 | 11 | export const user = { 12 | namespaced: true, 13 | state: userState, 14 | actions: { 15 | wallet ({state}, newVal) { 16 | return newVal + '!' 17 | }, 18 | }, 19 | mutations: defaultMutations(userState, config, {moduleNamespace: 'user/'}) 20 | } 21 | 22 | export const firestoreUser = { 23 | // firestore settings 24 | firestorePath: 'fakePath', 25 | firestoreRefType: 'doc', 26 | moduleName: 'userDB', 27 | statePropName: 'user', 28 | // the rest 29 | namespaced: true, 30 | state: userState, 31 | mutations: defaultMutations(userState, config, {moduleNamespace: 'userDB/'}) 32 | } 33 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | title: 'Vuex Easy Access', 4 | description: 'Unified syntax for accessing your Vuex store through simple set() and get() functions + auto generate mutations.', 5 | base: '/vuex-easy-access/', 6 | plugins: [['@vuepress/google-analytics', {ga: 'UA-92965499-4'}]], 7 | themeConfig: { 8 | displayAllHeaders: true, 9 | sidebar: [ 10 | ['/', 'What is Vuex Easy Access?'], 11 | '/setup', 12 | '/guide', 13 | '/advanced', 14 | '/hooks', 15 | '/reference', 16 | '/feedback', 17 | ], 18 | nav: [ 19 | { text: 'Changelog', link: 'https://github.com/mesqueeb/vuex-easy-access/releases' }, 20 | ], 21 | repo: 'mesqueeb/vuex-easy-access', 22 | repoLabel: 'Github', 23 | docsDir: 'docs', 24 | docsBranch: 'dev', 25 | editLinks: true, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/helpers/store/dex.js: -------------------------------------------------------------------------------- 1 | import { defaultMutations } from '../../../dist/index.cjs' 2 | import config from '../config' 3 | 4 | // MODULE: dex 5 | const state = { 6 | pokemonById: { 7 | '*': { 8 | name: '', 9 | tags: { 10 | '*': true 11 | }, 12 | powerUps: [] 13 | } 14 | }, 15 | emptyObject: {}, 16 | propToBeDeleted: true 17 | } 18 | 19 | export const dex = { 20 | namespaced: true, 21 | state: state, 22 | mutations: defaultMutations(state, config, {moduleNamespace: 'dex/'}) 23 | } 24 | 25 | export const firestoreDex = { 26 | // firestore settings 27 | firestorePath: 'fakePath', 28 | firestoreRefType: 'collection', 29 | moduleName: 'dexDB', 30 | statePropName: 'pokemonById', 31 | // the rest 32 | namespaced: true, 33 | state: state, 34 | mutations: defaultMutations(state, config, {moduleNamespace: 'dexDB/'}) 35 | } 36 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | # Installation & setup 2 | 3 | ## Installation 4 | 5 | ```bash 6 | npm i --save vuex-easy-access vue 7 | # or 8 | yarn add vuex-easy-access vue 9 | ``` 10 | 11 | Vue is a peer dependency; It will use your existing version. 12 | 13 | ## Setup 14 | 15 | Add as vuex store plugin 16 | 17 | ```js 18 | import createEasyAccess from 'vuex-easy-access' 19 | // do the magic 🧙🏻‍♂️ 20 | const easyAccess = createEasyAccess() 21 | // and include as plugin in your vuex store: 22 | store: { 23 | // ... your store 24 | plugins: [easyAccess] 25 | } 26 | ``` 27 | 28 | Add as per module in the mutations 29 | 30 | ```js 31 | import { defaultMutations } from 'vuex-easy-access' 32 | // in the root or a module's mutations: 33 | mutations: { 34 | // your other mutations 35 | ...defaultMutations(state) 36 | // pass your state object 37 | } 38 | ``` 39 | 40 | That's it!! Simple and clean. ♫ 41 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Vuex Easy Access plugin 3 | * Unified syntax with simple set() and get() store access + auto generate mutations! 4 | * 5 | * @author Luca Ban 6 | * @contact https://lucaban.com 7 | */ 8 | import { IDefaultConfig } from './defaultConfig'; 9 | import { defaultMutations } from './makeMutations'; 10 | import { defaultGetter } from './makeGetters'; 11 | import { defaultSetter, defaultDeletor } from './makeSetters'; 12 | import { getDeepRef, getKeysFromPath } from './pathUtils'; 13 | /** 14 | * This will create the `/set/` sub-modules and add `set()` `get()` and `delete()` to the store object. 15 | * 16 | * @param {IDefaultConfig} [userConfig={}] 17 | * @returns {*} the store object 18 | */ 19 | declare function createEasyAccess(userConfig?: IDefaultConfig): any; 20 | export default createEasyAccess; 21 | export { createEasyAccess, defaultMutations, defaultGetter, defaultSetter, defaultDeletor, getDeepRef, getKeysFromPath }; 22 | -------------------------------------------------------------------------------- /src/makeGetters.ts: -------------------------------------------------------------------------------- 1 | import { getDeepValue } from './pathUtils' 2 | import { IInitialisedStore } from './declarations' 3 | 4 | /** 5 | * Creates a getter function in the store to set any state value. 6 | * Usage: 7 | * `get('module/path/path.to.prop')` 8 | * it will check first for existence of: `getters['module/path/path.to.prop']` 9 | * if non existant it will return: `state.module.path.path.to.prop` 10 | * Import method: 11 | * `store.get = (path) => { return defaultGetter(path, store) }` 12 | * 13 | * @param {string} path the path of the prop to get eg. 'info/user/favColours.primary' 14 | * @param {IInitialisedStore} store the store to attach 15 | * @returns {*} getter or state 16 | */ 17 | function defaultGetter (path: string, store: IInitialisedStore): any { 18 | const getterExists = store.getters.hasOwnProperty(path) 19 | if (getterExists) return store.getters[path] 20 | return getDeepValue(store.state, path) 21 | } 22 | 23 | export { defaultGetter } 24 | -------------------------------------------------------------------------------- /test/helpers/store/locationJournal.js: -------------------------------------------------------------------------------- 1 | import { defaultMutations } from '../../../dist/index.cjs' 2 | import config from '../config' 3 | 4 | // MODULE: gymData 5 | const gymDataState = { 6 | defeated: { 7 | '*': false, 8 | palletTown: false, 9 | } 10 | } 11 | const gymData = { 12 | namespaced: true, 13 | state: gymDataState, 14 | mutations: defaultMutations(gymDataState, config, {moduleNamespace: 'locationJournal/gymData/'}) 15 | } 16 | 17 | // MODULE: locationJournal 18 | const locationJournalState = { 19 | visitedPlaces: { 20 | '*': { 21 | visited: false, 22 | gym: false, 23 | pokecentre: false, 24 | tags: {} 25 | }, 26 | palletTown: { 27 | visited: true, 28 | gym: false, 29 | } 30 | } 31 | } 32 | const locationJournal = { 33 | namespaced: true, 34 | state: locationJournalState, 35 | mutations: defaultMutations(locationJournalState, config, {moduleNamespace: 'locationJournal'}), 36 | modules: { gymData } 37 | } 38 | 39 | export default locationJournal 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Luca Ban - Mesqueeb 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. -------------------------------------------------------------------------------- /test/ignoreProps.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import store from './helpers/index.cjs.js' 3 | 4 | test('setters', t => { 5 | const actions = store._actions 6 | const mutations = store._mutations 7 | // console.log('store → ', store) 8 | const checkProps = [ 9 | {module: '', prop: '_secrets', ignored: true}, 10 | {module: '', prop: 'wallet', ignored: true}, 11 | {module: 'user/', prop: 'wallet', ignored: false}, 12 | {module: 'user/', prop: 'importedData', ignored: true}, 13 | {module: 'user/', prop: 'user.secretProp', ignored: true}, 14 | ] 15 | checkProps.forEach(p => { 16 | const checkFor = (p.ignored) ? 'falsy' : 'truthy' 17 | t[checkFor](mutations[`${p.module}${p.prop}`]) 18 | t[checkFor](mutations[`${p.module}${p.prop}.pop`]) 19 | t[checkFor](mutations[`${p.module}${p.prop}.push`]) 20 | t[checkFor](mutations[`${p.module}${p.prop}.splice`]) 21 | t[checkFor](actions[`${p.module}set/${p.prop}`]) 22 | // NOT YET IMPLEMENTED: 23 | // t[checkFor](actions[`${p.module}set/${p.prop}.pop`]) 24 | // t[checkFor](actions[`${p.module}set/${p.prop}.push`]) 25 | // t[checkFor](actions[`${p.module}set/${p.prop}.splice`]) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /docs/feedback.md: -------------------------------------------------------------------------------- 1 | # Feedback 2 | 3 | Do you have questions, comments, suggestions or feedback? Or any feature that's missing that you'd love to have? Feel free to open an [issue](https://github.com/mesqueeb/vuex-easy-access/issues)! ♥ 4 | 5 | Planned future features: 6 | 7 | - Make a blog post 8 | - Improve error handling 9 | - Explain to dev possible path mistakes ` / vs . ` when no mutation is found 10 | - Warn about ignoredProps appearing in two modules 11 | - Warn when there is a module called 'set' 12 | - Warn when there already is a 'set' prop on the store 13 | 14 | Also be sure to check out the sister vuex-plugin for Firebase: [Vuex Easy Firestore](https://mesqueeb.github.io/vuex-easy-firestore)! 15 | 16 | # Support 17 | 18 | If you like what I built, you can say thanks by buying me a coffee! :) 19 | 20 | Buy me a coffeeBuy me a coffee 21 | 22 | Thank you so much!! Every little bit helps. 23 | 24 | -- 25 | 26 | Happy Vuexing!
27 | -Luca Ban 28 | -------------------------------------------------------------------------------- /test/helpers/store/index.js: -------------------------------------------------------------------------------- 1 | // import createFirestores from 'vuex-easy-firestore' 2 | import { createEasyAccess, defaultMutations } from '../../../dist/index.cjs' 3 | import config from '../config' 4 | import locationJournal from './locationJournal' 5 | import { user } from './user' 6 | import { dex } from './dex' 7 | // import { user, firestoreUser } from './user' 8 | // import { dex, firestoreDex } from './dex' 9 | import { friendsList } from './friendsList' 10 | 11 | // set plugins 12 | const easyAccess = createEasyAccess(config) 13 | // const easyFirestores = createFirestores([firestoreDex, firestoreUser]) 14 | 15 | // Store root state 16 | function initialState () { 17 | return { 18 | pokemonBox: { 19 | waterPokemon: ['squirtle'], 20 | items: [], 21 | _secrets: [] 22 | }, 23 | wallet: [], 24 | propToBeDeleted_commit: true, 25 | propToBeDeleted_dispatch: true, 26 | propToBeDeleted_delete: true 27 | } 28 | } 29 | 30 | // export store 31 | export default { 32 | modules: { locationJournal, user, dex, friendsList }, 33 | state: initialState(), 34 | mutations: defaultMutations(initialState(), config, {moduleNamespace: ''}), 35 | actions: {}, 36 | getters: {}, 37 | // plugins: [easyFirestores, easyAccess] 38 | plugins: [easyAccess] 39 | } 40 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Vuex Easy Access plugin 3 | * Unified syntax with simple set() and get() store access + auto generate mutations! 4 | * 5 | * @author Luca Ban 6 | * @contact https://lucaban.com 7 | */ 8 | 9 | import defaultConfig from './defaultConfig' 10 | import { IDefaultConfig } from './defaultConfig' 11 | import { defaultMutations } from './makeMutations' 12 | import { defaultGetter } from './makeGetters' 13 | import { defaultSetter, defaultDeletor, generateSetterModules } from './makeSetters' 14 | import { getDeepRef, getKeysFromPath } from './pathUtils' 15 | 16 | /** 17 | * This will create the `/set/` sub-modules and add `set()` `get()` and `delete()` to the store object. 18 | * 19 | * @param {IDefaultConfig} [userConfig={}] 20 | * @returns {*} the store object 21 | */ 22 | function createEasyAccess (userConfig: IDefaultConfig = {}): any { 23 | const conf = Object.assign({}, defaultConfig, userConfig) 24 | return store => { 25 | generateSetterModules(store, conf) 26 | store[conf.setter] = (path, payload) => { return defaultSetter(path, payload, store, conf) } 27 | store[conf.getter] = (path) => { return defaultGetter(path, store) } 28 | store[conf.deletor] = (path, payload) => { return defaultDeletor(path, payload, store, conf) } 29 | } 30 | } 31 | 32 | export default createEasyAccess 33 | export { createEasyAccess, defaultMutations, defaultGetter, defaultSetter, defaultDeletor, getDeepRef, getKeysFromPath } 34 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true 9 | }, 10 | extends: [ 11 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 12 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 13 | 'plugin:vue/essential', 14 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 15 | 'standard' 16 | ], 17 | // required to lint *.vue files 18 | plugins: [ 19 | 'vue' 20 | ], 21 | globals: { 22 | 'ga': true, // Google Analytics 23 | 'cordova': true, 24 | '__statics': true 25 | }, 26 | // add your custom rules here 27 | 'rules': { 28 | // allow async-await 29 | 'generator-star-spacing': 'off', 30 | 31 | // allow paren-less arrow functions 32 | 'arrow-parens': 0, 33 | 'one-var': 0, 34 | 35 | 'import/first': 0, 36 | 'import/named': 2, 37 | 'import/namespace': 2, 38 | 'import/default': 2, 39 | 'import/export': 2, 40 | 'import/extensions': 0, 41 | 'import/no-unresolved': 0, 42 | 'import/no-extraneous-dependencies': 0, 43 | 'comma-dangle': 0, 44 | 'no-unused-vars': 1, 45 | 'prefer-const': 1, 46 | 'no-new': 1, 47 | 'vue/valid-v-on': 0, 48 | 49 | // allow debugger during development 50 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | 2 | .bmc-button img 3 | width: 27px !important 4 | margin-bottom: 1px !important 5 | box-shadow: none !important 6 | border: none !important 7 | vertical-align: middle !important 8 | 9 | .bmc-button 10 | line-height: 36px !important 11 | height:37px !important 12 | text-decoration: none !important 13 | display:inline-flex !important 14 | color:#ffffff !important 15 | background-color:#3eaf7c !important 16 | border-radius: 3px !important 17 | border: 1px solid transparent !important 18 | padding: 1px 9px !important 19 | font-size: 23px !important 20 | letter-spacing:0.6px !important 21 | box-shadow: 0px 1px 2px rgba(190, 190, 190, 0.5) !important 22 | -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important 23 | margin: 0 auto !important 24 | font-family:'Cookie', cursive !important 25 | -webkit-box-sizing: border-box !important 26 | box-sizing: border-box !important 27 | -o-transition: 0.3s all linear !important 28 | -webkit-transition: 0.3s all linear !important 29 | -moz-transition: 0.3s all linear !important 30 | -ms-transition: 0.3s all linear !important 31 | transition: 0.3s all linear !important 32 | 33 | .bmc-button:hover, .bmc-button:active, .bmc-button:focus 34 | -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important 35 | text-decoration: none !important 36 | box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important 37 | opacity: 0.85 !important 38 | color:#ffffff !important 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-easy-access", 3 | "version": "3.1.8", 4 | "description": "Unified syntax with simple set() and get() store access + auto generate mutations", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint --ext .js,.vue src", 10 | "test": "ava", 11 | "build": "rollup -c build/rollup.js && npm run test", 12 | "watch": "rollup -c build/rollup.js --watch", 13 | "docs:dev": "vuepress dev docs", 14 | "docs:build": "vuepress build docs", 15 | "docs:deploy": "./docs/.vuepress/deploy.sh", 16 | "release": "npm run build && np" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/mesqueeb/vuex-easy-access.git" 21 | }, 22 | "keywords": ["vuex", "store", "plugin", "vue", "boilerplate"], 23 | "author": "Luca Ban - Mesqueeb", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/mesqueeb/vuex-easy-access/issues" 27 | }, 28 | "homepage": "https://github.com/mesqueeb/vuex-easy-access#readme", 29 | "devDependencies": { 30 | "ava": "^2.4.0", 31 | "babel-eslint": "^10.0.3", 32 | "eslint": "^6.7.0", 33 | "eslint-config-standard": "^14.1.0", 34 | "eslint-friendly-formatter": "^4.0.1", 35 | "eslint-loader": "^3.0.2", 36 | "eslint-plugin-import": "^2.18.2", 37 | "eslint-plugin-node": "^10.0.0", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-standard": "^4.0.1", 40 | "eslint-plugin-vue": "^6.0.1", 41 | "firebase": "^7.5.0", 42 | "np": "^7.4.0", 43 | "prettier": "^2.2.1", 44 | "rollup": "^1.27.4", 45 | "rollup-plugin-commonjs": "^10.1.0", 46 | "rollup-plugin-typescript2": "^0.25.2", 47 | "tsconfig-paths": "^3.9.0", 48 | "ts-node": "^8.5.2", 49 | "typescript": "^3.7.2", 50 | "vue": "^2.6.10", 51 | "vuepress": "^1.2.0", 52 | "vuex": "^3.1.2" 53 | }, 54 | "dependencies": { 55 | "find-and-replace-anything": "^2.0.4", 56 | "is-what": "^3.11.3", 57 | "merge-anything": "^2.4.2" 58 | }, 59 | "peerDependencies": { 60 | "vue": "^2.6.10" 61 | }, 62 | "ava": { 63 | "helpers": ["**/helpers/**/*"] 64 | }, 65 | "np": { 66 | "yarn": false, 67 | "branch": "production" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /types/makeSetters.d.ts: -------------------------------------------------------------------------------- 1 | import { IDefaultConfig, IInitialisedStore } from './declarations'; 2 | /** 3 | * Creates a setter function in the store to set any state value 4 | * Usage: 5 | * `set('module/path/path.to.prop', newValue)` 6 | * it will check first for existence of: `dispatch('module/path/path.to.prop')` 7 | * if non existant it will execute: `commit('module/path/path.to.prop', newValue)` 8 | * Import method: 9 | * `store.set = (path, payload) => { return defaultSetter(path, payload, store, conf) }` 10 | * 11 | * @param {string} path the path of the prop to set eg. 'info/user/favColours.primary' 12 | * @param {*} payload the payload to set the prop to 13 | * @param {IInitialisedStore} store the store to attach 14 | * @param {IDefaultConfig} [conf={}] user config 15 | * @returns {*} the dispatch or commit function 16 | */ 17 | export declare function defaultSetter(path: string, payload: any, store: IInitialisedStore, conf?: IDefaultConfig): any; 18 | export declare function formatSetter(path: string, payload: any, store: IInitialisedStore, conf?: IDefaultConfig): { 19 | command: string; 20 | _path?: string; 21 | _payload?: any; 22 | }; 23 | /** 24 | * Creates a delete function in the store to delete any prop from a value 25 | * Usage: 26 | * `delete('module/path/path.to.prop', id)` will delete prop[id] 27 | * `delete('module/path/path.to.prop.*', {id})` will delete prop[id] 28 | * it will check first for existence of: `dispatch('module/path/-path.to.prop')` or `dispatch('module/path/-path.to.prop.*')` 29 | * if non existant it will execute: `commit('module/path/-path.to.prop')` or `commit('module/path/-path.to.prop.*')` 30 | * Import method: 31 | * `store.delete = (path, payload) => { return defaultDeletor(path, payload, store, conf) }` 32 | * 33 | * @param {string} path the path of the prop to delete eg. 'info/user/favColours.primary' 34 | * @param {*} payload either nothing or an id or {id} 35 | * @param {IInitialisedStore} store the store to attach 36 | * @param {IDefaultConfig} [conf={}] user config 37 | * @returns {*} dispatch or commit 38 | */ 39 | export declare function defaultDeletor(path: string, payload: any, store: IInitialisedStore, conf?: IDefaultConfig): any; 40 | export declare function formatDeletor(path: string, payload: any, store: IInitialisedStore, conf?: IDefaultConfig): { 41 | command: string; 42 | _path?: string; 43 | _payload?: any; 44 | }; 45 | /** 46 | * Generate all vuex-easy-access modules: `/set/` and `/delete/` for each module 47 | * 48 | * @export 49 | * @param {IInitialisedStore} store 50 | * @param {IDefaultConfig} [conf={}] 51 | */ 52 | export declare function generateSetterModules(store: IInitialisedStore, conf?: IDefaultConfig): void; 53 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | ## All auto-generated setters / actions / mutations 4 | 5 | Vuex Easy Access creates one setter / action / mutation for every single property in your store! All AUTOMATICALLY! 6 | 7 | Here is the overview of an example store and everything that will be auto generated: 8 | 9 | ### Regular state props 10 | 11 | ```js 12 | // Only required setup 13 | state: { 14 | someProp: {nestedProp: ''} 15 | } 16 | // Gives you access to: 17 | 18 | // Easy Access shorthand 19 | set('module/someProp', newVal) 20 | set('module/someProp.nestedProp', newVal) 21 | delete('module/someProp') 22 | delete('module/someProp.nestedProp') 23 | 24 | // auto-generated Vuex actions 25 | dispatch('module/set/someProp', newVal) 26 | dispatch('module/set/someProp.nestedProp', newVal) 27 | dispatch('module/delete/someProp') 28 | dispatch('module/delete/someProp.nestedProp') 29 | 30 | // auto-generated Vuex mutations 31 | commit('module/someProp', newVal) 32 | commit('module/someProp.nestedProp', newVal) 33 | commit('module/-someProp') 34 | commit('module/-someProp.nestedProp')` 35 | ``` 36 | 37 | ### Array state props 38 | 39 | ```js 40 | // Only required setup 41 | state: { 42 | someArray: [] 43 | } 44 | // Gives you access to: 45 | 46 | // Easy Access shorthand 47 | set('module/someArray.push', newVal) 48 | set('module/someArray.shift', newVal) 49 | set('module/someArray.pop', newVal) 50 | set('module/someArray.splice', newVal) 51 | 52 | // auto-generated Vuex actions 53 | dispatch('module/set/someArray.push', newVal) 54 | dispatch('module/set/someArray.shift', newVal) 55 | dispatch('module/set/someArray.pop', newVal) 56 | dispatch('module/set/someArray.splice', newVal) 57 | 58 | // auto-generated Vuex mutations 59 | commit('module/someArray.push', newVal) 60 | commit('module/someArray.shift', newVal) 61 | commit('module/someArray.pop', newVal) 62 | commit('module/someArray.splice', newVal) 63 | ``` 64 | 65 | ### Wildcard state props 66 | 67 | ```js 68 | // Only required setup 69 | state: { 70 | someObject: {'*': ''} 71 | } 72 | // Gives you access to: 73 | 74 | // Easy Access shorthand 75 | set('module/someObject.*', {[id]: newVal}) 76 | delete('module/someObject.*', id) 77 | 78 | // auto-generated Vuex actions 79 | dispatch('module/set/someObject.*', {[id]: newVal}) 80 | dispatch('module/delete/someObject.*', id) 81 | 82 | // auto-generated Vuex mutations 83 | commit('module/someObject.*', {[id]: newVal}) 84 | commit('module/-someObject.*', id) 85 | ``` 86 | 87 | ### Getters 88 | 89 | ```js 90 | // Getters you can use 91 | $store.get('any/path/as.seen.above') 92 | ``` 93 | 94 | And Vuex Easy Access does all this for you... All you have to do is write your state! That's it!
Say goodbye to boilerplating. Don't let it be a roadblock in order to do best practices! 95 | -------------------------------------------------------------------------------- /test/storeSetup.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import store from './helpers/index.cjs.js' 3 | 4 | test('makeMutations', t => { 5 | const mutations = store._mutations 6 | const expectedMutations = [ 7 | 'propToBeDeleted_commit', 8 | '-propToBeDeleted_commit', 9 | 'propToBeDeleted_dispatch', 10 | '-propToBeDeleted_dispatch', 11 | 'propToBeDeleted_delete', 12 | '-propToBeDeleted_delete', 13 | 'pokemonBox', 14 | 'pokemonBox.waterPokemon', 15 | 'pokemonBox.waterPokemon.pop', 16 | 'pokemonBox.waterPokemon.push', 17 | 'pokemonBox.waterPokemon.splice', 18 | 'pokemonBox.items', 19 | 'pokemonBox.items.pop', 20 | 'pokemonBox.items.push', 21 | 'pokemonBox.items.splice', 22 | 'locationJournal/visitedPlaces', 23 | 'locationJournal/visitedPlaces.*', 24 | 'locationJournal/-visitedPlaces.*', 25 | 'locationJournal/visitedPlaces.*.visited', 26 | 'locationJournal/visitedPlaces.*.gym', 27 | 'locationJournal/visitedPlaces.palletTown', 28 | 'locationJournal/visitedPlaces.palletTown.visited', 29 | 'locationJournal/visitedPlaces.palletTown.gym', 30 | 'locationJournal/gymData/defeated', 31 | 'locationJournal/gymData/defeated.*', 32 | 'locationJournal/gymData/defeated.palletTown', 33 | 'dex/pokemonById', 34 | 'dex/pokemonById.*', 35 | 'dex/-pokemonById.*', 36 | 'dex/pokemonById.*.powerUps.push', 37 | 'dex/pokemonById.*.powerUps.splice', 38 | 'dex/pokemonById.*.powerUps.pop', 39 | 'dex/pokemonById.*.powerUps.shift', 40 | ] 41 | expectedMutations.forEach(m => t.truthy(mutations[m])) 42 | }) 43 | 44 | test('makeActions', t => { 45 | const actions = store._actions 46 | const expectedActions = [ 47 | 'locationJournal/set/visitedPlaces', 48 | 'locationJournal/set/visitedPlaces.*', 49 | 'locationJournal/delete/visitedPlaces.*', 50 | 'locationJournal/set/visitedPlaces.*.visited', 51 | 'locationJournal/set/visitedPlaces.*.gym', 52 | 'locationJournal/set/visitedPlaces.palletTown', 53 | 'locationJournal/set/visitedPlaces.palletTown.visited', 54 | 'locationJournal/set/visitedPlaces.palletTown.gym', 55 | 'locationJournal/set/visitedPlaces', 56 | 'locationJournal/gymData/set/defeated', 57 | 'locationJournal/gymData/set/defeated.*', 58 | 'locationJournal/gymData/delete/defeated.*', 59 | 'locationJournal/gymData/set/defeated.palletTown', 60 | 'set/pokemonBox', 61 | 'set/pokemonBox.waterPokemon', 62 | 'set/pokemonBox.items', 63 | 'set/pokemonBox.items.pop', 64 | 'set/pokemonBox.items.push', 65 | 'set/pokemonBox.items.splice', 66 | 'user/set/user', 67 | 'dex/set/pokemonById', 68 | 'dex/set/pokemonById.*', 69 | 'dex/delete/pokemonById.*', 70 | 'dex/set/pokemonById.*.powerUps.push', 71 | 'dex/set/pokemonById.*.powerUps.splice', 72 | 'dex/set/pokemonById.*.powerUps.pop', 73 | 'dex/set/pokemonById.*.powerUps.shift', 74 | ] 75 | expectedActions.forEach(a => t.truthy(actions[a])) 76 | }) 77 | 78 | test('makeSetterModules', t => { 79 | const modules = store._modulesNamespaceMap 80 | const expectedModules = [ 81 | 'user/set/', 82 | 'locationJournal/set/', 83 | 'locationJournal/delete/', 84 | 'locationJournal/gymData/set/', 85 | 'locationJournal/gymData/delete/', 86 | 'dex/set/', 87 | 'dex/delete/', 88 | 'set/', 89 | ] 90 | expectedModules.forEach(m => t.truthy(modules[m])) 91 | }) 92 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # What is Vuex Easy Access? 2 | 3 | 1. Zero boilerplate Vuex → auto generated actions & mutations! 4 | 2. Unified syntax for accessing your store through simple `set()` and `get()` 5 | 6 | ## Motivation 7 | 8 | [Vuex](https://vuex.vuejs.org/) is great for state management in a VueJS app, however **it requires a lot of boilerplating** setting up your store and all actions and mutations. 9 | 10 | #### The Vuex philosophy 11 | 12 | The philosophy of Vuex is to do everything through mutations that will record a history of the changes you make to the store. This makes it possible to easily track changes when things go wrong as your app grows. 13 | 14 | #### The Vuex Easy Access philosophy 15 | Instead of having to write all actions and mutations for each change you make to the store, wouldn't it be great if an action and mutation is generated for you from the start? That's exactly what Vuex Easy Access does! 16 | 17 | > Vuex Easy Access automatically generates actions and mutations for each state property! 18 | 19 | JavaScript | Vuex Easy Access 20 | -- | -- 21 | In vanilla JavaScript you can simply do:
`object.prop.nestedProp = newVal`
why shouldn't you be able to do this with Vuex? | With Vuex Easy Access you can!
`set('object.prop.nestedProp', newVal)` 22 | 23 | And the best part is, all state changes go through a mutation under the hood! 24 | 25 | ## Features 26 | 27 | - Automatically generated actions & mutations to: 28 | - Set state values 29 | - Set nested state values 30 | - Delete values 31 | - **Arrays:** Push/shift/pop/splice values 32 | - **Objects:** use ID wildcards 33 | - Shorthand `store.set()` for all the above 34 | - Streamlined `store.get()` to get state valuess 35 | 36 | ## Short overview 37 | 38 | ### 1. auto-generated Vuex actions 39 | 40 | _ | actions generated from state 41 | --|-- 42 | **State props** | eg. ```state: {someProp: {nestedProp: ''}}``` 43 | Set values
Set nested values

Delete values | `dispatch('module/set/someProp', newVal)`
`dispatch('module/set/someProp.nestedProp', newVal)`
`dispatch('module/delete/someProp')`
`dispatch('module/delete/someProp.nestedProp')` 44 | **Array props** | eg. ```state: {someArray: []}``` 45 | Push/shift/pop/splice values | `dispatch('module/set/someArray.push', newVal)`
`dispatch('module/set/someArray.shift')`
`dispatch('module/set/someArray.pop')`
`dispatch('module/set/someArray.splice', [ind, del, newVal])` 46 | **Objects with id wildcard** | eg. ```state: {someObject: {'*': ''}}``` 47 | Set and delete | `dispatch('module/set/someObject.*', {[id]: newVal})`
`dispatch('module/delete/someObject.*', id)` 48 | 49 | ### 2. Easy Access shorthand 50 | 51 | _ | available setters 52 | --|-- 53 | **State props** | eg. ```state: {someProp: {nestedProp: ''}}``` 54 | Set values
Set nested values

Delete values | `set('module/someProp', newVal)`*
`set('module/someProp.nestedProp', newVal)`
`delete('module/someProp')`
`delete('module/someProp.nestedProp')` 55 | **Array props** | eg. ```state: {someArray: []}``` 56 | Push/shift/pop/splice values | `set('module/someArray.push', newVal)`
`set('module/someArray.shift')`
`set('module/someArray.pop')`
`set('module/someArray.splice', [ind, del, newVal])` 57 | **Objects with id wildcard** | eg. ```state: {someObject: {'*': ''}}``` 58 | Set and delete | `set('module/someObject.*', {[id]: newVal})`
`delete('module/someObject.*', id)` 59 | 60 | \* `set()` and `delete()` are attached to the Vuex `store` object: `store.set()` 61 | 62 | # Support 63 | 64 | If you like what I built, you can say thanks by buying me a coffee! :) 65 | 66 | Buy me a coffeeBuy me a coffee 67 | 68 | Thank you so much!! Every little bit helps. 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is Vuex Easy Access? 2 | 3 | 1. Zero boilerplate Vuex → auto generated actions & mutations! 4 | 2. Unified syntax for accessing your store through simple `set()` and `get()` 5 | 6 | **[Full documentation](https://mesqueeb.github.io/vuex-easy-access)** 7 | 8 | ## Motivation 9 | 10 | [Vuex](https://vuex.vuejs.org/) is great for state management in a VueJS app, however **it requires a lot of boilerplating** setting up your store and all actions and mutations. 11 | 12 | #### The Vuex philosophy 13 | 14 | The philosophy of Vuex is to do everything through mutations that will record a history of the changes you make to the store. This makes it possible to easily track changes when things go wrong as your app grows. 15 | 16 | #### The Vuex Easy Access philosophy 17 | 18 | Instead of having to write all actions and mutations for each change you make to the store, wouldn't it be great if an action and mutation is generated for you from the start? That's exactly what Vuex Easy Access does! 19 | 20 | > Vuex Easy Access automatically generates actions and mutations for each state property! 21 | 22 | JavaScript | Vuex Easy Access 23 | -- | -- 24 | In vanilla JavaScript you can simply do:
`object.prop.nestedProp = newVal`
why shouldn't you be able to do this with Vuex? | With Vuex Easy Access you can!
`set('object.prop.nestedProp', newVal)` 25 | 26 | And the best part is, all state changes go through a mutation under the hood! 27 | 28 | ## Features 29 | 30 | - Automatically generated actions & mutations to: 31 | - Set state values 32 | - Set nested state values 33 | - Delete values 34 | - **Arrays:** Push/shift/pop/splice values 35 | - **Objects:** use ID wildcards 36 | - Shorthand `store.set()` for all the above 37 | - Streamlined `store.get()` to get state valuess 38 | 39 | **[Go to documentation](https://mesqueeb.github.io/vuex-easy-access/)** 40 | 41 | ## Short overview 42 | 43 | ### 1. auto-generated Vuex actions 44 | 45 | _ | actions generated from state 46 | --|-- 47 | **State props** | eg. ```state: {someProp: {nestedProp: ''}}``` 48 | Set values
Set nested values

Delete values | `dispatch('module/set/someProp', newVal)`
`dispatch('module/set/someProp.nestedProp', newVal)`
`dispatch('module/delete/someProp')`
`dispatch('module/delete/someProp.nestedProp')` 49 | **Array props** | eg. ```state: {someArray: []}``` 50 | Push/shift/pop/splice values | `dispatch('module/set/someArray.push', newVal)`
`dispatch('module/set/someArray.shift')`
`dispatch('module/set/someArray.pop')`
`dispatch('module/set/someArray.splice', [ind, del, newVal])` 51 | **Objects with id wildcard** | eg. ```state: {someObject: {'*': ''}}``` 52 | Set and delete | `dispatch('module/set/someObject.*', {[id]: newVal})`
`dispatch('module/delete/someObject.*', id)` 53 | 54 | ### 2. Easy Access shorthand 55 | 56 | _ | available setters 57 | --|-- 58 | **State props** | eg. ```state: {someProp: {nestedProp: ''}}``` 59 | Set values
Set nested values

Delete values | `set('module/someProp', newVal)`*
`set('module/someProp.nestedProp', newVal)`
`delete('module/someProp')`
`delete('module/someProp.nestedProp')` 60 | **Array props** | eg. ```state: {someArray: []}``` 61 | Push/shift/pop/splice values | `set('module/someArray.push', newVal)`
`set('module/someArray.shift')`
`set('module/someArray.pop')`
`set('module/someArray.splice', [ind, del, newVal])` 62 | **Objects with id wildcard** | eg. ```state: {someObject: {'*': ''}}``` 63 | Set and delete | `set('module/someObject.*', {[id]: newVal})`
`delete('module/someObject.*', id)` 64 | 65 | \* `set()` and `delete()` are attached to the Vuex `store` object: `store.set()` 66 | 67 | **[Installation and setup](https://mesqueeb.github.io/vuex-easy-access/setup.html) →** 68 | 69 | # Support 70 | 71 | If you like what I built, you can say thanks by buying me a coffee! :) 72 | 73 | Buy Me A Coffee 74 | 75 | Thank you so much!! Every little bit helps. 76 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import defaultConfig from './defaultConfig' 3 | import { IDefaultConfig, AnyObject } from './declarations' 4 | 5 | function getErrors ( 6 | conf: IDefaultConfig, 7 | path?: string, 8 | props?: string 9 | ): AnyObject { 10 | const originInfo = (path || props) ? `problem with prop: \`${props}\` at path: \`${path}\`` : '' 11 | const tradPatt = (conf.pattern === 'traditional') 12 | const setter = conf.setter 13 | const deletor = conf.deletor 14 | const prop = 'items' 15 | const mutationNameSet = tradPatt ? 'SET_' + prop.toUpperCase() : prop 16 | const mutationNameDel = tradPatt ? 'DELETE_' + prop.toUpperCase() : prop 17 | 18 | const exampleSetters__Wildcard = ` 19 | Correct usage examples: 20 | // From Vue-components: 21 | ${setter}('${prop}.*', {'123': {name: 'the best item'}}) 22 | 23 | // From the Vuex store: 24 | dispatch('${setter}/${prop}.*', {'123': {name: 'the best item'}}) 25 | // or 26 | commit('${mutationNameSet}.*', {'123': {name: 'the best item'}})` 27 | const exampleSetters__WildcardPath = `` 28 | const exampleSetters__DoubleWildcard = ` 29 | Correct usage examples: 30 | // From Vue-components: 31 | ${setter}('${prop}.*.tags.*', ['123', {water: true}]) 32 | 33 | // From the Vuex store: 34 | dispatch('${setter}/${prop}.*.tags.*', ['123', {water: true}]) 35 | // or 36 | commit('${mutationNameSet}.*.tags.*', ['123', {water: true}])` 37 | const exampleDeletor = ` 38 | Correct usage examples: 39 | // From Vue-components: 40 | ${deletor}('${prop}.*', '123') 41 | 42 | // From the Vuex store: 43 | dispatch('${deletor}/${prop}.*', '123') 44 | // or 45 | commit('${mutationNameDel}.*', '123')` 46 | 47 | return { 48 | mutationSetterNoId: `${originInfo} 49 | The payload needs to be an object with an \`id\` field. 50 | ${exampleSetters__Wildcard}`, 51 | mutationSetterPropPathWildcardMissingItemDoesntExist: `${originInfo} 52 | The item does not exist! Make sure you first set the item. 53 | ${exampleSetters__Wildcard}`, 54 | mutationSetterPropPathWildcardIdCount: `${originInfo} 55 | The amount of ids and wildcards \`'*'\` are not equal. 56 | If you have multiple wildcards you need to pass an array, where each item is an ID and the last is the property you want to set. 57 | ${exampleSetters__DoubleWildcard} 58 | `, 59 | mutationDeleteNoId: `${originInfo} 60 | The payload needs to be an object with an \`id\` field. 61 | ${exampleDeletor} 62 | `, 63 | wildcardFormatWrong: `${originInfo} 64 | There was something wrong with the payload passed when using a path with wildcards. 65 | 66 | A) Path with wildcard: 67 | ${exampleSetters__Wildcard} 68 | 69 | B) Path with multiple wildcards: 70 | ${exampleSetters__DoubleWildcard} 71 | `, 72 | missingDeleteMutation: ` 73 | There is no mutation set for '${path}'. 74 | Something went wrong with your vuex-easy-access setup. 75 | Did you manually add \`...defaultMutations(state)\` to your modules? 76 | See the documentation here: 77 | https://github.com/mesqueeb/VuexEasyAccess#setup 78 | 79 | 80 | // You can also manually add a mutation like so in the correct module (not recommended!!): 81 | mutations: { 82 | '${tradPatt ? 'DELETE_' + props.toUpperCase() : '-' + props}': (state, payload) => { 83 | this._vm.$delete(state.${props}) 84 | } 85 | } 86 | `, 87 | missingSetterMutation: ` 88 | There is no mutation set for '${path}'. 89 | Something went wrong with your vuex-easy-access setup. 90 | Did you manually add \`...defaultMutations(state)\` to your modules? 91 | See the documentation here: 92 | https://github.com/mesqueeb/VuexEasyAccess#setup 93 | 94 | 95 | // You can also manually add a mutation like so in the correct module (not recommended!!): 96 | mutations: { 97 | '${tradPatt ? 'SET_' + props.toUpperCase() : props}': (state, payload) => { 98 | state.${props} = payload 99 | } 100 | } 101 | `, 102 | } 103 | } 104 | 105 | /** 106 | * Error logging 107 | * 108 | * @export 109 | * @param {string} error the error code 110 | * @param {object} conf the user config 111 | * @param {string} [path] (optional) the path the error occured in 112 | * @param {string} [props] (optional) the props the error occured with 113 | * @returns {string} the error code 114 | */ 115 | export default function ( 116 | error: string, 117 | conf: object = {}, 118 | path?: string, 119 | props?: string 120 | ): string { 121 | const mergedConf: IDefaultConfig = Object.assign({}, defaultConfig, conf) 122 | const errorMessages = getErrors(mergedConf, path, props) 123 | console.error('[vuex-easy-access] Error!', errorMessages[error]) 124 | return error 125 | } 126 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | ## Warning 4 | 5 | ::: danger Please note that it is not recommended to use too much hooks in big applications. 6 |
7 | ::: 8 | 9 | If you want to add additional functionality to certain getters/setters **it is better to create a getters/mutation/action manually with a specific name.** 10 | 11 | The reason is that setting up a hook into the regular `get()` or `set()` of Vuex Easy Access is done on the Vuex-module side, and thus **not clear from just looking at a Vue component**! So it's easy to forget and might put you in situations where you expected different behaviour. 12 | 13 | Nevertheless, you can change the get/set behaviour as you please! 14 | 15 | ## Hook into get() 16 | 17 | Say that we want to make the first letter of our primary Pokémon always show up with a capital letter. For this we can overwrite the default `get('character/party.primary')` getter like so: 18 | 19 | ```js 20 | // Module: `character/` 21 | state: { party: { primary: 'mewtwo' } }, 22 | getters: { 23 | // create a getter with the prop name you want to overwrite: 24 | 'party.primary': (state) => { 25 | const name = state.party.primary 26 | // capitalise first letter: 27 | return name[0].toUpperCase() + name.substring(1) 28 | } 29 | } 30 | ``` 31 | 32 | Now automatically in your whole app where you used `get('character/party.primary')` it will return the Pokémon name with a first capital letter! 33 | 34 | The `get()` method of Vuex Easy Access first checks if a getter with the syntax like above exists. If it does it will return the getter, if not it will just return the state property: `state.character.party.primary`. 35 | 36 | ## Hook into set() 37 | 38 | Say we want a side effect to our setter. Instead of creating a new setter and changing all our Vue components, we can easily overwrite the default `set()` action to do extra stuff. 39 | 40 | Let's notify the user each time the primary Pokémon was changed: 41 | 42 | ```js 43 | // Module: `character/` 44 | actions: { 45 | // create an action with the prop name you want to overwrite: 46 | 'party.primary': ({commit, dispatch}, newPokemon) => { 47 | dispatch('notify', 'Primary Pokémon changed!', {root: true}) 48 | // do not forget to commit manually when overwriting the setter: 49 | return commit('party.primary', newPokemon) 50 | } 51 | } 52 | ``` 53 | 54 | The `set()` method of Vuex Easy Access checks to see if an action with the same path exist. If it exists it will dispatch this action, if not it will just make a default mutation: `commit('character/party.primary', newPokemon)`. 55 | 56 | As you can see **you need to manually commit a mutation**. All these mutations are also already created by Vuex Easy Access for you. Please review the correct mutation syntax down below or check the [overview](reference.html). 57 | 58 | ### Syntax for overwriting setters 59 | 60 | You can choose two setups for mutation/setter syntax: `simple` (default) or `traditional`. This will affect how you make commits or overwrite them! 61 | 62 | Let's take this example state:
`pokemonBox: { water: [] }` 63 | 64 | When you want to **overwrite the setter actions**: 65 | 66 | ```js 67 | // 'simple' uses just the property name: 68 | 'pokemonBox.water': ({commit}, newVal) => { 69 | // do something extra 70 | commit('pokemonBox.water', newVal) // you have to commit when overwriting 71 | } 72 | 73 | // 'traditional' uses 'set' in front of actions and 'SET_' in front of mutations: 74 | 'setPokemonBox.water': ({commit}, newVal) => { 75 | // do something extra 76 | commit('SET_POKEMONBOX.WATER', newVal) // you have to commit when overwriting 77 | } 78 | ``` 79 | 80 | And the underlying mutations it will use: 81 | 82 | ```js 83 | // 'simple' (default): 84 | commit('pokemonBox', newVal) 85 | commit('pokemonBox.water', newVal) 86 | commit('pokemonBox.water.push', newVal) 87 | commit('pokemonBox.water.pop', newVal) 88 | commit('pokemonBox.water.shift', newVal) 89 | commit('pokemonBox.water.splice', newVal) 90 | // 'traditional': 91 | commit('SET_POKEMONBOX', newVal) 92 | commit('SET_POKEMONBOX.WATER', newVal) 93 | commit('PUSH_POKEMONBOX.WATER', newVal) 94 | commit('POP_POKEMONBOX.WATER', newVal) 95 | commit('SHIFT_POKEMONBOX.WATER', newVal) 96 | commit('SPLICE_POKEMONBOX.WATER', newVal) 97 | ``` 98 | 99 | You can choose your preferred pattern like this: 100 | 101 | ```js 102 | const easyAccess = createEasyAccess({ 103 | pattern: 'traditional', // or 'simple' 104 | }) 105 | // and in your modules: 106 | mutations: { 107 | ...defaultMutations(state, {pattern: 'traditional'}) // or 'simple' 108 | } 109 | ``` 110 | 111 | (Do not use conflicting choices in the plugin settings and the defaultMutations) 112 | 113 | ## Hook for Firestore sync 114 | 115 | In cases you want to sync your Vuex store automatically with Firebase's Firestore see [Firestore integration (for Google Firebase)](advanced.html#firestore-integration-for-google-firebase). 116 | -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced configuration 2 | 3 | When you create your easyAccess plugin, you can make some configuration through an object: 4 | 5 | ```js 6 | import createEasyAccess from 'vuex-easy-access' 7 | const easyAccess = createEasyAccess({/* your configuration */}) 8 | ``` 9 | 10 | All possible values for the configuration are explained here: 11 | 12 | ## Firestore integration (for Google Firebase) 13 | 14 | You can add compatibility for the amazing sister plugin: [Vuex Easy Firestore](https://mesqueeb.github.io/vuex-easy-firestore). To do so just pass a variable in the configuration object like so: 15 | 16 | ```js 17 | const easyAccess = createEasyAccess({vuexEasyFirestore: true}) 18 | ``` 19 | 20 | This will make sure that whenever you set a value in a module where you enabled 'Vuex Easy Firestore', it will **auto-sync to your firestore**! Please see the [Vuex Easy Firestore documentation](https://mesqueeb.github.io/vuex-easy-firestore) for more information on how to set up auto-sync with firestore. 21 | 22 | Please note when using both plugins, it is important to pass the 'vuex-easy-firestore' plugin first, and the 'easyAccess' second for it to work properly. 23 | 24 | ## Change get() set() function names 25 | 26 | If for some reason you want to change the default function names for `get()` and `set()` (or `delete()`) to something else (eg. capitalising to `GET()` and `SET()`), you can do so by passing an object to `createEasyAccess()` like so: 27 | 28 | ```js 29 | const easyAccess = createEasyAccess({ 30 | getter: 'GET', 31 | setter: 'SET', 32 | deletor: 'DELETE', // See 'Use case: set with ID wildcard' 33 | }) 34 | ``` 35 | 36 | Now instead of the `get` `set` keywords, you will only be able to use `store.GET()` and `store.SET()` and for dispatches `dispatch('SET/prop', val)`. 37 | 38 | ## Ignore private state props 39 | 40 | Vuex Easy Access will ignore (and not make mutations/setters) any props that: 41 | 42 | - start with an underscore (*default*) 43 | - are added to the `ignoreProps: []` config 44 | 45 | ```js 46 | // in the config 47 | const easyAccess = createEasyAccess({ 48 | ignoreProps: ['normalProp.secretProp'] // true is the default 49 | }) 50 | const state = { 51 | _privateProp: null, // this prop is not touched at all! 52 | normalProp: { 53 | secretProp: null // this prop is not touched at all! 54 | } 55 | } 56 | const store = { 57 | state, 58 | mutations: { 59 | // and in the defaultMutations 60 | ...defaultMutations(state, { 61 | ignoreProps: ['normalProp.secretProp'] 62 | }) 63 | }, 64 | } 65 | ``` 66 | 67 | This will create only the mutation and dispatch setter for 'normalProp': 68 | 69 | - `mutate('normalProp', newVal)` 70 | - `dispatch('set/normalProp', newVal)` 71 | 72 | And none will be set for '_privateProp' and 'secretProp'! 73 | 74 | To disable ignoring the ones with an underscore (`_privateProp`) you need to do: 75 | 76 | ```js 77 | // in the config 78 | const easyAccess = createEasyAccess({ 79 | ignorePrivateProps: false, // true is the default 80 | }) 81 | // and in your modules: 82 | mutations: { 83 | ...defaultMutations(state, {ignorePrivateProps: false}) 84 | } 85 | ``` 86 | 87 | Please note that when passing a prop to `ignoreProps` it will be ignored in all modules regardless of the module namespace. This is because 'defaultMutations' doesn't know the exact namespace of the module when it's initiated. You can be specific about the prop to ignore in just the namespace you want by passing the 'moduleNamespace' as third prop to the 'defaultMutations'. See the example below: 88 | 89 | ```js 90 | // We will use the prop ignoreMeInUser in both the store root and the user module 91 | // But we will only ignore it in user 92 | const config = { ignoreProps: ['user/ignoreMeInUser'] } 93 | const easyAccess = createEasyAccess(config) // add config 94 | const rootAndUserState = { ignoreMeInUser: null } 95 | const userModule = { 96 | state: rootAndUserState, 97 | mutations: defaultMutations(rootAndUserState, config, {moduleNamespace: 'user/'}) // add config and moduleNamespace 98 | } 99 | const store = { 100 | modules: { user: userModule }, 101 | state: rootAndUserState, 102 | mutations: defaultMutations(rootAndUserState, config, {moduleNamespace: ''}) // add config and moduleNamespace 103 | } 104 | ``` 105 | 106 | ## Use traditional `SET_PROP` syntax 107 | 108 | Mutations are used under the hood. These mutations have a simple syntax which is just the name of the prop. Check the [Reference](reference.html) for a quick overview of the mutations used by Vuex Easy Firestore. 109 | 110 | You can also opt in for mutations to be in **the traditional syntax** (as per the vuex documentation). For this please read [Syntax for overwriting setters](hooks.html#hook-into-set) in the Hooks chapter. 111 | 112 | ### Missing any features? 113 | 114 | If you have any requests for more customisation of this functionality, please let me know in an [issue](https://github.com/mesqueeb/vuex-easy-access/issues)! 115 | -------------------------------------------------------------------------------- /types/pathUtils.d.ts: -------------------------------------------------------------------------------- 1 | import { AnyObject } from './declarations'; 2 | /** 3 | * Get all ids from an array payload. 4 | * 5 | * @param {any[]} payload 6 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 7 | * @param {string} [path] (optional - for error handling) the path called 8 | * @returns {string[]} all ids 9 | */ 10 | export declare function getIdsFromPayload(payload: any[], conf?: object, path?: string): string[]; 11 | /** 12 | * Returns a value of a payload piece. Eg. {[id]: 'val'} will return 'val' 13 | * 14 | * @param {(object | string)} payloadPiece 15 | * @returns {any} 16 | */ 17 | export declare function getValueFromPayloadPiece(payloadPiece: object | string): any; 18 | /** 19 | * Checks the ratio between an array of IDs and a path with wildcards 20 | * 21 | * @param {string[]} ids 22 | * @param {string} path 23 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 24 | * @returns {boolean} true if no problem. false if the ratio is incorrect 25 | */ 26 | export declare function checkIdWildcardRatio(ids: string[], path: string, conf: object): boolean; 27 | /** 28 | * Fill in IDs at '*' in a path, based on the IDs received. 29 | * 30 | * @param {string[]} ids 31 | * @param {string} path 'path.*.with.*.wildcards' 32 | * @param {object} [state] RELATIVE TO PATH START! the state to check if the value actually exists 33 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 34 | * @returns {string} The path with '*' replaced by IDs 35 | */ 36 | export declare function fillinPathWildcards(ids: string[], path: string, state?: object, conf?: object): string; 37 | /** 38 | * ('/sub.prop', payload) becomes → {sub: {prop: payload}} 39 | * ('sub', payload) becomes → {sub: payload} 40 | * 41 | * @param {string} path 'a/path/like.this' 42 | * @param {*} payload 43 | * @param {object} [state] the state to check if the value actually exists 44 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 45 | * @returns {AnyObject} a nested object re-created based on the path & payload 46 | */ 47 | export declare function createObjectFromPath(path: string, payload: any, state?: object, conf?: object): AnyObject; 48 | /** 49 | * Returns the keys of a path 50 | * 51 | * @param {string} path a/path/like.this 52 | * @returns {string[]} with keys 53 | */ 54 | export declare function getKeysFromPath(path: string): string[]; 55 | /** 56 | * Gets a deep property in an object, based on a path to that property 57 | * 58 | * @param {object} target an object to wherefrom to retrieve the deep reference of 59 | * @param {string} path 'path/to.prop' 60 | * @returns {AnyObject} the last prop in the path 61 | */ 62 | export declare function getDeepRef(target: object, path: string): AnyObject; 63 | /** 64 | * Gets a deep property in an object, based on a path to that property 65 | * 66 | * @param {object} target the Object to get the value of 67 | * @param {string} path 'path/to/prop.subprop' 68 | * @returns {AnyObject} the property's value 69 | */ 70 | export declare function getDeepValue(target: object, path: string): AnyObject; 71 | /** 72 | * Sets a value to a deep property in an object, based on a path to that property 73 | * 74 | * @param {object} target the Object to set the value on 75 | * @param {string} path 'path/to/prop.subprop' 76 | * @param {*} value the value to set 77 | * @returns {AnyObject} the original target object 78 | */ 79 | export declare function setDeepValue(target: object, path: string, value: any): AnyObject; 80 | /** 81 | * Pushes a value in an array which is a deep property in an object, based on a path to that property 82 | * 83 | * @param {object} target the Object to push the value on 84 | * @param {string} path 'path/to.sub.prop' 85 | * @param {*} value the value to push 86 | * @returns {number} the new length of the array 87 | */ 88 | export declare function pushDeepValue(target: object, path: string, value: any): number; 89 | /** 90 | * Pops a value of an array which is a deep property in an object, based on a path to that property 91 | * 92 | * @param {object} target the Object to pop the value of 93 | * @param {string} path 'path.to.sub.prop' 94 | * @returns {*} the popped value 95 | */ 96 | export declare function popDeepValue(target: object, path: string): any; 97 | /** 98 | * Shift a value of an array which is a deep property in an object, based on a path to that property 99 | * 100 | * @param {object} target the Object to shift the value of 101 | * @param {string} path 'path.to.sub.prop' 102 | * @returns {*} the shifted value 103 | */ 104 | export declare function shiftDeepValue(target: object, path: string): any; 105 | /** 106 | * Splice into an array which is a deep property in an object, based on a path to that property 107 | * 108 | * @param {object} target the Object to splice the value of 109 | * @param {string} path 'path/to.sub.prop' 110 | * @param {number} [index=0] the index to splice in the value, defaults to 0 111 | * @param {number} [deleteCount=0] the amount of items to delete, defaults to 0 112 | * @param {*} value the value to splice in 113 | * @returns {any[]} an array containing the deleted elements 114 | */ 115 | export declare function spliceDeepValue(target: object, path: string, index: number, deleteCount: number, value: any): any[]; 116 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | To keep everything fun we're gonna explain things with the example of a Pokémon app. 🐞 4 | 5 | ## get() set() example 6 | 7 | Vuex Easy Access adds a `get()` and `set()` function on the store object, as can be seen in the following example: 8 | 9 | - module: `player/` 10 | - state: `{party: {primary: 'bulbasaur'}}` 11 | 12 | We have a `player/` module with one `primary` Pokémon. Let's create a Vue component with an option to swap the primary Pokémon: 13 | 14 | ```html 15 | 19 | ``` 20 | 21 | Nothing required inside your Vue component methods! Very clean! 🏄🏼‍ 22 | 23 | With Vuex Easy Access you **only need to set your state** and you can use these handy setters automatically! 24 | 25 | ## set as Vuex action 26 | 27 | Besides `set()` on the store object, you also get Vuex actions! Any module will automatically receive a sub-module called `/set/`! In this sub-module the actions are created to `set` each of the state props. 28 | 29 | So in our example we would receive the following a sub module: 30 | 31 | ```js 32 | // module: `player/` 33 | state: {party: {primary: 'bulbasaur'}}, 34 | modules: { 35 | set: { // `/set/` sub module with a setter for each state prop! 36 | actions: { 37 | 'party': () => {} // set the prop `party` 38 | 'party.primary': () => {} // set `primary` inside `party` 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | This makes it possible for you to do things like: 45 | 46 | ```js 47 | actions: { 48 | randomizePrimaryPokemon ({dispatch}) { 49 | dispatch('player/set/party.primary', random()) 50 | } 51 | } 52 | ``` 53 | 54 | Below we see how you can set up the state and what kind of powers that will give you in each case. The actions you'll receive will differ slightly based on the kind of state props you have. 55 | 56 | ## Regular state props 57 | 58 | Every single state prop (and nested ones) will receive a **getter, setter and deletor** (is that a word? :). 59 | 60 | ```js 61 | // module: `player/` 62 | state: { 63 | party: { 64 | primary: '' 65 | }, 66 | } 67 | 68 | // Setters you can use from Vue components 69 | $store.set('player/party', newParty) // modules end in `/` 70 | $store.set('player/party.primary', newPokemon) // separate sub-props with `.` 71 | $store.delete('player/party.primary') // completely deletes props 72 | 73 | // Actions you can use (from `set/` sub-module) 74 | dispatch('player/set/party', newParty) 75 | dispatch('player/set/party.primary', newPokemon) 76 | dispatch('player/delete/party.primary') // completely deletes props 77 | ``` 78 | 79 | Delete uses `Vue.delete` under the hood, so reactivity works as expected. However: be careful of the `delete` action, because **it will completely delete the property**. In most cases it's better to just `set('player/party.primary', null)`. 80 | 81 | ## Array props [] 82 | 83 | If a state prop is an empty array `[]` you will have super powers 💪🏻! Push, pop, shift, splice just like regular JavaScript! 84 | 85 | ```js 86 | // module: `player/` 87 | state: { 88 | pokeBox: [], 89 | } 90 | 91 | // Setters you can use from Vue components 92 | $store.set('player/pokeBox.push', newPokemon) 93 | $store.set('player/pokeBox.pop') 94 | $store.set('player/pokeBox.shift') 95 | $store.set('player/pokeBox.splice', [0, 1, newPokemon]) // second argument is an array 96 | 97 | // Actions you can use (from `set/` sub-module) 98 | dispatch('player/set/pokeBox.push', newPokemon) 99 | dispatch('player/set/pokeBox.pop') 100 | dispatch('player/set/pokeBox.shift') 101 | dispatch('player/set/pokeBox.splice', [0, 1, newPokemon]) 102 | ``` 103 | 104 | ## ID wildcard props 🃏 105 | 106 | Just add `'*'` as prop in your state and you will have super powers 💪🏻! Perfect for working with IDs! This uses `Vue.set` under the hood, so reactivity works as expected. 107 | 108 | ```js 109 | // module: `player/` 110 | state: { 111 | pokeDex: { 112 | '*': { 113 | name: '', 114 | seen: false, 115 | types: {'*': false} 116 | } 117 | }, 118 | } 119 | 120 | // Setters you can use from Vue components 121 | $store.set('player/pokeDex.*', {'001': {name: 'Bulbasaur'}}) // or 122 | $store.set('player/pokeDex.*', {id: '001', name: 'Bulbasaur'}) 123 | // You can use pass `{[id]: item}` or `{id, item}` as the payload 124 | $store.delete('player/pokeDex.*', id) 125 | 126 | // Actions you can use (from `set/` sub-module) 127 | dispatch('player/set/pokeDex.*', newPokemon) // info on payload above 128 | dispatch('player/delete/pokeDex.*', id) // from `delete/` sub-module 129 | ``` 130 | 131 | ## Wildcard default values 132 | 133 | As per the example above, values to be expected for each Pokémon are `name`, `seen` and `types`. When you define your state like so, **all expected values will automatically be added** to any new Pokémon you insert! 134 | 135 | ```js 136 | $store.set('player/pokeDex.*', {'001': {name: 'Bulbasaur'}}) 137 | // results in the state: 138 | pokeDex: { 139 | '001': { 140 | name: 'Bulbasaur', 141 | seen: false, // automatically added 142 | types: {} // automatically added 143 | } 144 | } 145 | ``` 146 | 147 | ## Multiple wildcards 148 | 149 | You can work with multiple wildcards! Let's edit the sub props of our Bulbasaur. 150 | 151 | ```js 152 | const id = '001' 153 | 154 | // Use an array to pass each id/value for * in the path 155 | $store.set('player/pokeDex.*.seen', [id, true]) 156 | // the * in the path receives the id passed first 157 | // `seen` prop receives `true` passed second 158 | 159 | // Use {[id]: value} for passing a value to a second wildcard 160 | $store.set('player/pokeDex.*.types.*', [id, {'grass': true}]) 161 | // `grass` in `types` will be set to true (any other types set stay as is!) 162 | 163 | // results in the state: 164 | pokeDex: { 165 | '001': { 166 | name: 'Bulbasaur', 167 | seen: true, // set through `*.seen` 168 | types: {grass: true} // set through `*.types.*` 169 | } 170 | } 171 | 172 | // All this is also possible from actions of course: 173 | dispatch('player/set/pokeDex.*.seen', [id, true]) 174 | dispatch('player/set/pokeDex.*.types.*', [id, {'grass': true}]) 175 | ``` 176 | 177 | ## Getters 178 | 179 | Remember, paths for setters and getters use `/` for modules and `.` for sub props. 180 | 181 | ```js 182 | // Getters you can use 183 | $store.get('any/path/as.seen.above') 184 | ``` 185 | 186 | I hope you understood the basics! If there are any unclear parts please open an [issue](https://github.com/mesqueeb/vuex-easy-access/issues) and let me know! I'll try to improve the docs as much as possible. 187 | -------------------------------------------------------------------------------- /test/pathUtils.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { 3 | getIdsFromPayload, 4 | checkIdWildcardRatio, 5 | getValueFromPayloadPiece, 6 | fillinPathWildcards, 7 | createObjectFromPath, 8 | getKeysFromPath, 9 | getDeepRef, 10 | getDeepValue, 11 | setDeepValue, 12 | pushDeepValue, 13 | popDeepValue, 14 | shiftDeepValue, 15 | spliceDeepValue, 16 | } from './helpers/pathUtils.cjs' 17 | 18 | test('getIdsFromPayload', t => { 19 | let payload, res 20 | payload = ['001'] 21 | res = getIdsFromPayload(payload) 22 | t.is(res[0], '001') 23 | payload = ['001', '002'] 24 | res = getIdsFromPayload(payload) 25 | t.is(res[0], '001') 26 | t.is(res[1], '002') 27 | payload = ['001', {'002': true}, {'003': {id: 'fake'}}, {id: '004', vals: 'fake'}] 28 | res = getIdsFromPayload(payload) 29 | t.is(res[0], '001') 30 | t.is(res[1], '002') 31 | t.is(res[2], '003') 32 | t.is(res[3], '004') 33 | }) 34 | test('checkIdWildcardRatio', t => { 35 | let ids, path, conf, res 36 | ids = ['001'] 37 | path = 'dex/pokemonById.*' 38 | res = checkIdWildcardRatio(ids, path, conf) 39 | t.is(res, true) 40 | ids = ['001'] 41 | path = 'dex/pokemonById.*.tags.*' 42 | res = checkIdWildcardRatio(ids, path, conf) 43 | t.is(res, false) 44 | }) 45 | test('getValueFromPayloadPiece', t => { 46 | let payloadPiece, res 47 | payloadPiece = '001' 48 | res = getValueFromPayloadPiece(payloadPiece) 49 | t.deepEqual(res, '001') 50 | payloadPiece = {'002': true} 51 | res = getValueFromPayloadPiece(payloadPiece) 52 | t.deepEqual(res, true) 53 | payloadPiece = {'003': {id: 'fake'}} 54 | res = getValueFromPayloadPiece(payloadPiece) 55 | t.deepEqual(res, {id: 'fake'}) 56 | payloadPiece = {id: '004', vals: 'fake'} 57 | res = getValueFromPayloadPiece(payloadPiece) 58 | t.deepEqual(res, {id: '004', vals: 'fake'}) 59 | }) 60 | test('fillinPathWildcards', t => { 61 | let ids, path, state, conf, res 62 | ids = ['001', 'water'] 63 | path = 'dex/pokemonById.*.tags.*' 64 | res = fillinPathWildcards(ids, path, state, conf) 65 | t.is(res, 'dex/pokemonById.001.tags.water') 66 | path = 'dex/pokemonById.*.tags' 67 | res = fillinPathWildcards(ids, path, state, conf) 68 | t.is(res, 'dex/pokemonById.001.tags') 69 | path = 'dex/*.tags.*' 70 | res = fillinPathWildcards(ids, path, state, conf) 71 | t.is(res, 'dex/001.tags.water') 72 | path = '*.tags.*' 73 | res = fillinPathWildcards(ids, path, state, conf) 74 | t.is(res, '001.tags.water') 75 | path = '*.tags.deeper.*' 76 | res = fillinPathWildcards(ids, path, state, conf) 77 | t.is(res, '001.tags.deeper.water') 78 | path = '*.tags.deeper.d.d.d.*' 79 | res = fillinPathWildcards(ids, path, state, conf) 80 | t.is(res, '001.tags.deeper.d.d.d.water') 81 | path = '*' 82 | res = fillinPathWildcards(['001'], path, state, conf) 83 | t.is(res, '001') 84 | }) 85 | test('fillinPathWildcards without *', t => { 86 | let path, res, state, conf 87 | path = 'a/random/path.with.vals-!' 88 | res = fillinPathWildcards([], path, state, conf) 89 | t.is(res, 'a/random/path.with.vals-!') 90 | }) 91 | test('createObjectFromPath', t => { 92 | let path, payload, state, conf, res 93 | path = '*' 94 | payload = {'001': {name: 'bulba'}} 95 | res = createObjectFromPath(path, payload, state, conf) 96 | t.deepEqual(res, {'001': {name: 'bulba'}}) 97 | 98 | path = 'test' 99 | payload = {'001': {name: 'bulba'}} 100 | res = createObjectFromPath(path, payload, state, conf) 101 | console.log('res → ', res) 102 | t.deepEqual(res, {test: {'001': {name: 'bulba'}}}) 103 | 104 | path = 'locationJournal/gymData/defeated.palletTown' 105 | payload = true 106 | res = createObjectFromPath(path, payload, state, conf) 107 | t.deepEqual(res, {locationJournal: {gymData: {defeated: {palletTown: true}}}}) 108 | 109 | path = 'dex/pokemonById.*' 110 | payload = {id: '001', name: 'bulba'} 111 | res = createObjectFromPath(path, payload, state, conf) 112 | t.deepEqual(res, {dex: {pokemonById: {'001': {id: '001', name: 'bulba'}}}}) 113 | 114 | path = 'dex/pokemonById.*' 115 | payload = {'001': {name: 'bulba'}} 116 | res = createObjectFromPath(path, payload, state, conf) 117 | t.deepEqual(res, {dex: {pokemonById: {'001': {id: '001', name: 'bulba'}}}}) 118 | 119 | path = 'dex/*.tags' 120 | payload = ['001', []] 121 | res = createObjectFromPath(path, payload, state, conf) 122 | t.deepEqual(res, {dex: {'001': {tags: []}}}) 123 | 124 | path = 'dex/pokemonById.*.tags' 125 | payload = ['001', {dark: true}] 126 | res = createObjectFromPath(path, payload, state, conf) 127 | t.deepEqual(res, {dex: {pokemonById: {'001': {tags: {dark: true}}}}}) 128 | 129 | path = 'dex/pokemonById.*.tags' 130 | payload = ['001', ['nieuwe', 'tags']] 131 | res = createObjectFromPath(path, payload, state, conf) 132 | t.deepEqual(res, {dex: {pokemonById: {'001': {tags: ['nieuwe', 'tags']}}}}) 133 | 134 | path = 'dex/pokemonById.*.tags.*' 135 | payload = ['001', {dark: true}] 136 | res = createObjectFromPath(path, payload, state, conf) 137 | t.deepEqual(res, {dex: {pokemonById: {'001': {tags: {dark: true}}}}}) 138 | 139 | path = 'dex/pokemonById.*.tags.*' 140 | payload = ['001', {id: 'dark', state: true}] 141 | res = createObjectFromPath(path, payload, state, conf) 142 | t.deepEqual(res, {dex: {pokemonById: {'001': {tags: {dark: {id: 'dark', state: true}}}}}}) 143 | 144 | path = 'dex/pokemonById.*.tags.*' 145 | payload = ['001/m', {id: 'dark@universe.com', state: true}] 146 | res = createObjectFromPath(path, payload, state, conf) 147 | t.deepEqual(res, {dex: {pokemonById: {'001/m': {tags: {'dark@universe.com': {id: 'dark@universe.com', state: true}}}}}}) 148 | }) 149 | test('getKeysFromPath', t => { 150 | let path, res 151 | path = 'dex/pokemonById.*.tags.*' 152 | res = getKeysFromPath(path) 153 | t.deepEqual(res, ['dex', 'pokemonById', '*', 'tags', '*']) 154 | path = '_/dex 1/pokemon-By-Id.*._tags_.*123 abc' 155 | res = getKeysFromPath(path) 156 | t.deepEqual(res, ['_', 'dex 1', 'pokemon-By-Id', '*', '_tags_', '*123 abc']) 157 | }) 158 | test('getDeepRef', t => { 159 | let target, path, res 160 | target = {dex: {pokemonById: {'001': {tags: {grass: true, water: false}}}}} 161 | path = 'dex/pokemonById.001.tags.water' 162 | res = getDeepRef(target, path) 163 | t.is(res, target.dex.pokemonById['001'].tags.water) 164 | t.is(res, target.dex.pokemonById['001'].tags.water) 165 | path = 'dex/pokemonById.001.tags' 166 | res = getDeepRef(target, path) 167 | t.is(res, target.dex.pokemonById['001'].tags) 168 | t.deepEqual(res, {grass: true, water: false}) 169 | }) 170 | test('getDeepRef returns target on empty string', t => { 171 | let res 172 | res = getDeepRef({theState: true}, '') 173 | t.deepEqual(res, {theState: true}) 174 | }) 175 | test('getDeepValue', t => { 176 | let target, path, res 177 | target = {dex: {pokemonById: {'001': {tags: {grass: true, water: false}}}}} 178 | path = 'dex/pokemonById.001.tags.water' 179 | res = getDeepValue(target, path) 180 | t.is(res, target.dex.pokemonById['001'].tags.water) 181 | path = 'dex/pokemonById.001.tags' 182 | res = getDeepValue(target, path) 183 | t.is(res, target.dex.pokemonById['001'].tags) 184 | t.deepEqual(res, {grass: true, water: false}) 185 | }) 186 | test('setDeepValue', t => { 187 | let target, path, value, res 188 | target = {dex: {pokemonById: {'001': {tags: {grass: true, water: false}}}}} 189 | path = 'dex/pokemonById.001.tags.water' 190 | res = setDeepValue(target, path, 'wazegdegijnu') 191 | t.is(target.dex.pokemonById['001'].tags.water, 'wazegdegijnu') 192 | path = 'dex/pokemonById.001.tags' 193 | res = setDeepValue(target, path, ['1']) 194 | t.deepEqual(target.dex.pokemonById['001'].tags, ['1']) 195 | }) 196 | test('pushDeepValue', t => { 197 | let target, path, value, res 198 | target = {dex: {pokemonById: {'001': {tags: ['0', '1', '2', '3']}}}} 199 | path = 'dex/pokemonById.001.tags' 200 | res = pushDeepValue(target, path, 'squirt') 201 | t.deepEqual(target.dex.pokemonById['001'].tags, ['0', '1', '2', '3', 'squirt']) 202 | t.is(res, 5) 203 | }) 204 | test('popDeepValue', t => { 205 | let target, path, res 206 | target = {dex: {pokemonById: {'001': {tags: ['0', '1', '2', '3']}}}} 207 | path = 'dex/pokemonById.001.tags' 208 | res = popDeepValue(target, path) 209 | t.deepEqual(target.dex.pokemonById['001'].tags, ['0', '1', '2']) 210 | t.is(res, '3') 211 | }) 212 | test('shiftDeepValue', t => { 213 | let target, path, res 214 | target = {dex: {pokemonById: {'001': {tags: ['0', '1', '2', '3']}}}} 215 | path = 'dex/pokemonById.001.tags' 216 | res = shiftDeepValue(target, path) 217 | t.deepEqual(target.dex.pokemonById['001'].tags, ['1', '2', '3']) 218 | t.is(res, '0') 219 | }) 220 | test('spliceDeepValue', t => { 221 | let target, path, index, deleteCount, value, res 222 | path = 'dex/pokemonById.001.tags' 223 | target = {dex: {pokemonById: {'001': {tags: ['0', '1', '2', '3']}}}} 224 | res = spliceDeepValue(target, path, 0, 1, 'squirt') 225 | t.deepEqual(target.dex.pokemonById['001'].tags, ['squirt', '1', '2', '3']) 226 | t.deepEqual(res, ['0']) 227 | target = {dex: {pokemonById: {'001': {tags: ['0', '1', '2', '3']}}}} 228 | res = spliceDeepValue(target, path, 1, 0, 'squirt') 229 | t.deepEqual(target.dex.pokemonById['001'].tags, ['0', 'squirt', '1', '2', '3']) 230 | t.deepEqual(res, []) 231 | target = {dex: {pokemonById: {'001': {tags: ['0', '1', '2', '3']}}}} 232 | res = spliceDeepValue(target, path, 2, 2, 'squirt') 233 | t.deepEqual(target.dex.pokemonById['001'].tags, ['0', '1', 'squirt']) 234 | t.deepEqual(res, ['2', '3']) 235 | target = {dex: {pokemonById: {'001': {tags: ['0', '1', '2', '3']}}}} 236 | res = spliceDeepValue(target, path, 1, 1) 237 | t.deepEqual(target.dex.pokemonById['001'].tags, ['0', '2', '3']) 238 | t.deepEqual(res, ['1']) 239 | }) 240 | -------------------------------------------------------------------------------- /test/defaultDeletor.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { formatDeletor } from './helpers/makeSetters.cjs' 3 | 4 | function getStore () { 5 | return { 6 | state: {}, 7 | _actions: {}, 8 | _mutations: {}, 9 | _modulesNamespaceMap: {} 10 | } 11 | } 12 | 13 | function getConf () { 14 | return { 15 | setter: 'set', 16 | getter: 'get', 17 | deletor: 'delete', 18 | vuexEasyFirestore: false, 19 | ignorePrivateProps: true, 20 | ignoreProps: [], 21 | pattern: 'simple' 22 | } 23 | } 24 | 25 | test('[simple] deletor no module', t => { 26 | let res, path, payload, store, conf, deletePath 27 | store = getStore() 28 | conf = getConf() 29 | // trigger mutation 30 | path = 'favColours.primary' 31 | deletePath = '-favColours.primary' 32 | store._actions[deletePath] = false 33 | store._mutations[deletePath] = true 34 | payload = 'red' 35 | res = formatDeletor(path, payload, store, conf) 36 | t.deepEqual(res, {command: 'commit', _path: deletePath, _payload: 'red'}) 37 | // trigger action 38 | store._actions[deletePath] = true 39 | store._mutations[deletePath] = true 40 | res = formatDeletor(path, payload, store, conf) 41 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'red'}) 42 | }) 43 | 44 | test('[simple] deletor single module & prop', t => { 45 | let res, path, payload, store, conf, deletePath 46 | store = getStore() 47 | conf = getConf() 48 | // trigger mutation 49 | path = 'colours/favColour' 50 | deletePath = 'colours/-favColour' 51 | store._actions[deletePath] = false 52 | store._mutations[deletePath] = true 53 | payload = 'red' 54 | res = formatDeletor(path, payload, store, conf) 55 | t.deepEqual(res, {command: 'commit', _path: deletePath, _payload: 'red'}) 56 | // trigger action 57 | store._actions[deletePath] = true 58 | store._mutations[deletePath] = true 59 | res = formatDeletor(path, payload, store, conf) 60 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'red'}) 61 | }) 62 | 63 | test('[simple] deletor deep', t => { 64 | let res, path, payload, store, conf, deletePath 65 | store = getStore() 66 | conf = getConf() 67 | // trigger mutation 68 | path = 'info/user/favColours.primary' 69 | deletePath = 'info/user/-favColours.primary' 70 | store._actions[deletePath] = false 71 | store._mutations[deletePath] = true 72 | payload = 'red' 73 | res = formatDeletor(path, payload, store, conf) 74 | t.deepEqual(res, {command: 'commit', _path: deletePath, _payload: 'red'}) 75 | // trigger action 76 | store._actions[deletePath] = true 77 | store._mutations[deletePath] = true 78 | res = formatDeletor(path, payload, store, conf) 79 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'red'}) 80 | }) 81 | 82 | test('[traditional] deletor no module', t => { 83 | let res, path, payload, store, conf, deletePathA, deletePathM 84 | store = getStore() 85 | conf = getConf() 86 | conf.pattern = 'traditional' 87 | // trigger mutation 88 | path = 'favColours.primary' 89 | deletePathA = 'deleteFavColours.primary' 90 | deletePathM = 'DELETE_FAVCOLOURS.PRIMARY' 91 | store._actions[deletePathA] = false 92 | store._mutations[deletePathM] = true 93 | payload = 'red' 94 | res = formatDeletor(path, payload, store, conf) 95 | t.deepEqual(res, {command: 'commit', _path: deletePathM, _payload: 'red'}) 96 | // trigger action 97 | store._actions[deletePathA] = true 98 | store._mutations[deletePathM] = true 99 | res = formatDeletor(path, payload, store, conf) 100 | t.deepEqual(res, {command: 'dispatch', _path: deletePathA, _payload: 'red'}) 101 | }) 102 | 103 | test('[traditional] deletor single module & prop', t => { 104 | let res, path, payload, store, conf, deletePathA, deletePathM 105 | store = getStore() 106 | conf = getConf() 107 | conf.pattern = 'traditional' 108 | // trigger mutation 109 | path = 'colours/favColour' 110 | deletePathA = 'colours/deleteFavColour' 111 | deletePathM = 'colours/DELETE_FAVCOLOUR' 112 | store._actions[deletePathA] = false 113 | store._mutations[deletePathM] = true 114 | payload = 'red' 115 | res = formatDeletor(path, payload, store, conf) 116 | t.deepEqual(res, {command: 'commit', _path: deletePathM, _payload: 'red'}) 117 | // trigger action 118 | store._actions[deletePathA] = true 119 | store._mutations[deletePathM] = true 120 | res = formatDeletor(path, payload, store, conf) 121 | t.deepEqual(res, {command: 'dispatch', _path: deletePathA, _payload: 'red'}) 122 | }) 123 | 124 | test('[traditional] deletor deep', t => { 125 | let res, path, payload, store, conf, deletePathA, deletePathM 126 | store = getStore() 127 | conf = getConf() 128 | conf.pattern = 'traditional' 129 | // trigger mutation 130 | path = 'info/user/favColours.primary' 131 | deletePathA = 'info/user/deleteFavColours.primary' 132 | deletePathM = 'info/user/DELETE_FAVCOLOURS.PRIMARY' 133 | store._actions[deletePathA] = false 134 | store._mutations[deletePathM] = true 135 | payload = 'red' 136 | res = formatDeletor(path, payload, store, conf) 137 | t.deepEqual(res, {command: 'commit', _path: deletePathM, _payload: 'red'}) 138 | // trigger action 139 | store._actions[deletePathA] = true 140 | store._mutations[deletePathM] = true 141 | res = formatDeletor(path, payload, store, conf) 142 | t.deepEqual(res, {command: 'dispatch', _path: deletePathA, _payload: 'red'}) 143 | }) 144 | 145 | test('[vuex-easy-firestore] trigger user action first', t => { 146 | let res, path, payload, store, conf, deletePathA, deletePathEF 147 | store = getStore() 148 | conf = getConf() 149 | conf.vuexEasyFirestore = true 150 | // trigger user action 151 | path = 'colours/favColour' 152 | deletePathA = 'colours/-favColour' 153 | deletePathEF = 'colours/delete' 154 | store._modulesNamespaceMap['colours/'] = {state: {_conf: {statePropName: ''}}} 155 | store._actions[deletePathEF] = true 156 | store._actions[deletePathA] = true 157 | payload = 'red' 158 | res = formatDeletor(path, payload, store, conf) 159 | t.deepEqual(res, {command: 'dispatch', _path: deletePathA, _payload: 'red'}) 160 | }) 161 | 162 | test('[vuex-easy-firestore] deletor single module & prop', t => { 163 | let res, path, payload, store, conf, deletePath 164 | store = getStore() 165 | conf = getConf() 166 | conf.vuexEasyFirestore = true 167 | // trigger vuex-easy-firestore action 168 | path = 'colours/favColour' 169 | deletePath = 'colours/delete' 170 | store._modulesNamespaceMap['colours/'] = {state: {_conf: {statePropName: ''}}} 171 | store._actions[deletePath] = true 172 | payload = 'red' 173 | res = formatDeletor(path, payload, store, conf) 174 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'favColour'}) 175 | }) 176 | 177 | test('[vuex-easy-firestore] deletor deep', t => { 178 | let res, path, payload, store, conf, deletePath 179 | store = getStore() 180 | conf = getConf() 181 | conf.vuexEasyFirestore = true 182 | // trigger vuex-easy-firestore action 183 | path = 'info/user/favColours.primary' 184 | deletePath = 'info/user/delete' 185 | store._modulesNamespaceMap['info/user/'] = {state: {_conf: {statePropName: ''}}} 186 | store._actions[deletePath] = true 187 | payload = 'red' 188 | res = formatDeletor(path, payload, store, conf) 189 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'favColours.primary'}) 190 | }) 191 | 192 | test('[vuex-easy-firestore] wildcards', t => { 193 | let res, path, payload, store, conf, deletePath 194 | store = getStore() 195 | conf = getConf() 196 | conf.vuexEasyFirestore = true 197 | // trigger vuex-easy-firestore action 198 | store._modulesNamespaceMap['info/user/'] = {state: {_conf: {statePropName: ''}}} 199 | deletePath = 'info/user/delete' 200 | store._actions[deletePath] = true 201 | // 1. end * 202 | path = 'info/user/favColours.primary.*' 203 | // a) proper id 204 | payload = {red: true} 205 | res = formatDeletor(path, payload, store, conf) 206 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'favColours.primary.red'}) 207 | path = 'info/user/favColours.primary.*' 208 | // b) flat id 209 | payload = {id: 'red', is: true} 210 | res = formatDeletor(path, payload, store, conf) 211 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'favColours.primary.red'}) 212 | store.state = {info: {user: {favColours: {primary: {visible: false}}}}} 213 | store._modulesNamespaceMap['info/user/'].state.favColours = {primary: {visible: false}} 214 | // 2. mid-way * 215 | path = 'info/user/favColours.*.visible' 216 | payload = ['primary', true] 217 | res = formatDeletor(path, payload, store, conf) 218 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'favColours.primary.visible'}) 219 | // 3. mid-way * and end * 220 | path = 'info/user/favColours.*.visible.*' 221 | payload = ['primary', {red: true}] 222 | res = formatDeletor(path, payload, store, conf) 223 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'favColours.primary.visible.red'}) 224 | // 4. statePropName 225 | store._modulesNamespaceMap['info/user/'].state._conf.statePropName = 'favColours' 226 | path = 'info/user/favColours.*.visible.deeper.*' 227 | payload = ['primary', {red: true}] 228 | res = formatDeletor(path, payload, store, conf) 229 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/delete', _payload: 'primary.visible.deeper.red'}) 230 | }) 231 | 232 | test('[vuex-easy-firestore] has statePropName', t => { 233 | let res, path, payload, store, conf, deletePath 234 | store = getStore() 235 | conf = getConf() 236 | conf.vuexEasyFirestore = true 237 | // trigger vuex-easy-firestore action 238 | path = 'info/user/favColours.primary' 239 | deletePath = 'info/user/delete' 240 | store._modulesNamespaceMap['info/user/'] = {state: {_conf: {statePropName: 'favColours'}}} 241 | store._actions[deletePath] = true 242 | payload = 'red' 243 | res = formatDeletor(path, payload, store, conf) 244 | t.deepEqual(res, {command: 'dispatch', _path: deletePath, _payload: 'primary'}) 245 | }) 246 | -------------------------------------------------------------------------------- /test/firestore.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | // import store from './helpers/index.cjs.js' 3 | 4 | test('-', t => { t.pass() }) 5 | 6 | // test('setters', t => { 7 | // const modulePath = 'locationJournal/gymData/' 8 | // const prop = 'defeated.palletTown' 9 | // const a = store.get(modulePath + prop) 10 | // store.set(modulePath + prop, 1) 11 | // const b = store.get(modulePath + prop) 12 | // t.is(b, 1) 13 | // store.dispatch(modulePath + 'set/defeated.palletTown', 2) 14 | // const c = store.get(modulePath + prop) 15 | // t.is(c, 2) 16 | // store.commit(modulePath + prop, 3) 17 | // const d = store.get(modulePath + prop) 18 | // t.is(d, 3) 19 | // t.is(store.state.locationJournal.gymData.defeated.palletTown, 3) 20 | // }) 21 | 22 | // test('arraySetters', t => { 23 | // const modulePath = '' 24 | // const prop = 'pokemonBox.waterPokemon' 25 | // const array = store.state.pokemonBox.waterPokemon 26 | // // MUTATIONS 27 | // // push 28 | // const pushVal = store.commit(modulePath + prop + '.push', 'charmander') 29 | // t.is(array.length, 2) 30 | // t.is(array[1], 'charmander') 31 | // // pop 32 | // const popVal = store.commit(modulePath + prop + '.pop') 33 | // t.is(array.length, 1) 34 | // // shift 35 | // const shiftVal = store.commit(modulePath + prop + '.shift') 36 | // t.is(array.length, 0) 37 | // // splice 38 | // store.commit(modulePath + prop + '.push', 'warturtle') 39 | // const spliceVal = store.commit(modulePath + prop + '.splice', [0, 0, 'blastoise']) 40 | // t.is(array.length, 2) 41 | // t.is(array[0], 'blastoise') 42 | // // ACTIONS 43 | // // push 44 | // store.dispatch(modulePath + 'set/' + prop + '.push', 'psyduck') 45 | // const _pushVal = store.dispatch(modulePath + 'set/' + prop + '.push', 'starmie') 46 | // t.is(array.length, 4) 47 | // t.is(array[3], 'starmie') 48 | // // splice 49 | // const _spliceVal = store.dispatch(modulePath + 'set/' + prop + '.splice', [1, 1, 'poliwag']) 50 | // t.is(array.length, 4) 51 | // t.is(array[1], 'poliwag') 52 | // // shift and pop 53 | // const _shiftVal = store.dispatch(modulePath + 'set/' + prop + '.shift') 54 | // const _popVal = store.dispatch(modulePath + 'set/' + prop + '.pop') 55 | // t.is(array.length, 2) 56 | // }) 57 | 58 | // test('object Setters 1 (generated by empty state Object props)', t => { 59 | // const modulePath = 'dex/' 60 | // const prop = 'pokemonById' 61 | // // Add items 62 | // store.set(modulePath + prop + '.*', {id: '001', name: 'bulbasaur'}) 63 | // store.commit(modulePath + prop + '.*', {id: '004', name: 'charmander'}) 64 | // store.dispatch(modulePath + 'set/' + prop + '.*', {id: '007', name: 'squirtle'}) 65 | // // add some more to test deletions 66 | // store.set(modulePath + prop + '.*', {id: '002', name: 'ivysaur'}) 67 | // store.set(modulePath + prop + '.*', {id: '003', name: 'venusaur'}) 68 | // store.set(modulePath + prop + '.*', {id: '005', name: 'charmeleon'}) 69 | // store.set(modulePath + prop + '.*', {id: '006', name: 'charizard'}) 70 | // store.set(modulePath + prop + '.*', {id: '008', name: 'warturtle'}) 71 | // store.set(modulePath + prop + '.*', {id: '009', name: 'blastoise'}) 72 | // // check amount 73 | // let dex = store.state.dex.pokemonById 74 | 75 | // // make deletions 76 | // // store.delete(modulePath + prop, '002') // now ONLY * syntax allowed 77 | // // t.falsy(dex['002']) 78 | // store.delete(modulePath + prop + '.*', '003') 79 | // t.falsy(dex['003']) 80 | // // store.commit(modulePath + '-' + prop, '005') // now ONLY * syntax allowed 81 | // // t.falsy(dex['005']) 82 | // store.commit(modulePath + '-' + prop + '.*', '006') 83 | // t.falsy(dex['006']) 84 | // // store.dispatch(modulePath + 'delete/' + prop, '008') // now ONLY * syntax allowed 85 | // // t.falsy(dex['008']) 86 | // store.dispatch(modulePath + 'delete/' + prop + '.*', '009') 87 | // t.falsy(dex['009']) 88 | // dex = store.state.dex.pokemonById 89 | 90 | // // check if additions are still there 91 | // t.is(dex['001'].name, 'bulbasaur') 92 | // t.truthy(dex['001']) 93 | // t.is(dex['004'].name, 'charmander') 94 | // t.truthy(dex['004']) 95 | // t.is(dex['007'].name, 'squirtle') 96 | // t.truthy(dex['007']) 97 | // }) 98 | 99 | // test('object Setters 2 (generated by empty state Object props)', t => { 100 | // const modulePath = 'locationJournal/' 101 | // const prop = 'visitedPlaces' 102 | // const _celurean = {id: 'celurean city', visited: true, gym: true} 103 | // const _1 = {id: '1', visited: false, gym: true} 104 | // const _2 = {id: '2', visited: true, gym: false} 105 | // const _3 = {id: '3', visited: false, gym: false} 106 | // const _4 = {id: '4', gym: true} 107 | // const _5 = {id: '5', visited: true} 108 | // const _6 = {id: '6'} 109 | // const _7 = {id: '7', visited: true, gym: true} 110 | // const _8 = {id: '8', visited: true, gym: true} 111 | // const _9 = {id: '9', visited: true, gym: true} 112 | // const _10 = {id: '10', visited: true, gym: true} 113 | // const _11 = {id: '11', visited: true, gym: true} 114 | // let places = store.state.locationJournal.visitedPlaces 115 | // const initialLength = Object.keys(places).length 116 | // t.is(Object.keys(places).length, 0 + initialLength) 117 | 118 | // // add 119 | // store.commit(modulePath + prop + '.*', _celurean) 120 | // store.dispatch(modulePath + 'set/' + prop + '.*', _4) 121 | // store.set(modulePath + prop + '.*', _6) 122 | // t.is(Object.keys(places).length, 3 + initialLength) 123 | // t.truthy(places[_celurean.id]) 124 | // t.truthy(places[_4.id]) 125 | // t.truthy(places[_6.id]) 126 | // t.is(places[_celurean.id].visited, true) 127 | // t.is(places[_celurean.id].gym, true) 128 | // t.is(places[_4.id].visited, false) 129 | // t.is(places[_4.id].gym, true) 130 | // t.is(places[_6.id].visited, false) 131 | // t.is(places[_6.id].gym, false) 132 | 133 | // // set sub props 134 | // store.set(modulePath + prop + '.*', _1) 135 | // t.is(Object.keys(places).length, 4 + initialLength) 136 | // // sub props: commit 137 | // t.is(places[_1.id].visited, false) 138 | // t.is(places[_1.id].gym, true) 139 | // store.commit(modulePath + prop + '.*', {id: _1.id, visited: 'booyah!'}) 140 | // store.commit(modulePath + prop + '.*.gym', [_1.id, 'booyah!']) 141 | // t.is(places[_1.id].visited, 'booyah!') 142 | // t.is(places[_1.id].gym, 'booyah!') 143 | // // sub props: dispatch 144 | // t.is(places[_4.id].visited, false) 145 | // t.is(places[_4.id].gym, true) 146 | // store.dispatch(modulePath + 'set/' + prop + '.*', {id: _4.id, visited: 'booyah!'}) 147 | // store.dispatch(modulePath + 'set/' + prop + '.*.gym', [_4.id, 'booyah!']) 148 | // t.is(places[_4.id].visited, 'booyah!') 149 | // t.is(places[_4.id].gym, 'booyah!') 150 | // // sub props: set 151 | // store.set(modulePath + prop + '.*', {id: _6.id, visited: 'booyah!'}) 152 | // store.set(modulePath + prop + '.*.gym', [_6.id, 'booyah!']) 153 | // t.is(places[_6.id].visited, 'booyah!') 154 | // t.is(places[_6.id].gym, 'booyah!') 155 | 156 | // // Set props not part of the item 157 | // // store.commit(modulePath + prop + '.*.new', {id: _1.id, val: 'new prop'}) 158 | // // store.dispatch(modulePath + 'set/' + prop + '.*.new', {id: _4.id, val: 'new prop'}) 159 | // // store.set(modulePath + prop + '.*.new', {id: _6.id, val: 'new prop'}) 160 | // // t.is(places[_1.id].new, 'new prop') 161 | // // t.is(places[_4.id].new, 'new prop') 162 | // // t.is(places[_6.id].new, 'new prop') 163 | 164 | // // sub props: with spaces in id 165 | // store.commit(modulePath + prop + '.*', {id: _celurean.id, visited: 'booyah!'}) 166 | // store.commit(modulePath + prop + '.*.gym', [_celurean.id, 'booyah!']) 167 | // t.is(places[_celurean.id].visited, 'booyah!') 168 | // t.is(places[_celurean.id].gym, 'booyah!') 169 | 170 | // // delete 171 | // store.delete(modulePath + prop + '.*', _celurean.id) 172 | // store.commit(modulePath + '-' + prop + '.*', _6.id) 173 | // store.dispatch(modulePath + 'delete/' + prop + '.*', _1.id) 174 | // t.is(Object.keys(places).length, 1 + initialLength) 175 | 176 | // }) 177 | 178 | // test('double id setters', t => { 179 | // const _027 = {id: '027', name: 'Sandshrew'} 180 | // const _043 = {id: '043', name: 'Oddish'} 181 | // const _077 = {id: '_077_', name: 'Ponyta'} 182 | // store.set('dexDB/pokemonById.*', _027) 183 | // store.set('dexDB/pokemonById.*', {[_043.id]: _043}) 184 | // store.set('dexDB/pokemonById.*', _077) 185 | // let dex = store.state.dexDB.pokemonById 186 | // t.is(dex[_027.id].name, 'Sandshrew') 187 | // t.is(dex[_043.id].name, 'Oddish') 188 | // t.is(dex[_077.id].name, 'Ponyta') 189 | // store.set('dexDB/pokemonById.*.tags.*', [_027.id, {'ground': true}]) 190 | // store.set('dexDB/pokemonById.*.tags.*', [_043.id, {'grass': true}]) 191 | // store.set('dexDB/pokemonById.*.tags.*', [_077.id, {'fire': true}]) 192 | // // store.set(`dexDB/pokemonById._007_.tags.*`, {fire: true}) 193 | // const id = 'dark' 194 | // store.set('dexDB/pokemonById.*.tags.*', [_027.id, {[id]: true}]) 195 | // store.set('dexDB/pokemonById.*.tags.*', [_043.id, {[id]: true}]) 196 | // store.set('dexDB/pokemonById.*.tags.*', [_077.id, {[id]: true}]) 197 | // t.truthy(dex[_027.id].tags['dark']) 198 | // t.truthy(dex[_043.id].tags['dark']) 199 | // t.truthy(dex[_077.id].tags['dark']) 200 | // t.is(dex[_027.id].tags['dark'], true) 201 | // t.is(dex[_043.id].tags['dark'], true) 202 | // t.is(dex[_077.id].tags['dark'], true) 203 | // // deletes 204 | // // store.delete('dexDB/pokemonById.*.tags.*', [_027.id, 'dark']) 205 | // // store.dispatch('dexDB/delete/pokemonById.*.tags.*', [_043.id, 'dark']) 206 | // // store.commit('dexDB/-pokemonById.*.tags.*', [_077.id, 'dark']) 207 | // t.is(dex[_027.id].name, 'Sandshrew') 208 | // t.is(dex[_043.id].name, 'Oddish') 209 | // t.is(dex[_077.id].name, 'Ponyta') 210 | // t.truthy(dex[_027.id].tags['ground']) 211 | // t.truthy(dex[_043.id].tags['grass']) 212 | // t.truthy(dex[_077.id].tags['fire']) 213 | // t.is(dex[_027.id].tags['ground'], true) 214 | // t.is(dex[_043.id].tags['grass'], true) 215 | // t.is(dex[_077.id].tags['fire'], true) 216 | // // t.falsy(dex[_027.id].tags['dark']) 217 | // // t.falsy(dex[_043.id].tags['dark']) 218 | // // t.falsy(dex[_077.id].tags['dark']) 219 | // }) 220 | -------------------------------------------------------------------------------- /src/pathUtils.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isString, isObject } from 'is-what' 2 | import error from './errors' 3 | import { AnyObject } from './declarations' 4 | 5 | /** 6 | * gets an ID from a single piece of payload. 7 | * 8 | * @param {(object | ({ id: string } & object) | string)} payloadPiece 9 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 10 | * @param {string} [path] (optional - for error handling) the path called 11 | * @param {(any[] | object | string)} [fullPayload] (optional - for error handling) the full payload on which each was `getId()` called 12 | * @returns {string} the id 13 | */ 14 | function getId ( 15 | payloadPiece: object | ({ id: string } & object) | string, 16 | conf?: object, 17 | path?: string, 18 | fullPayload?: any[] | object | string 19 | ): string { 20 | if (isObject(payloadPiece)) { 21 | if ('id' in payloadPiece) return payloadPiece.id 22 | if (Object.keys(payloadPiece).length === 1) return Object.keys(payloadPiece)[0] 23 | } 24 | if (isString(payloadPiece)) return payloadPiece 25 | error('wildcardFormatWrong', conf, path) 26 | return '' 27 | } 28 | 29 | /** 30 | * Get all ids from an array payload. 31 | * 32 | * @param {any[]} payload 33 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 34 | * @param {string} [path] (optional - for error handling) the path called 35 | * @returns {string[]} all ids 36 | */ 37 | export function getIdsFromPayload (payload: any[], conf?: object, path?: string): string[] { 38 | return payload.map(payloadPiece => getId(payloadPiece, conf, path, payload)) 39 | } 40 | 41 | /** 42 | * Returns a value of a payload piece. Eg. {[id]: 'val'} will return 'val' 43 | * 44 | * @param {(object | string)} payloadPiece 45 | * @returns {any} 46 | */ 47 | export function getValueFromPayloadPiece (payloadPiece: object | string): any { 48 | if ( 49 | isObject(payloadPiece) && 50 | !('id' in payloadPiece) && 51 | Object.keys(payloadPiece).length === 1 52 | ) { 53 | return Object.values(payloadPiece)[0] 54 | } 55 | return payloadPiece 56 | } 57 | 58 | /** 59 | * Checks the ratio between an array of IDs and a path with wildcards 60 | * 61 | * @param {string[]} ids 62 | * @param {string} path 63 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 64 | * @returns {boolean} true if no problem. false if the ratio is incorrect 65 | */ 66 | export function checkIdWildcardRatio (ids: string[], path: string, conf: object): boolean { 67 | const match = path.match(/\*/g) 68 | const idCount = (isArray(match)) 69 | ? match.length 70 | : 0 71 | if (ids.length === idCount) return true 72 | error('mutationSetterPropPathWildcardIdCount', conf) 73 | return false 74 | } 75 | 76 | /** 77 | * Fill in IDs at '*' in a path, based on the IDs received. 78 | * 79 | * @param {string[]} ids 80 | * @param {string} path 'path.*.with.*.wildcards' 81 | * @param {object} [state] RELATIVE TO PATH START! the state to check if the value actually exists 82 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 83 | * @returns {string} The path with '*' replaced by IDs 84 | */ 85 | export function fillinPathWildcards ( 86 | ids: string[], 87 | path: string, 88 | state?: object, 89 | conf?: object 90 | ): string { 91 | // Ignore pool check if '*' comes last 92 | const ignorePoolCheckOn = (path.endsWith('*')) ? ids[ids.length - 1] : null 93 | ids.forEach((_id, _index, _array) => { 94 | const idIndex = path.indexOf('*') 95 | const pathUntilPool = path.substring(0, idIndex) 96 | // check for errors when both state and conf are passed 97 | // pathUntilPool can be '' in case the path starts with '*' 98 | if (ignorePoolCheckOn !== _id && state && conf) { 99 | const pool = (pathUntilPool) 100 | ? getDeepRef(state, pathUntilPool) 101 | : state 102 | if (pool[_id] === undefined) return error('mutationSetterPropPathWildcardMissingItemDoesntExist', conf, pathUntilPool, _id) 103 | } 104 | path = path 105 | .split('') 106 | .map((char, ind) => ind === idIndex ? _id : char) 107 | .join('') 108 | }) 109 | return path 110 | } 111 | 112 | /** 113 | * ('/sub.prop', payload) becomes → {sub: {prop: payload}} 114 | * ('sub', payload) becomes → {sub: payload} 115 | * 116 | * @param {string} path 'a/path/like.this' 117 | * @param {*} payload 118 | * @param {object} [state] the state to check if the value actually exists 119 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 120 | * @returns {AnyObject} a nested object re-created based on the path & payload 121 | */ 122 | export function createObjectFromPath ( 123 | path: string, 124 | payload: any, 125 | state?: object, 126 | conf?: object 127 | ): AnyObject { 128 | // edge cases 129 | if (path === '*') return payload 130 | if (!path.includes('.') && !path.includes('/')) return {[path]: payload} 131 | // start 132 | let newValue = payload 133 | if (path.includes('*')) { 134 | // only work with arrays 135 | if (!isArray(payload)) payload = [payload] 136 | const lastPayloadPiece = payload.pop() 137 | let ids = payload 138 | // CASE: 'dex/pokemonById.*.tags' 139 | if (!path.endsWith('*')) { 140 | newValue = lastPayloadPiece 141 | } 142 | // CASE: 'dex/pokemonById.*.tags.*' 143 | if (path.endsWith('*')) { 144 | const lastId = getId(lastPayloadPiece, conf, path) 145 | ids.push(lastId) 146 | newValue = getValueFromPayloadPiece(lastPayloadPiece) 147 | if (isObject(newValue)) newValue.id = lastId 148 | } 149 | ids = ids.map(_id => { 150 | _id = _id.replace('.', '_____dot_____') 151 | _id = _id.replace('/', '_____slash_____') 152 | return _id 153 | }) 154 | if (!checkIdWildcardRatio(ids, path, conf)) return 155 | const pathWithIds = fillinPathWildcards(ids, path, state, conf) 156 | path = pathWithIds 157 | } 158 | // important to set the result here and not return the reduce directly! 159 | const result = {} 160 | path.match(/[^\/^\.]+/g) 161 | .reduce((carry, _prop, index, array) => { 162 | _prop = _prop.replace('_____dot_____', '.') 163 | _prop = _prop.replace('_____slash_____', '/') 164 | const container = (index === array.length - 1) 165 | ? newValue 166 | : {} 167 | carry[_prop] = container 168 | return container 169 | }, result) 170 | return result 171 | } 172 | 173 | /** 174 | * Returns the keys of a path 175 | * 176 | * @param {string} path a/path/like.this 177 | * @returns {string[]} with keys 178 | */ 179 | export function getKeysFromPath (path: string): string[] { 180 | if (!path) return [] 181 | return path.match(/[^\/^\.]+/g) 182 | } 183 | 184 | /** 185 | * Gets a deep property in an object, based on a path to that property 186 | * 187 | * @param {object} target an object to wherefrom to retrieve the deep reference of 188 | * @param {string} path 'path/to.prop' 189 | * @returns {AnyObject} the last prop in the path 190 | */ 191 | export function getDeepRef (target: object = {}, path: string): AnyObject { 192 | const keys = getKeysFromPath(path) 193 | if (!keys.length) return target 194 | let obj = target 195 | while (obj && keys.length > 1) { 196 | obj = obj[keys.shift()] 197 | } 198 | const key = keys.shift() 199 | if (obj && obj.hasOwnProperty(key)) { 200 | return obj[key] 201 | } 202 | } 203 | 204 | /** 205 | * Gets a deep property in an object, based on a path to that property 206 | * 207 | * @param {object} target the Object to get the value of 208 | * @param {string} path 'path/to/prop.subprop' 209 | * @returns {AnyObject} the property's value 210 | */ 211 | export function getDeepValue (target: object, path: string): AnyObject { 212 | return getDeepRef(target, path) 213 | } 214 | 215 | /** 216 | * Sets a value to a deep property in an object, based on a path to that property 217 | * 218 | * @param {object} target the Object to set the value on 219 | * @param {string} path 'path/to/prop.subprop' 220 | * @param {*} value the value to set 221 | * @returns {AnyObject} the original target object 222 | */ 223 | export function setDeepValue (target: object, path: string, value: any): AnyObject { 224 | const keys = getKeysFromPath(path) 225 | const lastKey = keys.pop() 226 | const deepRef = getDeepRef(target, keys.join('.')) 227 | if (deepRef && deepRef.hasOwnProperty(lastKey)) { 228 | deepRef[lastKey] = value 229 | } 230 | return target 231 | } 232 | 233 | /** 234 | * Pushes a value in an array which is a deep property in an object, based on a path to that property 235 | * 236 | * @param {object} target the Object to push the value on 237 | * @param {string} path 'path/to.sub.prop' 238 | * @param {*} value the value to push 239 | * @returns {number} the new length of the array 240 | */ 241 | export function pushDeepValue (target: object, path: string, value: any): number { 242 | const deepRef = getDeepRef(target, path) 243 | if (!isArray(deepRef)) return 244 | return deepRef.push(value) 245 | } 246 | 247 | /** 248 | * Pops a value of an array which is a deep property in an object, based on a path to that property 249 | * 250 | * @param {object} target the Object to pop the value of 251 | * @param {string} path 'path.to.sub.prop' 252 | * @returns {*} the popped value 253 | */ 254 | export function popDeepValue (target: object, path: string): any { 255 | const deepRef = getDeepRef(target, path) 256 | if (!isArray(deepRef)) return 257 | return deepRef.pop() 258 | } 259 | 260 | /** 261 | * Shift a value of an array which is a deep property in an object, based on a path to that property 262 | * 263 | * @param {object} target the Object to shift the value of 264 | * @param {string} path 'path.to.sub.prop' 265 | * @returns {*} the shifted value 266 | */ 267 | export function shiftDeepValue (target: object, path: string): any { 268 | const deepRef = getDeepRef(target, path) 269 | if (!isArray(deepRef)) return 270 | return deepRef.shift() 271 | } 272 | 273 | /** 274 | * Splice into an array which is a deep property in an object, based on a path to that property 275 | * 276 | * @param {object} target the Object to splice the value of 277 | * @param {string} path 'path/to.sub.prop' 278 | * @param {number} [index=0] the index to splice in the value, defaults to 0 279 | * @param {number} [deleteCount=0] the amount of items to delete, defaults to 0 280 | * @param {*} value the value to splice in 281 | * @returns {any[]} an array containing the deleted elements 282 | */ 283 | export function spliceDeepValue ( 284 | target: object, 285 | path: string, 286 | index: number = 0, 287 | deleteCount: number = 0, 288 | value: any 289 | ): any[] { 290 | const deepRef = getDeepRef(target, path) 291 | if (!isArray(deepRef)) return 292 | if (value === undefined) return deepRef.splice(index, deleteCount) 293 | return deepRef.splice(index, deleteCount, value) 294 | } 295 | -------------------------------------------------------------------------------- /src/makeMutations.ts: -------------------------------------------------------------------------------- 1 | import { getValueFromPayloadPiece, checkIdWildcardRatio, getIdsFromPayload, fillinPathWildcards, setDeepValue, getDeepRef, pushDeepValue, popDeepValue, shiftDeepValue, spliceDeepValue } from './pathUtils' 2 | import { isObject, isArray } from 'is-what' 3 | import error from './errors' 4 | import Vue from 'vue' 5 | import merge from 'merge-anything' 6 | import defaultConf from './defaultConfig' 7 | import { IDefaultConfig, AnyObject } from './declarations' 8 | 9 | interface IInfoNS { 10 | moduleNamespace: string 11 | } 12 | 13 | // define possible functions 14 | // eslint-disable-next-line 15 | function SET_PROP_SUBPROP ( 16 | state: AnyObject, 17 | payload: any, 18 | PROP_SUBPROP: string 19 | ): AnyObject { 20 | return setDeepValue(state, PROP_SUBPROP, payload) 21 | } 22 | 23 | // eslint-disable-next-line 24 | function DELETE_PROP_SUBPROP ( 25 | state: AnyObject, 26 | PROP_SUBPROP: string 27 | ) { 28 | const propsArray = (PROP_SUBPROP.includes('.')) 29 | ? PROP_SUBPROP.split('.') 30 | : [PROP_SUBPROP] 31 | const lastProp = propsArray.pop() 32 | const propsWithoutLast = propsArray.join('.') 33 | const ref = getDeepRef(state, propsWithoutLast) 34 | return Vue.delete(ref, lastProp) 35 | } 36 | 37 | // eslint-disable-next-line 38 | function MUTATE_PROP_x_SUBPROP ( 39 | state: AnyObject, 40 | payload: any, 41 | PROP_SUBPROP: string, 42 | conf: IDefaultConfig 43 | ): AnyObject { 44 | if (!isArray(payload)) payload = [payload] 45 | const newValue = payload.pop() 46 | const ids = getIdsFromPayload(payload, conf, PROP_SUBPROP) 47 | if (!checkIdWildcardRatio(ids, PROP_SUBPROP, conf)) return 48 | const pathWithIds = fillinPathWildcards(ids, PROP_SUBPROP, state, conf) 49 | return setDeepValue(state, pathWithIds, newValue) 50 | } 51 | 52 | // eslint-disable-next-line 53 | function DELETE_PROP_x_SUBPROP ( 54 | state: AnyObject, 55 | payload: any, 56 | PROP_SUBPROP: string, 57 | conf: IDefaultConfig 58 | ) { 59 | const propsArray = (PROP_SUBPROP.includes('.')) 60 | ? PROP_SUBPROP.split('.') 61 | : [PROP_SUBPROP] 62 | const lastProp = propsArray.pop() 63 | const propsWithoutLast = propsArray.join('.') 64 | const ids = payload 65 | if (!checkIdWildcardRatio(ids, propsWithoutLast, conf)) return 66 | const pathWithIds = fillinPathWildcards(ids, propsWithoutLast, state, conf) 67 | const ref = getDeepRef(state, pathWithIds) 68 | return Vue.delete(ref, lastProp) 69 | } 70 | 71 | // eslint-disable-next-line 72 | function MUTATE_PROP_x ( 73 | state: AnyObject, 74 | payload: any, 75 | PROP_SUBPROP: string, 76 | conf: IDefaultConfig, 77 | propValue: any 78 | ) { 79 | if (!isArray(payload)) payload = [payload] 80 | const ids = getIdsFromPayload(payload, conf, PROP_SUBPROP) 81 | if (!checkIdWildcardRatio(ids, PROP_SUBPROP, conf)) return 82 | const lastId = ids.pop() 83 | const propPathWithoutLast = PROP_SUBPROP.slice(0, -1) 84 | const pathWithIds = fillinPathWildcards(ids, propPathWithoutLast, state, conf) 85 | const ref = getDeepRef(state, pathWithIds) 86 | let newValue = getValueFromPayloadPiece(payload.pop()) 87 | if (isObject(newValue)) newValue.id = lastId 88 | if (isObject(propValue)) newValue = merge(propValue, newValue) 89 | return Vue.set(ref, lastId, newValue) 90 | } 91 | 92 | // eslint-disable-next-line 93 | function DELETE_PROP_x ( 94 | state: AnyObject, 95 | id: string, 96 | PROP_SUBPROP: string, 97 | conf: IDefaultConfig 98 | ) { 99 | if (!id) return error('mutationDeleteNoId', conf, PROP_SUBPROP) 100 | const ids = (!isArray(id)) ? [id] : id 101 | if (!checkIdWildcardRatio(ids, PROP_SUBPROP, conf)) return 102 | const lastId = ids.pop() 103 | const pathWithoutWildcard = (PROP_SUBPROP.endsWith('*')) 104 | ? PROP_SUBPROP.slice(0, -1) 105 | : PROP_SUBPROP 106 | const pathWithIds = fillinPathWildcards(ids, pathWithoutWildcard, state, conf) 107 | const ref = getDeepRef(state, pathWithIds) 108 | return Vue.delete(ref, lastId) 109 | } 110 | 111 | // execute mutation 112 | function executeArrayMutation ( 113 | state: AnyObject, 114 | payload: any, 115 | action: string, 116 | PROP_SUBPROP: string, 117 | conf: IDefaultConfig 118 | ) { 119 | let newValue, pathWithIds 120 | if (!PROP_SUBPROP.includes('*')) { 121 | newValue = payload 122 | pathWithIds = PROP_SUBPROP 123 | } else { 124 | if (!isArray(payload) && action !== 'splice') payload = [payload] 125 | if (action !== 'pop' && action !== 'shift') newValue = payload.pop() 126 | const ids = getIdsFromPayload(payload, conf, PROP_SUBPROP) 127 | if (!checkIdWildcardRatio(ids, PROP_SUBPROP, conf)) return 128 | pathWithIds = fillinPathWildcards(ids, PROP_SUBPROP, state, conf) 129 | } 130 | if (action === 'push') { 131 | return pushDeepValue(state, pathWithIds, newValue) 132 | } 133 | if (action === 'pop') { 134 | return popDeepValue(state, pathWithIds) 135 | } 136 | if (action === 'shift') { 137 | return shiftDeepValue(state, pathWithIds) 138 | } 139 | if (action === 'splice') { 140 | const index = newValue[0] 141 | const deleteCount = newValue[1] 142 | const value = newValue[2] 143 | return spliceDeepValue(state, pathWithIds, index, deleteCount, value) 144 | } 145 | } 146 | 147 | /** 148 | * Creates the mutations for each property of the object passed recursively 149 | * 150 | * @param {object} propParent an Object of which all props will get a mutation 151 | * @param {(string | null)} path the path taken until the current propParent instance 152 | * @param {IDefaultConfig} conf user config 153 | * @param {string} [infoNS] (optional) module namespace in light of ignoreProps config 154 | * 155 | * @returns {AnyObject} all mutations for each property. 156 | */ 157 | function makeMutationsForAllProps ( 158 | propParent: object, 159 | path: string | null, 160 | conf: IDefaultConfig, 161 | infoNS?: IInfoNS, 162 | ): AnyObject { 163 | if (!isObject(propParent)) return {} 164 | return Object.keys(propParent) 165 | .reduce((mutations, prop) => { 166 | // Get the path info up until this point 167 | const PROP_SUBPROP = (!path) 168 | ? prop 169 | : path + '.' + prop 170 | // Avoid making setters for private props 171 | if (conf.ignorePrivateProps && prop[0] === '_') return mutations 172 | if (conf.ignoreProps.some(ignPropFull => { 173 | const separatePropFromNS = /(.*?)\/([^\/]*?)$/.exec(ignPropFull) 174 | const ignPropNS = (separatePropFromNS) ? separatePropFromNS[1] + '/' : '' 175 | const ignProp = (separatePropFromNS) ? separatePropFromNS[2] : ignPropFull 176 | return ( 177 | (!infoNS && ignProp === PROP_SUBPROP) || 178 | (infoNS && infoNS.moduleNamespace === ignPropNS && ignProp === PROP_SUBPROP) 179 | ) 180 | })) { 181 | return mutations 182 | } 183 | // Get the value of the prop 184 | const propValue = propParent[prop] 185 | 186 | // =================================================> 187 | // SET & DELETE MUTATION NAMES 188 | // =================================================> 189 | const SET = (conf.pattern === 'traditional') 190 | ? 'SET_' + PROP_SUBPROP.toUpperCase() 191 | : PROP_SUBPROP 192 | const DELETE = (conf.pattern === 'traditional') 193 | ? 'DELETE_' + PROP_SUBPROP.toUpperCase() 194 | : '-' + PROP_SUBPROP 195 | // =================================================> 196 | // PROP MUTATION 197 | // =================================================> 198 | // All good, make the mutation! 199 | if (!PROP_SUBPROP.includes('*')) { 200 | mutations[SET] = (state, payload) => SET_PROP_SUBPROP(state, payload, PROP_SUBPROP) 201 | mutations[DELETE] = (state) => DELETE_PROP_SUBPROP(state, PROP_SUBPROP) 202 | } else if (prop !== '*') { 203 | // path includes wildcard, but prop is not a wildcard 204 | mutations[SET] = (state, payload) => MUTATE_PROP_x_SUBPROP(state, payload, PROP_SUBPROP, conf) 205 | mutations[DELETE] = (state, payload) => DELETE_PROP_x_SUBPROP(state, payload, PROP_SUBPROP, conf) 206 | } 207 | // =================================================> 208 | // WILDCARD MUTATION 209 | // =================================================> 210 | if (prop === '*') { 211 | mutations[SET] = (state, payload) => MUTATE_PROP_x(state, payload, PROP_SUBPROP, conf, propValue) 212 | mutations[DELETE] = (state, payload) => DELETE_PROP_x(state, payload, PROP_SUBPROP, conf) 213 | } 214 | // =================================================> 215 | // ARRAY MUTATIONS 216 | // =================================================> 217 | 218 | if (isArray(propValue)) { 219 | // PUSH mutation name 220 | const push = (conf.pattern === 'traditional') 221 | ? 'PUSH_' + PROP_SUBPROP.toUpperCase() 222 | : PROP_SUBPROP + '.push' 223 | mutations[push] = (state, payload) => { 224 | return executeArrayMutation(state, payload, 'push', PROP_SUBPROP, conf) 225 | } 226 | // POP mutation name 227 | const pop = (conf.pattern === 'traditional') 228 | ? 'POP_' + PROP_SUBPROP.toUpperCase() 229 | : PROP_SUBPROP + '.pop' 230 | mutations[pop] = (state, payload) => { 231 | return executeArrayMutation(state, payload, 'pop', PROP_SUBPROP, conf) 232 | } 233 | // SHIFT mutation name 234 | const shift = (conf.pattern === 'traditional') 235 | ? 'SHIFT_' + PROP_SUBPROP.toUpperCase() 236 | : PROP_SUBPROP + '.shift' 237 | mutations[shift] = (state, payload) => { 238 | return executeArrayMutation(state, payload, 'shift', PROP_SUBPROP, conf) 239 | } 240 | // SPLICE mutation name 241 | const splice = (conf.pattern === 'traditional') 242 | ? 'SPLICE_' + PROP_SUBPROP.toUpperCase() 243 | : PROP_SUBPROP + '.splice' 244 | mutations[splice] = (state, payload) => { 245 | return executeArrayMutation(state, payload, 'splice', PROP_SUBPROP, conf) 246 | } 247 | } 248 | // =================================================> 249 | // CHILDREN MUTATIONS 250 | // =================================================> 251 | if (isObject(propValue) && Object.keys(propValue).length) { 252 | const childrenMutations = makeMutationsForAllProps(propValue, PROP_SUBPROP, conf, infoNS) 253 | Object.assign(mutations, childrenMutations) 254 | } 255 | return mutations 256 | }, {}) 257 | } 258 | 259 | /** 260 | * Creates all mutations for the state of a module. 261 | * Usage: 262 | * commit('module/path/SET_PATH.TO.PROP', newValue) 263 | * Import method: 264 | * mutations { 265 | * ...defaultMutations(initialState) 266 | * } 267 | * 268 | * @param {object} initialState the initial state of a module 269 | * @param {IDefaultConfig} [userConf={}] (optional) user config 270 | * @param {IInfoNS} [infoNS] (optional) info on the module namespace 271 | * @returns {AnyObject} all mutations for the state 272 | */ 273 | export function defaultMutations ( 274 | initialState: object, 275 | userConf: IDefaultConfig = {}, 276 | infoNS?: IInfoNS 277 | ): AnyObject { 278 | const mergedConf: IDefaultConfig = Object.assign({}, defaultConf, userConf) 279 | return makeMutationsForAllProps(initialState, null, mergedConf, infoNS) 280 | } 281 | -------------------------------------------------------------------------------- /test/defaultSetter.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { formatSetter } from './helpers/makeSetters.cjs' 3 | 4 | function getStore () { 5 | return { 6 | state: {}, 7 | _actions: {}, 8 | _mutations: {}, 9 | _modulesNamespaceMap: {} 10 | } 11 | } 12 | 13 | function getConf () { 14 | return { 15 | setter: 'set', 16 | getter: 'get', 17 | deletor: 'delete', 18 | vuexEasyFirestore: false, 19 | ignorePrivateProps: true, 20 | ignoreProps: [], 21 | pattern: 'simple' 22 | } 23 | } 24 | 25 | test('[simple] setters no module', t => { 26 | let res, path, payload, store, conf 27 | store = getStore() 28 | conf = getConf() 29 | // trigger mutation 30 | path = 'favColours.primary' 31 | store._actions[path] = false 32 | store._mutations[path] = true 33 | payload = 'red' 34 | res = formatSetter(path, payload, store, conf) 35 | t.deepEqual(res, {command: 'commit', _path: 'favColours.primary', _payload: 'red'}) 36 | // trigger action 37 | store._actions[path] = true 38 | store._mutations[path] = true 39 | res = formatSetter(path, payload, store, conf) 40 | t.deepEqual(res, {command: 'dispatch', _path: 'favColours.primary', _payload: 'red'}) 41 | }) 42 | 43 | test('[simple] setters single module & prop', t => { 44 | let res, path, payload, store, conf 45 | store = getStore() 46 | conf = getConf() 47 | // trigger mutation 48 | path = 'colours/favColour' 49 | store._actions[path] = false 50 | store._mutations[path] = true 51 | payload = 'red' 52 | res = formatSetter(path, payload, store, conf) 53 | t.deepEqual(res, {command: 'commit', _path: 'colours/favColour', _payload: 'red'}) 54 | // trigger action 55 | store._actions[path] = true 56 | store._mutations[path] = true 57 | res = formatSetter(path, payload, store, conf) 58 | t.deepEqual(res, {command: 'dispatch', _path: 'colours/favColour', _payload: 'red'}) 59 | }) 60 | 61 | test('[simple] setters deep', t => { 62 | let res, path, payload, store, conf 63 | store = getStore() 64 | conf = getConf() 65 | // trigger mutation 66 | path = 'info/user/favColours.primary' 67 | store._actions[path] = false 68 | store._mutations[path] = true 69 | payload = 'red' 70 | res = formatSetter(path, payload, store, conf) 71 | t.deepEqual(res, {command: 'commit', _path: 'info/user/favColours.primary', _payload: 'red'}) 72 | // trigger action 73 | store._actions[path] = true 74 | store._mutations[path] = true 75 | res = formatSetter(path, payload, store, conf) 76 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/favColours.primary', _payload: 'red'}) 77 | }) 78 | 79 | test('[traditional] setters no module', t => { 80 | let res, path, payload, store, conf 81 | store = getStore() 82 | conf = getConf() 83 | conf.pattern = 'traditional' 84 | // trigger mutation 85 | path = 'favColours.primary' 86 | store._actions['setFavColours.primary'] = false 87 | store._mutations['SET_FAVCOLOURS.PRIMARY'] = true 88 | payload = 'red' 89 | res = formatSetter(path, payload, store, conf) 90 | t.deepEqual(res, {command: 'commit', _path: 'SET_FAVCOLOURS.PRIMARY', _payload: 'red'}) 91 | // trigger action 92 | store._actions['setFavColours.primary'] = true 93 | store._mutations['SET_FAVCOLOURS.PRIMARY'] = true 94 | res = formatSetter(path, payload, store, conf) 95 | t.deepEqual(res, {command: 'dispatch', _path: 'setFavColours.primary', _payload: 'red'}) 96 | }) 97 | 98 | test('[traditional] setters single module & prop', t => { 99 | let res, path, payload, store, conf 100 | store = getStore() 101 | conf = getConf() 102 | conf.pattern = 'traditional' 103 | // trigger mutation 104 | path = 'colours/favColour' 105 | store._actions['colours/setFavColour'] = false 106 | store._mutations['colours/SET_FAVCOLOUR'] = true 107 | payload = 'red' 108 | res = formatSetter(path, payload, store, conf) 109 | t.deepEqual(res, {command: 'commit', _path: 'colours/SET_FAVCOLOUR', _payload: 'red'}) 110 | // trigger action 111 | store._actions['colours/setFavColour'] = true 112 | store._mutations['colours/SET_FAVCOLOUR'] = true 113 | res = formatSetter(path, payload, store, conf) 114 | t.deepEqual(res, {command: 'dispatch', _path: 'colours/setFavColour', _payload: 'red'}) 115 | }) 116 | 117 | test('[traditional] setters deep', t => { 118 | let res, path, payload, store, conf 119 | store = getStore() 120 | conf = getConf() 121 | conf.pattern = 'traditional' 122 | // trigger mutation 123 | path = 'info/user/favColours.primary' 124 | store._actions['info/user/setFavColours.primary'] = false 125 | store._mutations['info/user/SET_FAVCOLOURS.PRIMARY'] = true 126 | payload = 'red' 127 | res = formatSetter(path, payload, store, conf) 128 | t.deepEqual(res, {command: 'commit', _path: 'info/user/SET_FAVCOLOURS.PRIMARY', _payload: 'red'}) 129 | // trigger action 130 | store._actions['info/user/setFavColours.primary'] = true 131 | store._mutations['info/user/SET_FAVCOLOURS.PRIMARY'] = true 132 | res = formatSetter(path, payload, store, conf) 133 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/setFavColours.primary', _payload: 'red'}) 134 | }) 135 | 136 | test('[vuex-easy-firestore] trigger user action first', t => { 137 | let res, path, payload, store, conf 138 | store = getStore() 139 | conf = getConf() 140 | conf.vuexEasyFirestore = true 141 | // trigger user action 142 | path = 'colours/favColour' 143 | store._modulesNamespaceMap['colours/'] = {state: {_conf: {statePropName: ''}}} 144 | store._actions['colours/set'] = true 145 | store._actions[path] = true 146 | payload = 'red' 147 | res = formatSetter(path, payload, store, conf) 148 | t.deepEqual(res, {command: 'dispatch', _path: 'colours/favColour', _payload: 'red'}) 149 | }) 150 | 151 | test('[vuex-easy-firestore] setters single module & prop', t => { 152 | let res, path, payload, store, conf 153 | store = getStore() 154 | conf = getConf() 155 | conf.vuexEasyFirestore = true 156 | // trigger vuex-easy-firestore action 157 | path = 'colours/favColour' 158 | store._modulesNamespaceMap['colours/'] = {state: {_conf: {statePropName: ''}}} 159 | store._actions['colours/set'] = true 160 | payload = 'red' 161 | res = formatSetter(path, payload, store, conf) 162 | t.deepEqual(res, {command: 'dispatch', _path: 'colours/set', _payload: {favColour: 'red'}}) 163 | }) 164 | 165 | test('[vuex-easy-firestore] setters deep', t => { 166 | let res, path, payload, store, conf 167 | store = getStore() 168 | conf = getConf() 169 | conf.vuexEasyFirestore = true 170 | // trigger vuex-easy-firestore action 171 | path = 'info/user/favColours.primary' 172 | store._modulesNamespaceMap['info/user/'] = {state: {_conf: {statePropName: ''}}} 173 | store._actions['info/user/set'] = true 174 | payload = 'red' 175 | res = formatSetter(path, payload, store, conf) 176 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {favColours: {primary: 'red'}}}) 177 | }) 178 | 179 | test('[vuex-easy-firestore] wildcards', t => { 180 | let res, path, payload, store, conf 181 | store = getStore() 182 | conf = getConf() 183 | conf.vuexEasyFirestore = true 184 | // trigger vuex-easy-firestore action 185 | store._modulesNamespaceMap['info/user/'] = {state: {_conf: {statePropName: ''}}} 186 | store._actions['info/user/set'] = true 187 | // 1. end * 188 | path = 'info/user/favColours.primary.*' 189 | // a) proper id 190 | payload = {red: true} 191 | res = formatSetter(path, payload, store, conf) 192 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {favColours: {primary: {red: true}}}}) 193 | path = 'info/user/favColours.primary.*' 194 | // b) flat id 195 | payload = {id: 'red', is: true} 196 | res = formatSetter(path, payload, store, conf) 197 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {favColours: {primary: {red: {id: 'red', is: true}}}}}) 198 | store.state = {info: {user: {favColours: {primary: {visible: false}}}}} 199 | store._modulesNamespaceMap['info/user/'].state.favColours = {primary: {visible: false}} 200 | // 2. mid-way * 201 | path = 'info/user/favColours.*.visible' 202 | payload = ['primary', true] 203 | res = formatSetter(path, payload, store, conf) 204 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {favColours: {primary: {visible: true}}}}) 205 | // 3. mid-way * and end * 206 | path = 'info/user/favColours.*.visible.*' 207 | payload = ['primary', {red: true}] 208 | res = formatSetter(path, payload, store, conf) 209 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {favColours: {primary: {visible: {red: true}}}}}) 210 | // 4. statePropName 211 | store._modulesNamespaceMap['info/user/'].state._conf.statePropName = 'favColours' 212 | path = 'info/user/favColours.*.visible.deeper.*' 213 | payload = ['primary', {red: true}] 214 | res = formatSetter(path, payload, store, conf) 215 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {primary: {visible: {deeper: {red: true}}}}}) 216 | }) 217 | 218 | test('[vuex-easy-firestore] has statePropName', t => { 219 | let res, path, payload, store, conf 220 | store = getStore() 221 | conf = getConf() 222 | conf.vuexEasyFirestore = true 223 | // trigger vuex-easy-firestore action 224 | path = 'info/user/favColours.primary' 225 | store._modulesNamespaceMap['info/user/'] = {state: {_conf: {statePropName: 'favColours'}}} 226 | store._actions['info/user/set'] = true 227 | payload = 'red' 228 | res = formatSetter(path, payload, store, conf) 229 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {primary: 'red'}}) 230 | 231 | // 1. end * 232 | path = 'info/user/favColours.primary.*' 233 | // a) proper id 234 | payload = {red: true} 235 | res = formatSetter(path, payload, store, conf) 236 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {primary: {red: true}}}) 237 | path = 'info/user/favColours.primary.*' 238 | // b) flat id 239 | payload = {id: 'red', is: true} 240 | res = formatSetter(path, payload, store, conf) 241 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {primary: {red: {id: 'red', is: true}}}}) 242 | 243 | // 1.B end * with less long path 244 | store._modulesNamespaceMap['pokemonBox/'] = {state: {_conf: {statePropName: 'pokemon'}}} 245 | store._actions['pokemonBox/set'] = true 246 | path = 'pokemonBox/pokemon.*' 247 | // a) proper id 248 | payload = {'001': {name: 'bulba'}} 249 | res = formatSetter(path, payload, store, conf) 250 | t.deepEqual(res, {command: 'dispatch', _path: 'pokemonBox/set', _payload: {'001': {name: 'bulba'}}}) 251 | path = 'pokemonBox/pokemon.*' 252 | // b) flat id 253 | payload = {id: '001', name: 'bulba'} 254 | res = formatSetter(path, payload, store, conf) 255 | t.deepEqual(res, {command: 'dispatch', _path: 'pokemonBox/set', _payload: {id: '001', name: 'bulba'}}) 256 | 257 | // setup for 2+ 258 | store.state = {info: {user: {favColours: {primary: {visible: false}}}}} 259 | store._modulesNamespaceMap['info/user/'].state.favColours = {primary: {visible: false}} 260 | // 2. mid-way * 261 | path = 'info/user/favColours.*.visible' 262 | payload = ['primary', true] 263 | res = formatSetter(path, payload, store, conf) 264 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {primary: {visible: true}}}) 265 | 266 | // 3. mid-way * and end * 267 | path = 'info/user/favColours.*.visible.*' 268 | payload = ['primary', {red: true}] 269 | res = formatSetter(path, payload, store, conf) 270 | t.deepEqual(res, {command: 'dispatch', _path: 'info/user/set', _payload: {primary: {visible: {red: true}}}}) 271 | 272 | // await store.set('pokemonBox/pokemon.*.nested.a.met.de', [id, 'ebe']) 273 | // await store.dispatch('pokemonBox/set', {id, nested: {a: {met: {de: 'ebe'}}}}) 274 | // await store.set('pokemonBox/pokemon.*.nested.a.met.*', [id, {de: 'ebe'}]) 275 | // await store.dispatch('pokemonBox/set', {id, nested: {a: {met: {de: 'ebe'}}}}) 276 | }) 277 | -------------------------------------------------------------------------------- /src/makeSetters.ts: -------------------------------------------------------------------------------- 1 | import { fillinPathWildcards, createObjectFromPath, getKeysFromPath, getIdsFromPayload, getDeepRef, pushDeepValue, popDeepValue, shiftDeepValue, spliceDeepValue } from './pathUtils' 2 | import { isObject, isArray } from 'is-what' 3 | import defaultConf from './defaultConfig' 4 | import { IDefaultConfig, IInitialisedStore, AnyObject } from './declarations' 5 | import error from './errors' 6 | 7 | /** 8 | * Creates a setter function in the store to set any state value 9 | * Usage: 10 | * `set('module/path/path.to.prop', newValue)` 11 | * it will check first for existence of: `dispatch('module/path/path.to.prop')` 12 | * if non existant it will execute: `commit('module/path/path.to.prop', newValue)` 13 | * Import method: 14 | * `store.set = (path, payload) => { return defaultSetter(path, payload, store, conf) }` 15 | * 16 | * @param {string} path the path of the prop to set eg. 'info/user/favColours.primary' 17 | * @param {*} payload the payload to set the prop to 18 | * @param {IInitialisedStore} store the store to attach 19 | * @param {IDefaultConfig} [conf={}] user config 20 | * @returns {*} the dispatch or commit function 21 | */ 22 | export function defaultSetter ( 23 | path: string, 24 | payload: any, 25 | store: IInitialisedStore, 26 | conf: IDefaultConfig = {} 27 | ): any { 28 | const {command, _path, _payload} = formatSetter(path, payload, store, conf) 29 | if (command === 'error') return _payload 30 | return store[command](_path, _payload) 31 | } 32 | 33 | export function formatSetter ( 34 | path: string, 35 | payload: any, 36 | store: IInitialisedStore, 37 | conf: IDefaultConfig = {} 38 | ): { 39 | command: string, 40 | _path?: string, 41 | _payload?: any 42 | } { 43 | const dConf: IDefaultConfig = Object.assign({}, defaultConf, conf) 44 | const pArr = path.split('/') // ['info', 'user', 'favColours.primary'] 45 | const props = pArr.pop() // 'favColours.primary' 46 | const modulePath = (pArr.length) 47 | ? pArr.join('/') + '/' // 'info/user/' 48 | : '' 49 | const setProp = (dConf.pattern === 'traditional') 50 | ? 'set' + props[0].toUpperCase() + props.substring(1) // 'setFavColours.primary' 51 | : props // 'favColours.primary' 52 | // Check if an action exists, if it does, trigger that and return early! 53 | const moduleSetProp = modulePath + setProp 54 | const actionExists = store._actions[moduleSetProp] 55 | if (actionExists) { 56 | return {command: 'dispatch', _path: moduleSetProp, _payload: payload} 57 | } 58 | // [vuex-easy-firestore] check if it's a firestore module 59 | const fsModulePath = (!modulePath && props && !props.includes('.') && dConf.vuexEasyFirestore) 60 | ? props + '/' 61 | : modulePath 62 | const _module = store._modulesNamespaceMap[fsModulePath] 63 | const fsConf = (!_module) ? null : _module.state._conf 64 | if (dConf.vuexEasyFirestore && fsConf) { 65 | // 'info/user/set', {favColours: {primary: payload}}' 66 | const firestoreActionPath = fsModulePath + 'set' 67 | const firestoreActionExists = store._actions[firestoreActionPath] 68 | if (firestoreActionExists) { 69 | const fsPropName = fsConf.statePropName 70 | const fsProps = (fsPropName && props.startsWith(`${fsPropName}.`)) 71 | ? props.replace(`${fsPropName}.`, '') 72 | : props 73 | const newPayload = (!fsProps || (!modulePath && fsProps && !fsProps.includes('.'))) 74 | ? payload 75 | : createObjectFromPath(fsProps, payload) 76 | return {command: 'dispatch', _path: firestoreActionPath, _payload: newPayload} 77 | } 78 | } 79 | // Trigger the mutation! 80 | const SET_PROP = (dConf.pattern === 'traditional') 81 | ? 'SET_' + props.toUpperCase() // 'SET_FAVCOLOURS.PRIMARY' 82 | : props // 'favColours.primary' 83 | const MODULES_SET_PROP = modulePath + SET_PROP 84 | const mutationExists = store._mutations[MODULES_SET_PROP] 85 | if (mutationExists) { 86 | return {command: 'commit', _path: MODULES_SET_PROP, _payload: payload} 87 | } 88 | const triggeredError = error('missingSetterMutation', dConf, MODULES_SET_PROP, props) 89 | return {command: 'error', _payload: triggeredError} 90 | } 91 | 92 | /** 93 | * Creates a delete function in the store to delete any prop from a value 94 | * Usage: 95 | * `delete('module/path/path.to.prop', id)` will delete prop[id] 96 | * `delete('module/path/path.to.prop.*', {id})` will delete prop[id] 97 | * it will check first for existence of: `dispatch('module/path/-path.to.prop')` or `dispatch('module/path/-path.to.prop.*')` 98 | * if non existant it will execute: `commit('module/path/-path.to.prop')` or `commit('module/path/-path.to.prop.*')` 99 | * Import method: 100 | * `store.delete = (path, payload) => { return defaultDeletor(path, payload, store, conf) }` 101 | * 102 | * @param {string} path the path of the prop to delete eg. 'info/user/favColours.primary' 103 | * @param {*} payload either nothing or an id or {id} 104 | * @param {IInitialisedStore} store the store to attach 105 | * @param {IDefaultConfig} [conf={}] user config 106 | * @returns {*} dispatch or commit 107 | */ 108 | export function defaultDeletor ( 109 | path: string, 110 | payload: any, 111 | store: IInitialisedStore, 112 | conf: IDefaultConfig = {} 113 | ): any { 114 | const {command, _path, _payload} = formatDeletor(path, payload, store, conf) 115 | if (command === 'error') return _payload 116 | return store[command](_path, _payload) 117 | } 118 | 119 | export function formatDeletor ( 120 | path: string, 121 | payload: any, 122 | store: IInitialisedStore, 123 | conf: IDefaultConfig = {} 124 | ): { 125 | command: string, 126 | _path?: string, 127 | _payload?: any 128 | } { 129 | const dConf: IDefaultConfig = Object.assign({}, defaultConf, conf) // 'user/items.*.tags.*' 130 | const pArr = path.split('/') // ['user', 'items.*.tags.*'] 131 | const props = pArr.pop() // 'items.*.tags.*' 132 | const modulePath = (pArr.length) 133 | ? pArr.join('/') + '/' // 'user/' 134 | : '' 135 | const deleteProp = (dConf.pattern === 'traditional') 136 | ? 'delete' + props[0].toUpperCase() + props.substring(1) // 'deleteItems.*.tags.*' 137 | : '-' + props // '-items.*.tags.*' 138 | // Check if an action exists, if it does, trigger that and return early! 139 | const moduleDeleteProp = modulePath + deleteProp 140 | const actionExists = store._actions[moduleDeleteProp] 141 | if (actionExists) { 142 | return {command: 'dispatch', _path: moduleDeleteProp, _payload: payload} 143 | } 144 | // [vuex-easy-firestore] check if it's a firestore module 145 | const _module = store._modulesNamespaceMap[modulePath] 146 | const fsConf = (!_module) ? null : _module.state._conf 147 | if (dConf.vuexEasyFirestore && fsConf) { 148 | // DOC: 'user/favColours.*', 'primary' 149 | // COLLECTION: 'items.*', '123' 150 | // COLLECTION: 'items.*.tags.*', ['123', 'dark'] 151 | const fsPropName = fsConf.statePropName 152 | const fsProps = (fsPropName && props.startsWith(`${fsPropName}.`)) 153 | ? props.replace(`${fsPropName}.`, '') 154 | : props 155 | let newPath = fsProps 156 | if (fsProps.includes('*')) { 157 | const idsPayload = (!isArray(payload)) ? [payload] : payload 158 | const ids = getIdsFromPayload(idsPayload) 159 | newPath = fillinPathWildcards(ids, fsProps) 160 | } 161 | if (newPath) return {command: 'dispatch', _path: modulePath + 'delete', _payload: newPath} 162 | } 163 | // Trigger the mutation! 164 | const DELETE_PROP = (dConf.pattern === 'traditional') 165 | ? 'DELETE_' + props.toUpperCase() // 'DELETE_ITEMS.*.TAGS.*' 166 | : '-' + props // '-items.*.tags.*' 167 | const MODULE_DELETE_PROP = modulePath + DELETE_PROP 168 | const mutationExists = store._mutations[MODULE_DELETE_PROP] 169 | if (mutationExists) { 170 | return {command: 'commit', _path: MODULE_DELETE_PROP, _payload: payload} 171 | } 172 | const triggeredError = error('missingDeleteMutation', dConf, MODULE_DELETE_PROP, props) 173 | return {command: 'error', _payload: triggeredError} 174 | } 175 | 176 | /** 177 | * Creates a special 'setter-module' to be registered as a child of a module. This 'setter-module' will have the 'set' namespace (by default) and have one setter action per state prop in the parent module. The setter action's name will be the state prop name. 178 | * 179 | * @param {AnyObject} targetState parent module's state object 180 | * @param {string} [moduleNS=''] parent module's namespace, must end in '/' 181 | * @param {IInitialisedStore} store vuex store 182 | * @param {IDefaultConfig} conf user config 183 | * @returns {*} a special 'setter-module' to be registered as child of target module. 184 | */ 185 | function createSetterModule ( 186 | targetState: AnyObject, 187 | moduleNS: string = '', 188 | store: IInitialisedStore, 189 | conf: IDefaultConfig 190 | ): any { 191 | function getSetters (_targetState, _propPath = '') { 192 | return Object.keys(_targetState).reduce((carry, stateProp) => { 193 | // Get the path info up until this point 194 | const PROP_SUBPROP = (_propPath) 195 | ? _propPath + '.' + stateProp 196 | : stateProp 197 | const MODULE_PROP_SUBPROP = moduleNS + PROP_SUBPROP 198 | // Avoid making setters for private props 199 | if (conf.ignorePrivateProps && stateProp[0] === '_') return carry 200 | if (conf.ignoreProps.includes(MODULE_PROP_SUBPROP)) return carry 201 | // Avoid making setters for props which are an entire module on its own 202 | if (store._modulesNamespaceMap[MODULE_PROP_SUBPROP + '/']) return carry 203 | // =================================================> 204 | // NORMAL SETTER 205 | // =================================================> 206 | // All good, make the action! 207 | carry[PROP_SUBPROP] = (context, payload) => { 208 | return defaultSetter(MODULE_PROP_SUBPROP, payload, store, conf) 209 | } 210 | // Get the value of the prop 211 | const propValue = _targetState[stateProp] 212 | // =================================================> 213 | // ARRAY SETTERS 214 | // =================================================> 215 | if (isArray(propValue)) { 216 | carry[PROP_SUBPROP + '.push'] = (context, payload) => { 217 | return defaultSetter(MODULE_PROP_SUBPROP + '.push', payload, store, conf) 218 | } 219 | carry[PROP_SUBPROP + '.pop'] = (context, payload) => { 220 | return defaultSetter(MODULE_PROP_SUBPROP + '.pop', payload, store, conf) 221 | } 222 | carry[PROP_SUBPROP + '.shift'] = (context, payload) => { 223 | return defaultSetter(MODULE_PROP_SUBPROP + '.shift', payload, store, conf) 224 | } 225 | carry[PROP_SUBPROP + '.splice'] = (context, payload) => { 226 | return defaultSetter(MODULE_PROP_SUBPROP + '.splice', payload, store, conf) 227 | } 228 | } 229 | // =================================================> 230 | // WILDCARDS SETTER 231 | // =================================================> 232 | // if (isObject(propValue) && !Object.keys(propValue).length) { 233 | // carry[PROP_SUBPROP + '.*'] = (context, payload) => { 234 | // return defaultSetter(MODULE_PROP_SUBPROP + '.*', payload, store, conf) 235 | // } 236 | // } 237 | // =================================================> 238 | // CHILDREN SETTERS 239 | // =================================================> 240 | // let's do it's children as well! 241 | if (isObject(propValue) && Object.keys(propValue).length) { 242 | const childrenSetters = getSetters(propValue, PROP_SUBPROP) 243 | Object.assign(carry, childrenSetters) 244 | } 245 | return carry 246 | }, {}) 247 | } 248 | const setters = getSetters(targetState) 249 | return {actions: setters, namespaced: true} 250 | } 251 | 252 | /** 253 | * Creates a special 'delete-module' to be registered as a child of a module. This 'delete-module' will have the 'delete' namespace (by default) and have one delete action per state prop in the parent module which holds an empty object. The delete action's name will be the state prop name + `.*`. 254 | * 255 | * @param {AnyObject} targetState parent module's state object 256 | * @param {string} [moduleNS=''] parent module's namespace, must end in '/' 257 | * @param {IInitialisedStore} store vuex store 258 | * @param {IDefaultConfig} conf user config 259 | * @returns {*} a special 'delete-module' to be registered as child of target module. 260 | */ 261 | function createDeleteModule ( 262 | targetState: AnyObject, 263 | moduleNS: string = '', 264 | store: IInitialisedStore, 265 | conf: IDefaultConfig 266 | ): any { 267 | function getDeletors (_targetState, _propPath = '') { 268 | return Object.keys(_targetState).reduce((carry, stateProp) => { 269 | // Get the path info up until this point 270 | const PROP_SUBPROP = (_propPath) 271 | ? _propPath + '.' + stateProp 272 | : stateProp 273 | const MODULE_PROP_SUBPROP = moduleNS + PROP_SUBPROP 274 | // Avoid making deletor for private props 275 | if (conf.ignorePrivateProps && stateProp[0] === '_') return carry 276 | if (conf.ignoreProps.includes(MODULE_PROP_SUBPROP)) return carry 277 | // Avoid making deletor for props which are an entire module on its own 278 | if (store._modulesNamespaceMap[MODULE_PROP_SUBPROP + '/']) return carry 279 | // Get the value of the prop 280 | const propValue = _targetState[stateProp] 281 | carry[PROP_SUBPROP] = (context, payload) => { 282 | return defaultDeletor(MODULE_PROP_SUBPROP, payload, store, conf) 283 | } 284 | // let's do it's children as well! 285 | if (isObject(propValue) && Object.keys(propValue).length) { 286 | const childrenDeletors = getDeletors(propValue, PROP_SUBPROP) 287 | Object.assign(carry, childrenDeletors) 288 | } 289 | return carry 290 | }, {}) 291 | } 292 | const deletors = getDeletors(targetState) 293 | return {actions: deletors, namespaced: true} 294 | } 295 | 296 | /** 297 | * Generate all vuex-easy-access modules: `/set/` and `/delete/` for each module 298 | * 299 | * @export 300 | * @param {IInitialisedStore} store 301 | * @param {IDefaultConfig} [conf={}] 302 | */ 303 | export function generateSetterModules ( 304 | store: IInitialisedStore, 305 | conf: IDefaultConfig = {} 306 | ): void { 307 | const modules = store._modulesNamespaceMap 308 | const dConf: IDefaultConfig = Object.assign({}, defaultConf, conf) 309 | Object.keys(modules).forEach(moduleNS => { 310 | const _module = modules[moduleNS] 311 | const moduleName = getKeysFromPath(moduleNS + dConf.setter) 312 | const setterModule = createSetterModule(_module.state, moduleNS, store, dConf) 313 | store.registerModule(moduleName, setterModule) 314 | const deleteModule = createDeleteModule(_module.state, moduleNS, store, dConf) 315 | const deleteModuleName = getKeysFromPath(moduleNS + dConf.deletor) 316 | store.registerModule(deleteModuleName, deleteModule) 317 | }) 318 | const rootModuleName = dConf.setter 319 | const rootSetterModule = createSetterModule(store.state, '', store, dConf) 320 | store.registerModule(rootModuleName, rootSetterModule) 321 | const rootDeleteModuleName = dConf.deletor 322 | const rootDeleteModule = createDeleteModule(store.state, '', store, dConf) 323 | store.registerModule(rootDeleteModuleName, rootDeleteModule) 324 | } 325 | -------------------------------------------------------------------------------- /test/actions.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import store from './helpers/index.cjs.js' 3 | 4 | test('hooks', async t => { 5 | const modulePath = 'user/' 6 | const prop = 'wallet' 7 | const path = modulePath + prop 8 | let res 9 | t.deepEqual(store.get(path), []) 10 | res = await store.set(path, 1) 11 | t.is(res, '1!') 12 | }) 13 | 14 | test('setters', t => { 15 | const path = 'locationJournal/gymData/defeated.palletTown' 16 | const a = store.get(path) 17 | store.set(path, 1) 18 | const b = store.get(path) 19 | t.is(b, 1) 20 | store.dispatch('locationJournal/gymData/set/defeated.palletTown', 2) 21 | const c = store.get(path) 22 | t.is(c, 2) 23 | store.commit(path, 3) 24 | const d = store.get(path) 25 | t.is(d, 3) 26 | t.is(store.state.locationJournal.gymData.defeated.palletTown, 3) 27 | }) 28 | 29 | test('delete props', t => { 30 | let target 31 | // commit 32 | target = store.get('propToBeDeleted_commit') 33 | t.is(target, true) 34 | t.is(store.state.propToBeDeleted_commit, true) 35 | store.commit('-propToBeDeleted_commit') 36 | t.is(store.state.propToBeDeleted_commit, undefined) 37 | 38 | // dispatch 39 | target = store.get('propToBeDeleted_dispatch') 40 | t.is(target, true) 41 | t.is(store.state.propToBeDeleted_dispatch, true) 42 | store.dispatch('delete/propToBeDeleted_dispatch') 43 | t.is(store.state.propToBeDeleted_dispatch, undefined) 44 | 45 | // delete() 46 | target = store.get('propToBeDeleted_delete') 47 | t.is(target, true) 48 | t.is(store.state.propToBeDeleted_delete, true) 49 | store.delete('propToBeDeleted_delete') 50 | t.is(store.state.propToBeDeleted_delete, undefined) 51 | 52 | // submodules 53 | target = store.get('dex/propToBeDeleted') 54 | t.is(target, true) 55 | t.is(store.state.dex.propToBeDeleted, true) 56 | store.delete('dex/propToBeDeleted') 57 | t.is(store.state.dex.propToBeDeleted, undefined) 58 | }) 59 | 60 | test('arraySetters', t => { 61 | const box = store.state.pokemonBox.waterPokemon 62 | // box starts as ['squirtle'] 63 | t.deepEqual(box, ['squirtle']) 64 | 65 | // MUTATIONS 66 | // push 67 | store.commit('pokemonBox.waterPokemon.push', 'charmander') 68 | t.deepEqual(box, ['squirtle', 'charmander']) 69 | // pop 70 | store.commit('pokemonBox.waterPokemon.pop') 71 | t.deepEqual(box, ['squirtle']) 72 | // shift 73 | store.commit('pokemonBox.waterPokemon.shift') 74 | t.deepEqual(box, []) 75 | // splice 76 | store.commit('pokemonBox.waterPokemon.push', 'warturtle') 77 | store.commit('pokemonBox.waterPokemon.splice', [0, 0, 'blastoise']) 78 | t.deepEqual(box, ['blastoise', 'warturtle']) 79 | 80 | // ACTIONS 81 | // push 82 | store.dispatch('set/pokemonBox.waterPokemon.push', 'psyduck') 83 | store.dispatch('set/pokemonBox.waterPokemon.push', 'starmie') 84 | t.deepEqual(box, ['blastoise', 'warturtle', 'psyduck', 'starmie']) 85 | // splice 86 | store.dispatch('set/pokemonBox.waterPokemon.splice', [1, 1, 'poliwag']) 87 | t.deepEqual(box, ['blastoise', 'poliwag', 'psyduck', 'starmie']) 88 | // shift and pop 89 | store.dispatch('set/pokemonBox.waterPokemon.shift') 90 | store.dispatch('set/pokemonBox.waterPokemon.pop') 91 | t.deepEqual(box, ['poliwag', 'psyduck']) 92 | 93 | // SET() 94 | // push 95 | store.set('pokemonBox.waterPokemon.push', 'charmander') 96 | t.deepEqual(box, ['poliwag', 'psyduck', 'charmander']) 97 | // pop 98 | store.set('pokemonBox.waterPokemon.pop') 99 | t.deepEqual(box, ['poliwag', 'psyduck']) 100 | // shift 101 | store.set('pokemonBox.waterPokemon.shift') 102 | t.deepEqual(box, ['psyduck']) 103 | // splice 104 | store.set('pokemonBox.waterPokemon.push', 'warturtle') 105 | t.deepEqual(box, ['psyduck', 'warturtle']) 106 | store.set('pokemonBox.waterPokemon.splice', [0, 0, 'blastoise']) 107 | t.deepEqual(box, ['blastoise', 'psyduck', 'warturtle']) 108 | 109 | // SUB MODULES 110 | store.dispatch('dex/set/pokemonById.*', {'151': {name: 'mew', powerUps: []}}) 111 | t.is(store.state.dex.pokemonById['151'].name, 'mew') 112 | t.deepEqual(store.state.dex.pokemonById['151'].powerUps, []) 113 | store.dispatch('dex/set/pokemonById.*.powerUps.push', ['151', 'pw1']) 114 | t.deepEqual(store.state.dex.pokemonById['151'].powerUps, ['pw1']) 115 | }) 116 | 117 | test('Wildcard setters 1 (generated by \'*\')', t => { 118 | const _celurean = {id: 'celurean city', visited: true, gym: true} 119 | const _1 = {id: '1', visited: false, gym: true} 120 | const _2 = {id: '2', visited: true, gym: false} 121 | const _3 = {id: '3', visited: false, gym: false} 122 | const _4 = {id: '4', gym: true} 123 | const _5 = {id: '5', visited: true} 124 | const _6 = {id: '6'} 125 | const _7 = {id: '7', visited: true, gym: true} 126 | const _8 = {id: '8', visited: true, gym: true} 127 | const _9 = {id: '9', visited: true, gym: true} 128 | const _10 = {id: '10', visited: true, gym: true} 129 | const _11 = {id: '11', visited: true, gym: true} 130 | let places = store.state.locationJournal.visitedPlaces 131 | const initialLength = Object.keys(places).length 132 | t.is(Object.keys(places).length, 0 + initialLength) 133 | 134 | // add 135 | store.commit('locationJournal/visitedPlaces.*', _celurean) 136 | store.dispatch('locationJournal/set/visitedPlaces.*', _4) 137 | store.set('locationJournal/visitedPlaces.*', _6) 138 | t.is(Object.keys(places).length, 3 + initialLength) 139 | t.truthy(places[_celurean.id]) 140 | t.is(places[_celurean.id].gym, _celurean.gym) 141 | t.truthy(places[_4.id]) 142 | t.is(places[_4.id].gym, _4.gym) 143 | t.truthy(places[_6.id]) 144 | t.true(places[_6.id].gym !== undefined) 145 | t.true(places[_6.id].visited !== undefined) 146 | 147 | // set sub props 148 | store.set('locationJournal/visitedPlaces.*', _1) 149 | t.is(Object.keys(places).length, 4 + initialLength) 150 | // sub props: commit 151 | t.truthy(places[_1.id]) 152 | t.is(places[_1.id].visited, _1.visited) 153 | t.is(places[_1.id].gym, _1.gym) 154 | const bef = Object.keys(places[_1.id]) 155 | store.commit('locationJournal/visitedPlaces.*', {[_1.id]: {visited: 'booyah!'}}) 156 | store.commit('locationJournal/visitedPlaces.*.gym', [_1.id, 'booyah!']) 157 | t.is(bef.length, Object.keys(places[_1.id]).length) 158 | t.is(places[_1.id].visited, 'booyah!') 159 | t.is(places[_1.id].gym, 'booyah!') 160 | // sub props: dispatch 161 | t.is(places[_4.id].visited, false) 162 | t.is(places[_4.id].gym, true) 163 | store.dispatch('locationJournal/set/visitedPlaces.*', {id: _4.id, visited: 'booyah!'}) 164 | store.dispatch('locationJournal/set/visitedPlaces.*.gym', [_4.id, 'booyah!']) 165 | t.is(places[_4.id].visited, 'booyah!') 166 | t.is(places[_4.id].gym, 'booyah!') 167 | // sub props: set 168 | store.set('locationJournal/visitedPlaces.*', {id: _6.id, visited: 'booyah!'}) 169 | store.set('locationJournal/visitedPlaces.*.gym', [_6.id, 'booyah!']) 170 | t.is(places[_6.id].visited, 'booyah!') 171 | t.is(places[_6.id].gym, 'booyah!') 172 | 173 | // Set props not part of the item 174 | // store.commit('locationJournal/visitedPlaces.*.new', {id: _1.id, val: 'new prop'}) 175 | // store.dispatch('locationJournal/set/visitedPlaces.*.new', {id: _4.id, val: 'new prop'}) 176 | // store.set('locationJournal/visitedPlaces.*.new', {id: _6.id, val: 'new prop'}) 177 | // t.is(places[_1.id].new, 'new prop') 178 | // t.is(places[_4.id].new, 'new prop') 179 | // t.is(places[_6.id].new, 'new prop') 180 | 181 | // sub props: with spaces in id 182 | store.commit('locationJournal/visitedPlaces.*', {id: _celurean.id, visited: 'booyah!'}) 183 | store.commit('locationJournal/visitedPlaces.*.gym', [_celurean.id, 'booyah!']) 184 | t.is(places[_celurean.id].visited, 'booyah!') 185 | t.is(places[_celurean.id].gym, 'booyah!') 186 | 187 | // delete 188 | store.delete('locationJournal/visitedPlaces.*', _celurean.id) 189 | store.commit('locationJournal/-visitedPlaces.*', _6.id) 190 | store.dispatch('locationJournal/delete/visitedPlaces.*', _1.id) 191 | t.is(Object.keys(places).length, 1 + initialLength) 192 | }) 193 | 194 | test('Wildcard setters 2 (generated by \'*\')', t => { 195 | // Add items 196 | store.set('dex/pokemonById.*', {id: '001', name: 'bulbasaur'}) 197 | store.commit('dex/pokemonById.*', {id: '004', name: 'charmander'}) 198 | store.dispatch('dex/set/pokemonById.*', {id: '007', name: 'squirtle'}) 199 | // add some more to test deletions 200 | store.set('dex/pokemonById.*', {id: '002', name: 'ivysaur'}) 201 | store.set('dex/pokemonById.*', {id: '003', name: 'venusaur'}) 202 | store.set('dex/pokemonById.*', {id: '005', name: 'charmeleon'}) 203 | store.set('dex/pokemonById.*', {id: '006', name: 'charizard'}) 204 | store.set('dex/pokemonById.*', {id: '008', name: 'warturtle'}) 205 | store.set('dex/pokemonById.*', {id: '009', name: 'blastoise'}) 206 | // check amount 207 | let dex = store.state.dex.pokemonById 208 | 209 | // make deletions 210 | // store.delete('dex/pokemonById', '002') // now ONLY * syntax allowed 211 | // t.falsy(dex['002']) 212 | store.delete('dex/pokemonById.*', '003') 213 | t.falsy(dex['003']) 214 | // store.commit('dex/-pokemonById', '005') // now ONLY * syntax allowed 215 | // t.falsy(dex['005']) 216 | store.commit('dex/-pokemonById.*', '006') 217 | t.falsy(dex['006']) 218 | // store.dispatch('dex/delete/pokemonById', '008') // now ONLY * syntax allowed 219 | // t.falsy(dex['008']) 220 | store.dispatch('dex/delete/pokemonById.*', '009') 221 | t.falsy(dex['009']) 222 | dex = store.state.dex.pokemonById 223 | 224 | // check if additions are still there 225 | t.truthy(dex['001']) 226 | t.truthy(dex['004']) 227 | t.truthy(dex['007']) 228 | t.is(dex['001'].name, 'bulbasaur') 229 | t.is(dex['004'].name, 'charmander') 230 | t.is(dex['007'].name, 'squirtle') 231 | }) 232 | 233 | test('Wildcard setters 3 (double wildcards)', t => { 234 | const _027 = {id: '027', name: 'Sandshrew'} 235 | const _043 = {id: '043', name: 'Oddish'} 236 | const _077 = {id: '_077_', name: 'Ponyta'} 237 | store.set('dex/pokemonById.*', _027) 238 | store.set('dex/pokemonById.*', {[_043.id]: _043}) 239 | store.set('dex/pokemonById.*', _077) 240 | const dex = store.state.dex.pokemonById 241 | t.is(dex[_027.id].name, 'Sandshrew') 242 | t.is(dex[_043.id].name, 'Oddish') 243 | t.is(dex[_077.id].name, 'Ponyta') 244 | store.set('dex/pokemonById.*.tags.*', [_027.id, {'ground': true}]) 245 | store.set('dex/pokemonById.*.tags.*', [_043.id, {'grass': true}]) 246 | store.set('dex/pokemonById.*.tags.*', [_077.id, {'fire': true}]) 247 | // store.set(`dex/pokemonById._007_.tags.*`, {fire: true}) 248 | const id = 'dark' 249 | store.set('dex/pokemonById.*.tags.*', [_027.id, {[id]: true}]) 250 | store.set('dex/pokemonById.*.tags.*', [_043.id, {[id]: true}]) 251 | store.set('dex/pokemonById.*.tags.*', [_077.id, {[id]: true}]) 252 | t.truthy(dex[_027.id].tags['dark']) 253 | t.truthy(dex[_043.id].tags['dark']) 254 | t.truthy(dex[_077.id].tags['dark']) 255 | t.is(dex[_027.id].tags['dark'], true) 256 | t.is(dex[_043.id].tags['dark'], true) 257 | t.is(dex[_077.id].tags['dark'], true) 258 | // deletes 259 | store.delete('dex/pokemonById.*.tags.*', [_027.id, 'dark']) 260 | store.dispatch('dex/delete/pokemonById.*.tags.*', [_043.id, 'dark']) 261 | store.commit('dex/-pokemonById.*.tags.*', [_077.id, 'dark']) 262 | t.is(dex[_027.id].name, 'Sandshrew') 263 | t.is(dex[_043.id].name, 'Oddish') 264 | t.is(dex[_077.id].name, 'Ponyta') 265 | t.truthy(dex[_027.id].tags['ground']) 266 | t.truthy(dex[_043.id].tags['grass']) 267 | t.truthy(dex[_077.id].tags['fire']) 268 | t.is(dex[_027.id].tags['ground'], true) 269 | t.is(dex[_043.id].tags['grass'], true) 270 | t.is(dex[_077.id].tags['fire'], true) 271 | t.falsy(dex[_027.id].tags['dark']) 272 | t.falsy(dex[_043.id].tags['dark']) 273 | t.falsy(dex[_077.id].tags['dark']) 274 | 275 | // push pop shift splice mutations 276 | store.commit('dex/pokemonById.*.powerUps.push', [_027.id, 'str1']) 277 | store.commit('dex/pokemonById.*.powerUps.push', [_027.id, 'res1']) 278 | t.deepEqual(dex[_027.id].powerUps, ['str1', 'res1']) 279 | store.commit('dex/pokemonById.*.powerUps.pop', _027.id) 280 | t.deepEqual(dex[_027.id].powerUps, ['str1']) 281 | store.commit('dex/pokemonById.*.powerUps.push', [_027.id, 'str2']) 282 | t.deepEqual(dex[_027.id].powerUps, ['str1', 'str2']) 283 | store.commit('dex/pokemonById.*.powerUps.shift', _027.id) 284 | t.deepEqual(dex[_027.id].powerUps, ['str2']) 285 | store.commit('dex/pokemonById.*.powerUps.splice', [_027.id, [0, 0, 'res3']]) 286 | t.deepEqual(dex[_027.id].powerUps, ['res3', 'str2']) 287 | 288 | // push pop shift splice actions 289 | store.dispatch('dex/set/pokemonById.*.powerUps.push', [_027.id, 'def1']) 290 | t.deepEqual(dex[_027.id].powerUps, ['res3', 'str2', 'def1']) 291 | store.dispatch('dex/set/pokemonById.*.powerUps.pop', _027.id) 292 | t.deepEqual(dex[_027.id].powerUps, ['res3', 'str2']) 293 | store.dispatch('dex/set/pokemonById.*.powerUps.shift', _027.id) 294 | t.deepEqual(dex[_027.id].powerUps, ['str2']) 295 | store.dispatch('dex/set/pokemonById.*.powerUps.splice', [_027.id, [0, 0, 'def2']]) 296 | t.deepEqual(dex[_027.id].powerUps, ['def2', 'str2']) 297 | store.dispatch('dex/set/pokemonById.*.powerUps.splice', [_027.id, [1, 0, 'def3']]) 298 | t.deepEqual(dex[_027.id].powerUps, ['def2', 'def3', 'str2']) 299 | store.dispatch('dex/set/pokemonById.*.powerUps.splice', [_027.id, [1, 1]]) 300 | t.deepEqual(dex[_027.id].powerUps, ['def2', 'str2']) 301 | store.dispatch('dex/set/pokemonById.*.powerUps.splice', [_027.id, [0, 1]]) 302 | t.deepEqual(dex[_027.id].powerUps, ['str2']) 303 | store.dispatch('dex/set/pokemonById.*.powerUps.splice', [_027.id, [0, 1]]) 304 | t.deepEqual(dex[_027.id].powerUps, []) 305 | }) 306 | 307 | test('errors', t => { 308 | let res 309 | res = store.set('user/name', 'Ash') 310 | t.is(res, 'missingSetterMutation') 311 | res = store.delete('user/name') 312 | t.is(res, 'missingDeleteMutation') 313 | // can't test because commit doesn't return anything: 314 | // res = store.commit('dex/-pokemonById.*') 315 | // t.is(res, 'mutationDeleteNoId') 316 | }) 317 | 318 | test('Set and delete wildcard props directly on state', t => { 319 | // Add items 320 | store.set('friendsList/*', {id: '001', name: 'bulbasaur'}) 321 | store.commit('friendsList/*', {id: '004', name: 'charmander'}) 322 | store.dispatch('friendsList/set/*', {id: '007', name: 'squirtle'}) 323 | // add some more to test deletions 324 | store.set('friendsList/*', {id: '002', name: 'ivysaur'}) 325 | store.set('friendsList/*', {id: '003', name: 'venusaur'}) 326 | store.set('friendsList/*', {id: '005', name: 'charmeleon'}) 327 | store.set('friendsList/*', {id: '006', name: 'charizard'}) 328 | store.set('friendsList/*', {id: '008', name: 'warturtle'}) 329 | store.set('friendsList/*', {id: '009', name: 'blastoise'}) 330 | // check amount 331 | const fL = store.state.friendsList 332 | 333 | // DELETE with wildcard in path 334 | store.delete('friendsList/*.name', ['003']) 335 | t.truthy(fL['003']) 336 | t.is(fL['003'].name, undefined) 337 | store.commit('friendsList/-*.name', ['006']) 338 | t.truthy(fL['006']) 339 | t.is(fL['006'].name, undefined) 340 | store.dispatch('friendsList/delete/*.name', ['009']) 341 | t.truthy(fL['009']) 342 | t.is(fL['009'].name, undefined) 343 | 344 | // fully DELETE wildcard item 345 | store.delete('friendsList/*', '003') 346 | t.is(fL['003'], undefined) 347 | store.commit('friendsList/-*', '006') 348 | t.is(fL['006'], undefined) 349 | store.dispatch('friendsList/delete/*', '009') 350 | t.is(fL['009'], undefined) 351 | 352 | // check if additions are still there 353 | t.truthy(fL['001']) 354 | t.truthy(fL['004']) 355 | t.truthy(fL['007']) 356 | t.is(fL['001'].name, 'bulbasaur') 357 | t.is(fL['004'].name, 'charmander') 358 | t.is(fL['007'].name, 'squirtle') 359 | 360 | store.set('friendsList/*', {id: '001', name: '!'}) 361 | store.commit('friendsList/*', {'004': {name: '?'}}) 362 | store.dispatch('friendsList/set/*', {id: '007', name: '><'}) 363 | t.is(fL['001'].name, '!') 364 | t.is(fL['004'].name, '?') 365 | t.is(fL['007'].name, '><') 366 | 367 | store.set('friendsList/*.tags.*', ['001', {guild: true}]) 368 | store.commit('friendsList/*.tags.*', ['004', {guild: true}]) 369 | store.dispatch('friendsList/set/*.tags.*', ['007', {guild: true}]) 370 | t.is(fL['001'].tags.guild, true) 371 | t.is(fL['004'].tags.guild, true) 372 | t.is(fL['007'].tags.guild, true) 373 | }) 374 | -------------------------------------------------------------------------------- /test/helpers/pathUtils.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | var isWhat = require('is-what'); 6 | 7 | var defaultConfig = { 8 | setter: 'set', 9 | getter: 'get', 10 | deletor: 'delete', 11 | vuexEasyFirestore: false, 12 | ignorePrivateProps: true, 13 | ignoreProps: [], 14 | pattern: 'simple' 15 | }; 16 | 17 | /* eslint-disable */ 18 | function getErrors(conf, path, props) { 19 | var originInfo = (path || props) ? "problem with prop: `" + props + "` at path: `" + path + "`" : ''; 20 | var tradPatt = (conf.pattern === 'traditional'); 21 | var setter = conf.setter; 22 | var deletor = conf.deletor; 23 | var prop = 'items'; 24 | var mutationNameSet = tradPatt ? 'SET_' + prop.toUpperCase() : prop; 25 | var mutationNameDel = tradPatt ? 'DELETE_' + prop.toUpperCase() : prop; 26 | var exampleSetters__Wildcard = "\n Correct usage examples:\n // From Vue-components:\n " + setter + "('" + prop + ".*', {'123': {name: 'the best item'}})\n\n // From the Vuex store:\n dispatch('" + setter + "/" + prop + ".*', {'123': {name: 'the best item'}})\n // or\n commit('" + mutationNameSet + ".*', {'123': {name: 'the best item'}})"; 27 | var exampleSetters__DoubleWildcard = "\n Correct usage examples:\n // From Vue-components:\n " + setter + "('" + prop + ".*.tags.*', ['123', {water: true}])\n\n // From the Vuex store:\n dispatch('" + setter + "/" + prop + ".*.tags.*', ['123', {water: true}])\n // or\n commit('" + mutationNameSet + ".*.tags.*', ['123', {water: true}])"; 28 | var exampleDeletor = "\n Correct usage examples:\n // From Vue-components:\n " + deletor + "('" + prop + ".*', '123')\n\n // From the Vuex store:\n dispatch('" + deletor + "/" + prop + ".*', '123')\n // or\n commit('" + mutationNameDel + ".*', '123')"; 29 | return { 30 | mutationSetterNoId: originInfo + "\n The payload needs to be an object with an `id` field.\n " + exampleSetters__Wildcard, 31 | mutationSetterPropPathWildcardMissingItemDoesntExist: originInfo + "\n The item does not exist! Make sure you first set the item.\n " + exampleSetters__Wildcard, 32 | mutationSetterPropPathWildcardIdCount: originInfo + "\n The amount of ids and wildcards `'*'` are not equal.\n If you have multiple wildcards you need to pass an array, where each item is an ID and the last is the property you want to set.\n " + exampleSetters__DoubleWildcard + "\n ", 33 | mutationDeleteNoId: originInfo + "\n The payload needs to be an object with an `id` field.\n " + exampleDeletor + "\n ", 34 | wildcardFormatWrong: originInfo + "\n There was something wrong with the payload passed when using a path with wildcards.\n\n A) Path with wildcard:\n " + exampleSetters__Wildcard + "\n\n B) Path with multiple wildcards:\n " + exampleSetters__DoubleWildcard + "\n ", 35 | missingDeleteMutation: "\n There is no mutation set for '" + path + "'.\n Something went wrong with your vuex-easy-access setup.\n Did you manually add `...defaultMutations(state)` to your modules?\n See the documentation here:\n https://github.com/mesqueeb/VuexEasyAccess#setup\n\n\n // You can also manually add a mutation like so in the correct module (not recommended!!):\n mutations: {\n '" + (tradPatt ? 'DELETE_' + props.toUpperCase() : '-' + props) + "': (state, payload) => {\n this._vm.$delete(state." + props + ")\n }\n }\n ", 36 | missingSetterMutation: "\n There is no mutation set for '" + path + "'.\n Something went wrong with your vuex-easy-access setup.\n Did you manually add `...defaultMutations(state)` to your modules?\n See the documentation here:\n https://github.com/mesqueeb/VuexEasyAccess#setup\n\n\n // You can also manually add a mutation like so in the correct module (not recommended!!):\n mutations: {\n '" + (tradPatt ? 'SET_' + props.toUpperCase() : props) + "': (state, payload) => {\n state." + props + " = payload\n }\n }\n ", 37 | }; 38 | } 39 | /** 40 | * Error logging 41 | * 42 | * @export 43 | * @param {string} error the error code 44 | * @param {object} conf the user config 45 | * @param {string} [path] (optional) the path the error occured in 46 | * @param {string} [props] (optional) the props the error occured with 47 | * @returns {string} the error code 48 | */ 49 | function error (error, conf, path, props) { 50 | if (conf === void 0) { conf = {}; } 51 | var mergedConf = Object.assign({}, defaultConfig, conf); 52 | var errorMessages = getErrors(mergedConf, path, props); 53 | console.error('[vuex-easy-access] Error!', errorMessages[error]); 54 | return error; 55 | } 56 | 57 | /** 58 | * gets an ID from a single piece of payload. 59 | * 60 | * @param {(object | ({ id: string } & object) | string)} payloadPiece 61 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 62 | * @param {string} [path] (optional - for error handling) the path called 63 | * @param {(any[] | object | string)} [fullPayload] (optional - for error handling) the full payload on which each was `getId()` called 64 | * @returns {string} the id 65 | */ 66 | function getId(payloadPiece, conf, path, fullPayload) { 67 | if (isWhat.isObject(payloadPiece)) { 68 | if ('id' in payloadPiece) 69 | return payloadPiece.id; 70 | if (Object.keys(payloadPiece).length === 1) 71 | return Object.keys(payloadPiece)[0]; 72 | } 73 | if (isWhat.isString(payloadPiece)) 74 | return payloadPiece; 75 | error('wildcardFormatWrong', conf, path); 76 | return ''; 77 | } 78 | /** 79 | * Get all ids from an array payload. 80 | * 81 | * @param {any[]} payload 82 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 83 | * @param {string} [path] (optional - for error handling) the path called 84 | * @returns {string[]} all ids 85 | */ 86 | function getIdsFromPayload(payload, conf, path) { 87 | return payload.map(function (payloadPiece) { return getId(payloadPiece, conf, path); }); 88 | } 89 | /** 90 | * Returns a value of a payload piece. Eg. {[id]: 'val'} will return 'val' 91 | * 92 | * @param {(object | string)} payloadPiece 93 | * @returns {any} 94 | */ 95 | function getValueFromPayloadPiece(payloadPiece) { 96 | if (isWhat.isObject(payloadPiece) && 97 | !('id' in payloadPiece) && 98 | Object.keys(payloadPiece).length === 1) { 99 | return Object.values(payloadPiece)[0]; 100 | } 101 | return payloadPiece; 102 | } 103 | /** 104 | * Checks the ratio between an array of IDs and a path with wildcards 105 | * 106 | * @param {string[]} ids 107 | * @param {string} path 108 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 109 | * @returns {boolean} true if no problem. false if the ratio is incorrect 110 | */ 111 | function checkIdWildcardRatio(ids, path, conf) { 112 | var match = path.match(/\*/g); 113 | var idCount = (isWhat.isArray(match)) 114 | ? match.length 115 | : 0; 116 | if (ids.length === idCount) 117 | return true; 118 | error('mutationSetterPropPathWildcardIdCount', conf); 119 | return false; 120 | } 121 | /** 122 | * Fill in IDs at '*' in a path, based on the IDs received. 123 | * 124 | * @param {string[]} ids 125 | * @param {string} path 'path.*.with.*.wildcards' 126 | * @param {object} [state] RELATIVE TO PATH START! the state to check if the value actually exists 127 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 128 | * @returns {string} The path with '*' replaced by IDs 129 | */ 130 | function fillinPathWildcards(ids, path, state, conf) { 131 | // Ignore pool check if '*' comes last 132 | var ignorePoolCheckOn = (path.endsWith('*')) ? ids[ids.length - 1] : null; 133 | ids.forEach(function (_id, _index, _array) { 134 | var idIndex = path.indexOf('*'); 135 | var pathUntilPool = path.substring(0, idIndex); 136 | // check for errors when both state and conf are passed 137 | // pathUntilPool can be '' in case the path starts with '*' 138 | if (ignorePoolCheckOn !== _id && state && conf) { 139 | var pool = (pathUntilPool) 140 | ? getDeepRef(state, pathUntilPool) 141 | : state; 142 | if (pool[_id] === undefined) 143 | return error('mutationSetterPropPathWildcardMissingItemDoesntExist', conf, pathUntilPool, _id); 144 | } 145 | path = path 146 | .split('') 147 | .map(function (char, ind) { return ind === idIndex ? _id : char; }) 148 | .join(''); 149 | }); 150 | return path; 151 | } 152 | /** 153 | * ('/sub.prop', payload) becomes → {sub: {prop: payload}} 154 | * ('sub', payload) becomes → {sub: payload} 155 | * 156 | * @param {string} path 'a/path/like.this' 157 | * @param {*} payload 158 | * @param {object} [state] the state to check if the value actually exists 159 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 160 | * @returns {AnyObject} a nested object re-created based on the path & payload 161 | */ 162 | function createObjectFromPath(path, payload, state, conf) { 163 | var _a; 164 | // edge cases 165 | if (path === '*') 166 | return payload; 167 | if (!path.includes('.') && !path.includes('/')) 168 | return _a = {}, _a[path] = payload, _a; 169 | // start 170 | var newValue = payload; 171 | if (path.includes('*')) { 172 | // only work with arrays 173 | if (!isWhat.isArray(payload)) 174 | payload = [payload]; 175 | var lastPayloadPiece = payload.pop(); 176 | var ids = payload; 177 | // CASE: 'dex/pokemonById.*.tags' 178 | if (!path.endsWith('*')) { 179 | newValue = lastPayloadPiece; 180 | } 181 | // CASE: 'dex/pokemonById.*.tags.*' 182 | if (path.endsWith('*')) { 183 | var lastId = getId(lastPayloadPiece, conf, path); 184 | ids.push(lastId); 185 | newValue = getValueFromPayloadPiece(lastPayloadPiece); 186 | if (isWhat.isObject(newValue)) 187 | newValue.id = lastId; 188 | } 189 | ids = ids.map(function (_id) { 190 | _id = _id.replace('.', '_____dot_____'); 191 | _id = _id.replace('/', '_____slash_____'); 192 | return _id; 193 | }); 194 | if (!checkIdWildcardRatio(ids, path, conf)) 195 | return; 196 | var pathWithIds = fillinPathWildcards(ids, path, state, conf); 197 | path = pathWithIds; 198 | } 199 | // important to set the result here and not return the reduce directly! 200 | var result = {}; 201 | path.match(/[^\/^\.]+/g) 202 | .reduce(function (carry, _prop, index, array) { 203 | _prop = _prop.replace('_____dot_____', '.'); 204 | _prop = _prop.replace('_____slash_____', '/'); 205 | var container = (index === array.length - 1) 206 | ? newValue 207 | : {}; 208 | carry[_prop] = container; 209 | return container; 210 | }, result); 211 | return result; 212 | } 213 | /** 214 | * Returns the keys of a path 215 | * 216 | * @param {string} path a/path/like.this 217 | * @returns {string[]} with keys 218 | */ 219 | function getKeysFromPath(path) { 220 | if (!path) 221 | return []; 222 | return path.match(/[^\/^\.]+/g); 223 | } 224 | /** 225 | * Gets a deep property in an object, based on a path to that property 226 | * 227 | * @param {object} target an object to wherefrom to retrieve the deep reference of 228 | * @param {string} path 'path/to.prop' 229 | * @returns {AnyObject} the last prop in the path 230 | */ 231 | function getDeepRef(target, path) { 232 | if (target === void 0) { target = {}; } 233 | var keys = getKeysFromPath(path); 234 | if (!keys.length) 235 | return target; 236 | var obj = target; 237 | while (obj && keys.length > 1) { 238 | obj = obj[keys.shift()]; 239 | } 240 | var key = keys.shift(); 241 | if (obj && obj.hasOwnProperty(key)) { 242 | return obj[key]; 243 | } 244 | } 245 | /** 246 | * Gets a deep property in an object, based on a path to that property 247 | * 248 | * @param {object} target the Object to get the value of 249 | * @param {string} path 'path/to/prop.subprop' 250 | * @returns {AnyObject} the property's value 251 | */ 252 | function getDeepValue(target, path) { 253 | return getDeepRef(target, path); 254 | } 255 | /** 256 | * Sets a value to a deep property in an object, based on a path to that property 257 | * 258 | * @param {object} target the Object to set the value on 259 | * @param {string} path 'path/to/prop.subprop' 260 | * @param {*} value the value to set 261 | * @returns {AnyObject} the original target object 262 | */ 263 | function setDeepValue(target, path, value) { 264 | var keys = getKeysFromPath(path); 265 | var lastKey = keys.pop(); 266 | var deepRef = getDeepRef(target, keys.join('.')); 267 | if (deepRef && deepRef.hasOwnProperty(lastKey)) { 268 | deepRef[lastKey] = value; 269 | } 270 | return target; 271 | } 272 | /** 273 | * Pushes a value in an array which is a deep property in an object, based on a path to that property 274 | * 275 | * @param {object} target the Object to push the value on 276 | * @param {string} path 'path/to.sub.prop' 277 | * @param {*} value the value to push 278 | * @returns {number} the new length of the array 279 | */ 280 | function pushDeepValue(target, path, value) { 281 | var deepRef = getDeepRef(target, path); 282 | if (!isWhat.isArray(deepRef)) 283 | return; 284 | return deepRef.push(value); 285 | } 286 | /** 287 | * Pops a value of an array which is a deep property in an object, based on a path to that property 288 | * 289 | * @param {object} target the Object to pop the value of 290 | * @param {string} path 'path.to.sub.prop' 291 | * @returns {*} the popped value 292 | */ 293 | function popDeepValue(target, path) { 294 | var deepRef = getDeepRef(target, path); 295 | if (!isWhat.isArray(deepRef)) 296 | return; 297 | return deepRef.pop(); 298 | } 299 | /** 300 | * Shift a value of an array which is a deep property in an object, based on a path to that property 301 | * 302 | * @param {object} target the Object to shift the value of 303 | * @param {string} path 'path.to.sub.prop' 304 | * @returns {*} the shifted value 305 | */ 306 | function shiftDeepValue(target, path) { 307 | var deepRef = getDeepRef(target, path); 308 | if (!isWhat.isArray(deepRef)) 309 | return; 310 | return deepRef.shift(); 311 | } 312 | /** 313 | * Splice into an array which is a deep property in an object, based on a path to that property 314 | * 315 | * @param {object} target the Object to splice the value of 316 | * @param {string} path 'path/to.sub.prop' 317 | * @param {number} [index=0] the index to splice in the value, defaults to 0 318 | * @param {number} [deleteCount=0] the amount of items to delete, defaults to 0 319 | * @param {*} value the value to splice in 320 | * @returns {any[]} an array containing the deleted elements 321 | */ 322 | function spliceDeepValue(target, path, index, deleteCount, value) { 323 | if (index === void 0) { index = 0; } 324 | if (deleteCount === void 0) { deleteCount = 0; } 325 | var deepRef = getDeepRef(target, path); 326 | if (!isWhat.isArray(deepRef)) 327 | return; 328 | if (value === undefined) 329 | return deepRef.splice(index, deleteCount); 330 | return deepRef.splice(index, deleteCount, value); 331 | } 332 | 333 | exports.checkIdWildcardRatio = checkIdWildcardRatio; 334 | exports.createObjectFromPath = createObjectFromPath; 335 | exports.fillinPathWildcards = fillinPathWildcards; 336 | exports.getDeepRef = getDeepRef; 337 | exports.getDeepValue = getDeepValue; 338 | exports.getIdsFromPayload = getIdsFromPayload; 339 | exports.getKeysFromPath = getKeysFromPath; 340 | exports.getValueFromPayloadPiece = getValueFromPayloadPiece; 341 | exports.popDeepValue = popDeepValue; 342 | exports.pushDeepValue = pushDeepValue; 343 | exports.setDeepValue = setDeepValue; 344 | exports.shiftDeepValue = shiftDeepValue; 345 | exports.spliceDeepValue = spliceDeepValue; 346 | -------------------------------------------------------------------------------- /test/helpers/makeSetters.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | var isWhat = require('is-what'); 6 | 7 | var defaultConf = { 8 | setter: 'set', 9 | getter: 'get', 10 | deletor: 'delete', 11 | vuexEasyFirestore: false, 12 | ignorePrivateProps: true, 13 | ignoreProps: [], 14 | pattern: 'simple' 15 | }; 16 | 17 | /* eslint-disable */ 18 | function getErrors(conf, path, props) { 19 | var originInfo = (path || props) ? "problem with prop: `" + props + "` at path: `" + path + "`" : ''; 20 | var tradPatt = (conf.pattern === 'traditional'); 21 | var setter = conf.setter; 22 | var deletor = conf.deletor; 23 | var prop = 'items'; 24 | var mutationNameSet = tradPatt ? 'SET_' + prop.toUpperCase() : prop; 25 | var mutationNameDel = tradPatt ? 'DELETE_' + prop.toUpperCase() : prop; 26 | var exampleSetters__Wildcard = "\n Correct usage examples:\n // From Vue-components:\n " + setter + "('" + prop + ".*', {'123': {name: 'the best item'}})\n\n // From the Vuex store:\n dispatch('" + setter + "/" + prop + ".*', {'123': {name: 'the best item'}})\n // or\n commit('" + mutationNameSet + ".*', {'123': {name: 'the best item'}})"; 27 | var exampleSetters__DoubleWildcard = "\n Correct usage examples:\n // From Vue-components:\n " + setter + "('" + prop + ".*.tags.*', ['123', {water: true}])\n\n // From the Vuex store:\n dispatch('" + setter + "/" + prop + ".*.tags.*', ['123', {water: true}])\n // or\n commit('" + mutationNameSet + ".*.tags.*', ['123', {water: true}])"; 28 | var exampleDeletor = "\n Correct usage examples:\n // From Vue-components:\n " + deletor + "('" + prop + ".*', '123')\n\n // From the Vuex store:\n dispatch('" + deletor + "/" + prop + ".*', '123')\n // or\n commit('" + mutationNameDel + ".*', '123')"; 29 | return { 30 | mutationSetterNoId: originInfo + "\n The payload needs to be an object with an `id` field.\n " + exampleSetters__Wildcard, 31 | mutationSetterPropPathWildcardMissingItemDoesntExist: originInfo + "\n The item does not exist! Make sure you first set the item.\n " + exampleSetters__Wildcard, 32 | mutationSetterPropPathWildcardIdCount: originInfo + "\n The amount of ids and wildcards `'*'` are not equal.\n If you have multiple wildcards you need to pass an array, where each item is an ID and the last is the property you want to set.\n " + exampleSetters__DoubleWildcard + "\n ", 33 | mutationDeleteNoId: originInfo + "\n The payload needs to be an object with an `id` field.\n " + exampleDeletor + "\n ", 34 | wildcardFormatWrong: originInfo + "\n There was something wrong with the payload passed when using a path with wildcards.\n\n A) Path with wildcard:\n " + exampleSetters__Wildcard + "\n\n B) Path with multiple wildcards:\n " + exampleSetters__DoubleWildcard + "\n ", 35 | missingDeleteMutation: "\n There is no mutation set for '" + path + "'.\n Something went wrong with your vuex-easy-access setup.\n Did you manually add `...defaultMutations(state)` to your modules?\n See the documentation here:\n https://github.com/mesqueeb/VuexEasyAccess#setup\n\n\n // You can also manually add a mutation like so in the correct module (not recommended!!):\n mutations: {\n '" + (tradPatt ? 'DELETE_' + props.toUpperCase() : '-' + props) + "': (state, payload) => {\n this._vm.$delete(state." + props + ")\n }\n }\n ", 36 | missingSetterMutation: "\n There is no mutation set for '" + path + "'.\n Something went wrong with your vuex-easy-access setup.\n Did you manually add `...defaultMutations(state)` to your modules?\n See the documentation here:\n https://github.com/mesqueeb/VuexEasyAccess#setup\n\n\n // You can also manually add a mutation like so in the correct module (not recommended!!):\n mutations: {\n '" + (tradPatt ? 'SET_' + props.toUpperCase() : props) + "': (state, payload) => {\n state." + props + " = payload\n }\n }\n ", 37 | }; 38 | } 39 | /** 40 | * Error logging 41 | * 42 | * @export 43 | * @param {string} error the error code 44 | * @param {object} conf the user config 45 | * @param {string} [path] (optional) the path the error occured in 46 | * @param {string} [props] (optional) the props the error occured with 47 | * @returns {string} the error code 48 | */ 49 | function error (error, conf, path, props) { 50 | if (conf === void 0) { conf = {}; } 51 | var mergedConf = Object.assign({}, defaultConf, conf); 52 | var errorMessages = getErrors(mergedConf, path, props); 53 | console.error('[vuex-easy-access] Error!', errorMessages[error]); 54 | return error; 55 | } 56 | 57 | /** 58 | * gets an ID from a single piece of payload. 59 | * 60 | * @param {(object | ({ id: string } & object) | string)} payloadPiece 61 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 62 | * @param {string} [path] (optional - for error handling) the path called 63 | * @param {(any[] | object | string)} [fullPayload] (optional - for error handling) the full payload on which each was `getId()` called 64 | * @returns {string} the id 65 | */ 66 | function getId(payloadPiece, conf, path, fullPayload) { 67 | if (isWhat.isObject(payloadPiece)) { 68 | if ('id' in payloadPiece) 69 | return payloadPiece.id; 70 | if (Object.keys(payloadPiece).length === 1) 71 | return Object.keys(payloadPiece)[0]; 72 | } 73 | if (isWhat.isString(payloadPiece)) 74 | return payloadPiece; 75 | error('wildcardFormatWrong', conf, path); 76 | return ''; 77 | } 78 | /** 79 | * Get all ids from an array payload. 80 | * 81 | * @param {any[]} payload 82 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 83 | * @param {string} [path] (optional - for error handling) the path called 84 | * @returns {string[]} all ids 85 | */ 86 | function getIdsFromPayload(payload, conf, path) { 87 | return payload.map(function (payloadPiece) { return getId(payloadPiece, conf, path); }); 88 | } 89 | /** 90 | * Returns a value of a payload piece. Eg. {[id]: 'val'} will return 'val' 91 | * 92 | * @param {(object | string)} payloadPiece 93 | * @returns {any} 94 | */ 95 | function getValueFromPayloadPiece(payloadPiece) { 96 | if (isWhat.isObject(payloadPiece) && 97 | !('id' in payloadPiece) && 98 | Object.keys(payloadPiece).length === 1) { 99 | return Object.values(payloadPiece)[0]; 100 | } 101 | return payloadPiece; 102 | } 103 | /** 104 | * Checks the ratio between an array of IDs and a path with wildcards 105 | * 106 | * @param {string[]} ids 107 | * @param {string} path 108 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 109 | * @returns {boolean} true if no problem. false if the ratio is incorrect 110 | */ 111 | function checkIdWildcardRatio(ids, path, conf) { 112 | var match = path.match(/\*/g); 113 | var idCount = (isWhat.isArray(match)) 114 | ? match.length 115 | : 0; 116 | if (ids.length === idCount) 117 | return true; 118 | error('mutationSetterPropPathWildcardIdCount', conf); 119 | return false; 120 | } 121 | /** 122 | * Fill in IDs at '*' in a path, based on the IDs received. 123 | * 124 | * @param {string[]} ids 125 | * @param {string} path 'path.*.with.*.wildcards' 126 | * @param {object} [state] RELATIVE TO PATH START! the state to check if the value actually exists 127 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 128 | * @returns {string} The path with '*' replaced by IDs 129 | */ 130 | function fillinPathWildcards(ids, path, state, conf) { 131 | // Ignore pool check if '*' comes last 132 | var ignorePoolCheckOn = (path.endsWith('*')) ? ids[ids.length - 1] : null; 133 | ids.forEach(function (_id, _index, _array) { 134 | var idIndex = path.indexOf('*'); 135 | var pathUntilPool = path.substring(0, idIndex); 136 | // check for errors when both state and conf are passed 137 | // pathUntilPool can be '' in case the path starts with '*' 138 | if (ignorePoolCheckOn !== _id && state && conf) { 139 | var pool = (pathUntilPool) 140 | ? getDeepRef(state, pathUntilPool) 141 | : state; 142 | if (pool[_id] === undefined) 143 | return error('mutationSetterPropPathWildcardMissingItemDoesntExist', conf, pathUntilPool, _id); 144 | } 145 | path = path 146 | .split('') 147 | .map(function (char, ind) { return ind === idIndex ? _id : char; }) 148 | .join(''); 149 | }); 150 | return path; 151 | } 152 | /** 153 | * ('/sub.prop', payload) becomes → {sub: {prop: payload}} 154 | * ('sub', payload) becomes → {sub: payload} 155 | * 156 | * @param {string} path 'a/path/like.this' 157 | * @param {*} payload 158 | * @param {object} [state] the state to check if the value actually exists 159 | * @param {object} [conf] (optional - for error handling) the vuex-easy-access config 160 | * @returns {AnyObject} a nested object re-created based on the path & payload 161 | */ 162 | function createObjectFromPath(path, payload, state, conf) { 163 | var _a; 164 | // edge cases 165 | if (path === '*') 166 | return payload; 167 | if (!path.includes('.') && !path.includes('/')) 168 | return _a = {}, _a[path] = payload, _a; 169 | // start 170 | var newValue = payload; 171 | if (path.includes('*')) { 172 | // only work with arrays 173 | if (!isWhat.isArray(payload)) 174 | payload = [payload]; 175 | var lastPayloadPiece = payload.pop(); 176 | var ids = payload; 177 | // CASE: 'dex/pokemonById.*.tags' 178 | if (!path.endsWith('*')) { 179 | newValue = lastPayloadPiece; 180 | } 181 | // CASE: 'dex/pokemonById.*.tags.*' 182 | if (path.endsWith('*')) { 183 | var lastId = getId(lastPayloadPiece, conf, path); 184 | ids.push(lastId); 185 | newValue = getValueFromPayloadPiece(lastPayloadPiece); 186 | if (isWhat.isObject(newValue)) 187 | newValue.id = lastId; 188 | } 189 | ids = ids.map(function (_id) { 190 | _id = _id.replace('.', '_____dot_____'); 191 | _id = _id.replace('/', '_____slash_____'); 192 | return _id; 193 | }); 194 | if (!checkIdWildcardRatio(ids, path, conf)) 195 | return; 196 | var pathWithIds = fillinPathWildcards(ids, path, state, conf); 197 | path = pathWithIds; 198 | } 199 | // important to set the result here and not return the reduce directly! 200 | var result = {}; 201 | path.match(/[^\/^\.]+/g) 202 | .reduce(function (carry, _prop, index, array) { 203 | _prop = _prop.replace('_____dot_____', '.'); 204 | _prop = _prop.replace('_____slash_____', '/'); 205 | var container = (index === array.length - 1) 206 | ? newValue 207 | : {}; 208 | carry[_prop] = container; 209 | return container; 210 | }, result); 211 | return result; 212 | } 213 | /** 214 | * Returns the keys of a path 215 | * 216 | * @param {string} path a/path/like.this 217 | * @returns {string[]} with keys 218 | */ 219 | function getKeysFromPath(path) { 220 | if (!path) 221 | return []; 222 | return path.match(/[^\/^\.]+/g); 223 | } 224 | /** 225 | * Gets a deep property in an object, based on a path to that property 226 | * 227 | * @param {object} target an object to wherefrom to retrieve the deep reference of 228 | * @param {string} path 'path/to.prop' 229 | * @returns {AnyObject} the last prop in the path 230 | */ 231 | function getDeepRef(target, path) { 232 | if (target === void 0) { target = {}; } 233 | var keys = getKeysFromPath(path); 234 | if (!keys.length) 235 | return target; 236 | var obj = target; 237 | while (obj && keys.length > 1) { 238 | obj = obj[keys.shift()]; 239 | } 240 | var key = keys.shift(); 241 | if (obj && obj.hasOwnProperty(key)) { 242 | return obj[key]; 243 | } 244 | } 245 | 246 | /** 247 | * Creates a setter function in the store to set any state value 248 | * Usage: 249 | * `set('module/path/path.to.prop', newValue)` 250 | * it will check first for existence of: `dispatch('module/path/path.to.prop')` 251 | * if non existant it will execute: `commit('module/path/path.to.prop', newValue)` 252 | * Import method: 253 | * `store.set = (path, payload) => { return defaultSetter(path, payload, store, conf) }` 254 | * 255 | * @param {string} path the path of the prop to set eg. 'info/user/favColours.primary' 256 | * @param {*} payload the payload to set the prop to 257 | * @param {IInitialisedStore} store the store to attach 258 | * @param {IDefaultConfig} [conf={}] user config 259 | * @returns {*} the dispatch or commit function 260 | */ 261 | function defaultSetter(path, payload, store, conf) { 262 | if (conf === void 0) { conf = {}; } 263 | var _a = formatSetter(path, payload, store, conf), command = _a.command, _path = _a._path, _payload = _a._payload; 264 | if (command === 'error') 265 | return _payload; 266 | return store[command](_path, _payload); 267 | } 268 | function formatSetter(path, payload, store, conf) { 269 | if (conf === void 0) { conf = {}; } 270 | var dConf = Object.assign({}, defaultConf, conf); 271 | var pArr = path.split('/'); // ['info', 'user', 'favColours.primary'] 272 | var props = pArr.pop(); // 'favColours.primary' 273 | var modulePath = (pArr.length) 274 | ? pArr.join('/') + '/' // 'info/user/' 275 | : ''; 276 | var setProp = (dConf.pattern === 'traditional') 277 | ? 'set' + props[0].toUpperCase() + props.substring(1) // 'setFavColours.primary' 278 | : props; // 'favColours.primary' 279 | // Check if an action exists, if it does, trigger that and return early! 280 | var moduleSetProp = modulePath + setProp; 281 | var actionExists = store._actions[moduleSetProp]; 282 | if (actionExists) { 283 | return { command: 'dispatch', _path: moduleSetProp, _payload: payload }; 284 | } 285 | // [vuex-easy-firestore] check if it's a firestore module 286 | var fsModulePath = (!modulePath && props && !props.includes('.') && dConf.vuexEasyFirestore) 287 | ? props + '/' 288 | : modulePath; 289 | var _module = store._modulesNamespaceMap[fsModulePath]; 290 | var fsConf = (!_module) ? null : _module.state._conf; 291 | if (dConf.vuexEasyFirestore && fsConf) { 292 | // 'info/user/set', {favColours: {primary: payload}}' 293 | var firestoreActionPath = fsModulePath + 'set'; 294 | var firestoreActionExists = store._actions[firestoreActionPath]; 295 | if (firestoreActionExists) { 296 | var fsPropName = fsConf.statePropName; 297 | var fsProps = (fsPropName && props.startsWith(fsPropName + ".")) 298 | ? props.replace(fsPropName + ".", '') 299 | : props; 300 | var newPayload = (!fsProps || (!modulePath && fsProps && !fsProps.includes('.'))) 301 | ? payload 302 | : createObjectFromPath(fsProps, payload); 303 | return { command: 'dispatch', _path: firestoreActionPath, _payload: newPayload }; 304 | } 305 | } 306 | // Trigger the mutation! 307 | var SET_PROP = (dConf.pattern === 'traditional') 308 | ? 'SET_' + props.toUpperCase() // 'SET_FAVCOLOURS.PRIMARY' 309 | : props; // 'favColours.primary' 310 | var MODULES_SET_PROP = modulePath + SET_PROP; 311 | var mutationExists = store._mutations[MODULES_SET_PROP]; 312 | if (mutationExists) { 313 | return { command: 'commit', _path: MODULES_SET_PROP, _payload: payload }; 314 | } 315 | var triggeredError = error('missingSetterMutation', dConf, MODULES_SET_PROP, props); 316 | return { command: 'error', _payload: triggeredError }; 317 | } 318 | /** 319 | * Creates a delete function in the store to delete any prop from a value 320 | * Usage: 321 | * `delete('module/path/path.to.prop', id)` will delete prop[id] 322 | * `delete('module/path/path.to.prop.*', {id})` will delete prop[id] 323 | * it will check first for existence of: `dispatch('module/path/-path.to.prop')` or `dispatch('module/path/-path.to.prop.*')` 324 | * if non existant it will execute: `commit('module/path/-path.to.prop')` or `commit('module/path/-path.to.prop.*')` 325 | * Import method: 326 | * `store.delete = (path, payload) => { return defaultDeletor(path, payload, store, conf) }` 327 | * 328 | * @param {string} path the path of the prop to delete eg. 'info/user/favColours.primary' 329 | * @param {*} payload either nothing or an id or {id} 330 | * @param {IInitialisedStore} store the store to attach 331 | * @param {IDefaultConfig} [conf={}] user config 332 | * @returns {*} dispatch or commit 333 | */ 334 | function defaultDeletor(path, payload, store, conf) { 335 | if (conf === void 0) { conf = {}; } 336 | var _a = formatDeletor(path, payload, store, conf), command = _a.command, _path = _a._path, _payload = _a._payload; 337 | if (command === 'error') 338 | return _payload; 339 | return store[command](_path, _payload); 340 | } 341 | function formatDeletor(path, payload, store, conf) { 342 | if (conf === void 0) { conf = {}; } 343 | var dConf = Object.assign({}, defaultConf, conf); // 'user/items.*.tags.*' 344 | var pArr = path.split('/'); // ['user', 'items.*.tags.*'] 345 | var props = pArr.pop(); // 'items.*.tags.*' 346 | var modulePath = (pArr.length) 347 | ? pArr.join('/') + '/' // 'user/' 348 | : ''; 349 | var deleteProp = (dConf.pattern === 'traditional') 350 | ? 'delete' + props[0].toUpperCase() + props.substring(1) // 'deleteItems.*.tags.*' 351 | : '-' + props; // '-items.*.tags.*' 352 | // Check if an action exists, if it does, trigger that and return early! 353 | var moduleDeleteProp = modulePath + deleteProp; 354 | var actionExists = store._actions[moduleDeleteProp]; 355 | if (actionExists) { 356 | return { command: 'dispatch', _path: moduleDeleteProp, _payload: payload }; 357 | } 358 | // [vuex-easy-firestore] check if it's a firestore module 359 | var _module = store._modulesNamespaceMap[modulePath]; 360 | var fsConf = (!_module) ? null : _module.state._conf; 361 | if (dConf.vuexEasyFirestore && fsConf) { 362 | // DOC: 'user/favColours.*', 'primary' 363 | // COLLECTION: 'items.*', '123' 364 | // COLLECTION: 'items.*.tags.*', ['123', 'dark'] 365 | var fsPropName = fsConf.statePropName; 366 | var fsProps = (fsPropName && props.startsWith(fsPropName + ".")) 367 | ? props.replace(fsPropName + ".", '') 368 | : props; 369 | var newPath = fsProps; 370 | if (fsProps.includes('*')) { 371 | var idsPayload = (!isWhat.isArray(payload)) ? [payload] : payload; 372 | var ids = getIdsFromPayload(idsPayload); 373 | newPath = fillinPathWildcards(ids, fsProps); 374 | } 375 | if (newPath) 376 | return { command: 'dispatch', _path: modulePath + 'delete', _payload: newPath }; 377 | } 378 | // Trigger the mutation! 379 | var DELETE_PROP = (dConf.pattern === 'traditional') 380 | ? 'DELETE_' + props.toUpperCase() // 'DELETE_ITEMS.*.TAGS.*' 381 | : '-' + props; // '-items.*.tags.*' 382 | var MODULE_DELETE_PROP = modulePath + DELETE_PROP; 383 | var mutationExists = store._mutations[MODULE_DELETE_PROP]; 384 | if (mutationExists) { 385 | return { command: 'commit', _path: MODULE_DELETE_PROP, _payload: payload }; 386 | } 387 | var triggeredError = error('missingDeleteMutation', dConf, MODULE_DELETE_PROP, props); 388 | return { command: 'error', _payload: triggeredError }; 389 | } 390 | /** 391 | * Creates a special 'setter-module' to be registered as a child of a module. This 'setter-module' will have the 'set' namespace (by default) and have one setter action per state prop in the parent module. The setter action's name will be the state prop name. 392 | * 393 | * @param {AnyObject} targetState parent module's state object 394 | * @param {string} [moduleNS=''] parent module's namespace, must end in '/' 395 | * @param {IInitialisedStore} store vuex store 396 | * @param {IDefaultConfig} conf user config 397 | * @returns {*} a special 'setter-module' to be registered as child of target module. 398 | */ 399 | function createSetterModule(targetState, moduleNS, store, conf) { 400 | if (moduleNS === void 0) { moduleNS = ''; } 401 | function getSetters(_targetState, _propPath) { 402 | if (_propPath === void 0) { _propPath = ''; } 403 | return Object.keys(_targetState).reduce(function (carry, stateProp) { 404 | // Get the path info up until this point 405 | var PROP_SUBPROP = (_propPath) 406 | ? _propPath + '.' + stateProp 407 | : stateProp; 408 | var MODULE_PROP_SUBPROP = moduleNS + PROP_SUBPROP; 409 | // Avoid making setters for private props 410 | if (conf.ignorePrivateProps && stateProp[0] === '_') 411 | return carry; 412 | if (conf.ignoreProps.includes(MODULE_PROP_SUBPROP)) 413 | return carry; 414 | // Avoid making setters for props which are an entire module on its own 415 | if (store._modulesNamespaceMap[MODULE_PROP_SUBPROP + '/']) 416 | return carry; 417 | // =================================================> 418 | // NORMAL SETTER 419 | // =================================================> 420 | // All good, make the action! 421 | carry[PROP_SUBPROP] = function (context, payload) { 422 | return defaultSetter(MODULE_PROP_SUBPROP, payload, store, conf); 423 | }; 424 | // Get the value of the prop 425 | var propValue = _targetState[stateProp]; 426 | // =================================================> 427 | // ARRAY SETTERS 428 | // =================================================> 429 | if (isWhat.isArray(propValue)) { 430 | carry[PROP_SUBPROP + '.push'] = function (context, payload) { 431 | return defaultSetter(MODULE_PROP_SUBPROP + '.push', payload, store, conf); 432 | }; 433 | carry[PROP_SUBPROP + '.pop'] = function (context, payload) { 434 | return defaultSetter(MODULE_PROP_SUBPROP + '.pop', payload, store, conf); 435 | }; 436 | carry[PROP_SUBPROP + '.shift'] = function (context, payload) { 437 | return defaultSetter(MODULE_PROP_SUBPROP + '.shift', payload, store, conf); 438 | }; 439 | carry[PROP_SUBPROP + '.splice'] = function (context, payload) { 440 | return defaultSetter(MODULE_PROP_SUBPROP + '.splice', payload, store, conf); 441 | }; 442 | } 443 | // =================================================> 444 | // WILDCARDS SETTER 445 | // =================================================> 446 | // if (isObject(propValue) && !Object.keys(propValue).length) { 447 | // carry[PROP_SUBPROP + '.*'] = (context, payload) => { 448 | // return defaultSetter(MODULE_PROP_SUBPROP + '.*', payload, store, conf) 449 | // } 450 | // } 451 | // =================================================> 452 | // CHILDREN SETTERS 453 | // =================================================> 454 | // let's do it's children as well! 455 | if (isWhat.isObject(propValue) && Object.keys(propValue).length) { 456 | var childrenSetters = getSetters(propValue, PROP_SUBPROP); 457 | Object.assign(carry, childrenSetters); 458 | } 459 | return carry; 460 | }, {}); 461 | } 462 | var setters = getSetters(targetState); 463 | return { actions: setters, namespaced: true }; 464 | } 465 | /** 466 | * Creates a special 'delete-module' to be registered as a child of a module. This 'delete-module' will have the 'delete' namespace (by default) and have one delete action per state prop in the parent module which holds an empty object. The delete action's name will be the state prop name + `.*`. 467 | * 468 | * @param {AnyObject} targetState parent module's state object 469 | * @param {string} [moduleNS=''] parent module's namespace, must end in '/' 470 | * @param {IInitialisedStore} store vuex store 471 | * @param {IDefaultConfig} conf user config 472 | * @returns {*} a special 'delete-module' to be registered as child of target module. 473 | */ 474 | function createDeleteModule(targetState, moduleNS, store, conf) { 475 | if (moduleNS === void 0) { moduleNS = ''; } 476 | function getDeletors(_targetState, _propPath) { 477 | if (_propPath === void 0) { _propPath = ''; } 478 | return Object.keys(_targetState).reduce(function (carry, stateProp) { 479 | // Get the path info up until this point 480 | var PROP_SUBPROP = (_propPath) 481 | ? _propPath + '.' + stateProp 482 | : stateProp; 483 | var MODULE_PROP_SUBPROP = moduleNS + PROP_SUBPROP; 484 | // Avoid making deletor for private props 485 | if (conf.ignorePrivateProps && stateProp[0] === '_') 486 | return carry; 487 | if (conf.ignoreProps.includes(MODULE_PROP_SUBPROP)) 488 | return carry; 489 | // Avoid making deletor for props which are an entire module on its own 490 | if (store._modulesNamespaceMap[MODULE_PROP_SUBPROP + '/']) 491 | return carry; 492 | // Get the value of the prop 493 | var propValue = _targetState[stateProp]; 494 | carry[PROP_SUBPROP] = function (context, payload) { 495 | return defaultDeletor(MODULE_PROP_SUBPROP, payload, store, conf); 496 | }; 497 | // let's do it's children as well! 498 | if (isWhat.isObject(propValue) && Object.keys(propValue).length) { 499 | var childrenDeletors = getDeletors(propValue, PROP_SUBPROP); 500 | Object.assign(carry, childrenDeletors); 501 | } 502 | return carry; 503 | }, {}); 504 | } 505 | var deletors = getDeletors(targetState); 506 | return { actions: deletors, namespaced: true }; 507 | } 508 | /** 509 | * Generate all vuex-easy-access modules: `/set/` and `/delete/` for each module 510 | * 511 | * @export 512 | * @param {IInitialisedStore} store 513 | * @param {IDefaultConfig} [conf={}] 514 | */ 515 | function generateSetterModules(store, conf) { 516 | if (conf === void 0) { conf = {}; } 517 | var modules = store._modulesNamespaceMap; 518 | var dConf = Object.assign({}, defaultConf, conf); 519 | Object.keys(modules).forEach(function (moduleNS) { 520 | var _module = modules[moduleNS]; 521 | var moduleName = getKeysFromPath(moduleNS + dConf.setter); 522 | var setterModule = createSetterModule(_module.state, moduleNS, store, dConf); 523 | store.registerModule(moduleName, setterModule); 524 | var deleteModule = createDeleteModule(_module.state, moduleNS, store, dConf); 525 | var deleteModuleName = getKeysFromPath(moduleNS + dConf.deletor); 526 | store.registerModule(deleteModuleName, deleteModule); 527 | }); 528 | var rootModuleName = dConf.setter; 529 | var rootSetterModule = createSetterModule(store.state, '', store, dConf); 530 | store.registerModule(rootModuleName, rootSetterModule); 531 | var rootDeleteModuleName = dConf.deletor; 532 | var rootDeleteModule = createDeleteModule(store.state, '', store, dConf); 533 | store.registerModule(rootDeleteModuleName, rootDeleteModule); 534 | } 535 | 536 | exports.defaultDeletor = defaultDeletor; 537 | exports.defaultSetter = defaultSetter; 538 | exports.formatDeletor = formatDeletor; 539 | exports.formatSetter = formatSetter; 540 | exports.generateSetterModules = generateSetterModules; 541 | --------------------------------------------------------------------------------