├── esdoc.json ├── .travis.yml ├── src ├── vuex-crud │ ├── client.js │ ├── createGetters.js │ ├── createState.js │ ├── createMutations.js │ └── createActions.js └── index.js ├── examples ├── blog │ ├── App.vue │ ├── store │ │ ├── articles.js │ │ └── index.js │ ├── index.html │ ├── app.js │ ├── router │ │ └── index.js │ └── components │ │ ├── Article.vue │ │ ├── Blog.vue │ │ └── ArticleDetail.vue ├── index.html ├── webpack.config.js └── server.js ├── .gitignore ├── .babelrc ├── .eslintrc.js ├── test ├── unit │ ├── fakeClient.js │ ├── vuex-crud │ │ ├── createState.spec.js │ │ ├── createGetters.spec.js │ │ ├── createActions.spec.js │ │ └── createMutations.spec.js │ └── index.spec.js └── e2e │ ├── runner.js │ ├── nightwatch.config.js │ └── specs │ └── blog.js ├── LICENSE ├── package.json └── README.md /esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./esdoc" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "8" 5 | - "9" 6 | -------------------------------------------------------------------------------- /src/vuex-crud/client.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default axios; 4 | -------------------------------------------------------------------------------- /examples/blog/App.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | lib 4 | esdoc 5 | coverage 6 | test/e2e/screenshots 7 | test/e2e/reports 8 | selenium-debug.log 9 | npm-debug.log 10 | coverage 11 | esdoc 12 | .tern-port 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "env": { 4 | "commonjs": { 5 | "plugins": [ 6 | ["transform-es2015-modules-commonjs", { "loose": true }] 7 | ] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/blog/store/articles.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */ 2 | import createCrudModule from 'vuex-crud'; 3 | /* eslint-enable */ 4 | 5 | export default createCrudModule({ 6 | resource: 'articles' 7 | }); 8 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vuex CRUD - Examples 6 | 7 | 8 |

Vuex CRUD - Examples

9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/blog/store/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import Vue from 'vue'; 3 | import Vuex from 'vuex'; 4 | /* eslint-enable */ 5 | 6 | import articles from './articles'; 7 | 8 | Vue.use(Vuex); 9 | 10 | export default new Vuex.Store({ 11 | modules: { 12 | articles 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /examples/blog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vuex CRUD example - Blog 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | parser: 'babel-eslint', 5 | 6 | extends: 'airbnb-base', 7 | 8 | plugins: [ 9 | 'html' 10 | ], 11 | 12 | parserOptions: { 13 | sourceType: 'module' 14 | }, 15 | 16 | env: { 17 | browser: true, 18 | node: true 19 | }, 20 | 21 | rules: { 22 | 'comma-dangle': [2, 'never'], 23 | 'no-underscore-dangle': 0, 24 | 'no-param-reassign': 0, 25 | 'func-names': 0 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /examples/blog/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */ 2 | import 'babel-polyfill'; 3 | import Vue from 'vue'; 4 | import { sync } from 'vuex-router-sync'; 5 | /* eslint-enable */ 6 | 7 | import store from './store'; 8 | import router from './router'; 9 | import App from './App.vue'; 10 | 11 | sync(store, router); 12 | 13 | export default new Vue({ 14 | el: '#app', 15 | store, 16 | router, 17 | render: h => h(App) 18 | }); 19 | -------------------------------------------------------------------------------- /examples/blog/router/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */ 2 | import Vue from 'vue'; 3 | import Router from 'vue-router'; 4 | /* eslint-enable */ 5 | 6 | import Blog from '../components/Blog.vue'; 7 | import Article from '../components/Article.vue'; 8 | 9 | Vue.use(Router); 10 | 11 | export default new Router({ 12 | routes: [ 13 | { 14 | path: '/', 15 | name: 'blog', 16 | component: Blog 17 | }, 18 | 19 | { 20 | path: '/articles/:id', 21 | name: 'article', 22 | component: Article 23 | } 24 | ] 25 | }); 26 | -------------------------------------------------------------------------------- /test/unit/fakeClient.js: -------------------------------------------------------------------------------- 1 | export default { 2 | successResponse: {}, 3 | 4 | errorResponse: {}, 5 | 6 | isSuccessful: true, 7 | 8 | defaultPromise() { 9 | const promise = new Promise((resolve, reject) => ((this.isSuccessful) ? 10 | resolve(this.successResponse) : 11 | reject(this.errorResponse) 12 | )); 13 | 14 | return promise; 15 | }, 16 | 17 | get() { 18 | return this.defaultPromise(); 19 | }, 20 | 21 | post() { 22 | return this.defaultPromise(); 23 | }, 24 | 25 | patch() { 26 | return this.defaultPromise(); 27 | }, 28 | 29 | put() { 30 | return this.defaultPromise(); 31 | }, 32 | 33 | delete() { 34 | return this.defaultPromise(); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | const spawn = require('cross-spawn'); 2 | 3 | /* eslint-disable no-var */ 4 | var args = process.argv.slice(2); 5 | /* eslint-enable */ 6 | 7 | const server = args.indexOf('--dev') > -1 8 | ? null 9 | : require('../../examples/server'); 10 | 11 | if (args.indexOf('--config') === -1) { 12 | args = args.concat(['--config', 'test/e2e/nightwatch.config.js']); 13 | } 14 | if (args.indexOf('--env') === -1) { 15 | args = args.concat(['--env', 'phantomjs']); 16 | } 17 | const i = args.indexOf('--test'); 18 | if (i > -1) { 19 | args[i + 1] = `test/e2e/specs/${args[i + 1]}`; 20 | } 21 | if (args.indexOf('phantomjs') > -1) { 22 | process.env.PHANTOMJS = true; 23 | } 24 | 25 | const runner = spawn('./node_modules/.bin/nightwatch', args, { 26 | stdio: 'inherit' 27 | }); 28 | 29 | runner.on('exit', (code) => { 30 | if (server) server.close(); 31 | process.exit(code); 32 | }); 33 | 34 | runner.on('error', (err) => { 35 | if (server) server.close(); 36 | throw err; 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jiří Chára 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/vuex-crud/createGetters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create default getters and merge them with getters defined by a user. 3 | */ 4 | const createGetters = ({ getters } = {}) => Object.assign({}, { 5 | /** 6 | * Return array of resources. 7 | */ 8 | list(state) { 9 | return state.list.map(id => state.entities[id.toString()]); 10 | }, 11 | 12 | /** 13 | * Get resource by id. 14 | */ 15 | byId(state) { 16 | return id => state.entities[id.toString()]; 17 | }, 18 | 19 | /** 20 | * Return true if there is a logged error. 21 | */ 22 | isError(state) { 23 | return state.fetchListError !== null || 24 | state.fetchSingleError !== null || 25 | state.createError !== null || 26 | state.updateError !== null || 27 | state.replaceError !== null || 28 | state.destroyError !== null; 29 | }, 30 | 31 | /** 32 | * Return true if there is a ongoing request. 33 | */ 34 | isLoading(state) { 35 | return state.isFetchingList || 36 | state.isFetchingSingle || 37 | state.isCreating || 38 | state.isUpdating || 39 | state.isReplacing || 40 | state.isDestroying; 41 | } 42 | }, getters); 43 | 44 | export default createGetters; 45 | -------------------------------------------------------------------------------- /src/vuex-crud/createState.js: -------------------------------------------------------------------------------- 1 | const createState = ({ state, only }) => { 2 | const crudState = { 3 | entities: {}, 4 | list: [] 5 | }; 6 | 7 | if (only.includes('FETCH_LIST')) { 8 | Object.assign(crudState, { 9 | isFetchingList: false, 10 | fetchListError: null 11 | }); 12 | } 13 | 14 | if (only.includes('FETCH_SINGLE')) { 15 | Object.assign(crudState, { 16 | isFetchingSingle: false, 17 | fetchSingleError: null 18 | }); 19 | } 20 | 21 | if (only.includes('CREATE')) { 22 | Object.assign(crudState, { 23 | isCreating: false, 24 | createError: null 25 | }); 26 | } 27 | 28 | if (only.includes('UPDATE')) { 29 | Object.assign(crudState, { 30 | isUpdating: false, 31 | updateError: null 32 | }); 33 | } 34 | 35 | if (only.includes('REPLACE')) { 36 | Object.assign(crudState, { 37 | isReplacing: false, 38 | replaceError: null 39 | }); 40 | } 41 | 42 | if (only.includes('DESTROY')) { 43 | Object.assign(crudState, { 44 | isDestroying: false, 45 | destroyError: null 46 | }); 47 | } 48 | 49 | return Object.assign(crudState, state); 50 | }; 51 | 52 | export default createState; 53 | -------------------------------------------------------------------------------- /examples/blog/components/Article.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 56 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | devtool: 'inline-source-map', 7 | 8 | entry: fs.readdirSync(__dirname).reduce((entries, dir) => { 9 | const fullDir = path.join(__dirname, dir); 10 | const entry = path.join(fullDir, 'app.js'); 11 | if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) { 12 | entries[dir] = ['webpack-hot-middleware/client', entry]; 13 | } 14 | 15 | return entries; 16 | }, {}), 17 | 18 | output: { 19 | path: path.join(__dirname, '__build__'), 20 | filename: '[name].js', 21 | chunkFilename: '[id].chunk.js', 22 | publicPath: '/__build__/' 23 | }, 24 | 25 | module: { 26 | rules: [ 27 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, 28 | { test: /\.vue$/, loader: 'vue-loader' } 29 | ] 30 | }, 31 | 32 | resolve: { 33 | alias: { 34 | 'vuex-crud': path.resolve(__dirname, '../src/index.js') 35 | } 36 | }, 37 | 38 | plugins: [ 39 | new webpack.optimize.CommonsChunkPlugin({ 40 | name: 'shared', 41 | filename: 'shared.js' 42 | }), 43 | new webpack.DefinePlugin({ 44 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 45 | }), 46 | new webpack.HotModuleReplacementPlugin(), 47 | new webpack.NoEmitOnErrorsPlugin() 48 | ] 49 | }; 50 | -------------------------------------------------------------------------------- /examples/blog/components/Blog.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 56 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quote-props */ 2 | const selenium = require('selenium-server'); 3 | const chromedriver = require('chromedriver'); 4 | 5 | module.exports = { 6 | 'src_folders': ['test/e2e/specs'], 7 | 'output_folder': 'test/e2e/reports', 8 | 'custom_commands_path': ['node_modules/nightwatch-helpers/commands'], 9 | 'custom_assertions_path': ['node_modules/nightwatch-helpers/assertions'], 10 | 11 | 'selenium': { 12 | 'start_process': true, 13 | 'server_path': selenium.path, 14 | 'host': '127.0.0.1', 15 | 'port': 4444, 16 | 'cli_args': { 17 | 'webdriver.chrome.driver': chromedriver.path 18 | } 19 | }, 20 | 21 | 'test_settings': { 22 | 'default': { 23 | 'selenium_port': 4444, 24 | 'selenium_host': 'localhost', 25 | 'silent': true, 26 | 'screenshots': { 27 | 'enabled': true, 28 | 'on_failure': true, 29 | 'on_error': false, 30 | 'path': 'test/e2e/screenshots' 31 | } 32 | }, 33 | 34 | 'chrome': { 35 | 'desiredCapabilities': { 36 | 'browserName': 'chrome', 37 | 'javascriptEnabled': true, 38 | 'acceptSslCerts': true, 39 | 'chromeOptions': { 40 | 'args': [ 41 | 'headless' 42 | ] 43 | } 44 | } 45 | }, 46 | 47 | 'phantomjs': { 48 | 'desiredCapabilities': { 49 | 'browserName': 'phantomjs', 50 | 'javascriptEnabled': true, 51 | 'acceptSslCerts': true 52 | } 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /examples/blog/components/ArticleDetail.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 71 | -------------------------------------------------------------------------------- /test/unit/vuex-crud/createState.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import createState from '../../../src/vuex-crud/createState'; 4 | 5 | test('creates store with fetch list props', (t) => { 6 | t.deepEqual(createState({ only: ['FETCH_LIST'] }), { 7 | entities: {}, 8 | list: [], 9 | 10 | isFetchingList: false, 11 | fetchListError: null 12 | }); 13 | }); 14 | 15 | test('creates store with fetch single props', (t) => { 16 | t.deepEqual(createState({ only: ['FETCH_SINGLE'] }), { 17 | entities: {}, 18 | list: [], 19 | 20 | isFetchingSingle: false, 21 | fetchSingleError: null 22 | }); 23 | }); 24 | 25 | test('creates store with create props', (t) => { 26 | t.deepEqual(createState({ only: ['CREATE'] }), { 27 | entities: {}, 28 | list: [], 29 | 30 | isCreating: false, 31 | createError: null 32 | }); 33 | }); 34 | 35 | test('creates store with update props', (t) => { 36 | t.deepEqual(createState({ only: ['UPDATE'] }), { 37 | entities: {}, 38 | list: [], 39 | 40 | isUpdating: false, 41 | updateError: null 42 | }); 43 | }); 44 | 45 | test('creates store with replace props', (t) => { 46 | t.deepEqual(createState({ only: ['REPLACE'] }), { 47 | entities: {}, 48 | list: [], 49 | 50 | isReplacing: false, 51 | replaceError: null 52 | }); 53 | }); 54 | 55 | test('creates store with destroy props', (t) => { 56 | t.deepEqual(createState({ only: ['DESTROY'] }), { 57 | entities: {}, 58 | list: [], 59 | 60 | isDestroying: false, 61 | destroyError: null 62 | }); 63 | }); 64 | 65 | test('returns all properties', (t) => { 66 | t.deepEqual(createState({ only: ['FETCH_LIST', 'FETCH_SINGLE', 'CREATE', 'UPDATE', 'REPLACE', 'DESTROY'] }), { 67 | entities: {}, 68 | list: [], 69 | 70 | isFetchingList: false, 71 | fetchListError: null, 72 | 73 | isFetchingSingle: false, 74 | fetchSingleError: null, 75 | 76 | isCreating: false, 77 | createError: null, 78 | 79 | isUpdating: false, 80 | updateError: null, 81 | 82 | isReplacing: false, 83 | replaceError: null, 84 | 85 | isDestroying: false, 86 | destroyError: null 87 | }); 88 | }); 89 | 90 | test('enhances state', (t) => { 91 | t.deepEqual(createState({ state: { foo: 'bar', list: [1] }, only: ['FETCH_LIST', 'CREATE'] }), { 92 | foo: 'bar', 93 | 94 | entities: {}, 95 | list: [1], 96 | 97 | isFetchingList: false, 98 | fetchListError: null, 99 | 100 | isCreating: false, 101 | createError: null 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-crud", 3 | "version": "0.3.2", 4 | "description": "A library for Vuex to build CRUD modules easily", 5 | "main": "lib/index.js", 6 | "repository": "https://github.com/JiriChara/vuex-crud", 7 | "author": "Jiri Chara ", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "npm run build:commonjs", 11 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 12 | "clean": "rimraf lib coverage esdoc", 13 | "doc": "mkdirp esdoc && esdoc -c esdoc.json", 14 | "lint": "eslint src test examples", 15 | "report": "npm run test && nyc report --reporter=lcov && codecov", 16 | "test:unit": "nyc ava", 17 | "test:e2e": "node test/e2e/runner.js", 18 | "test": "npm run lint && npm run test:unit && npm run test:e2e", 19 | "prepublish": "npm run clean && npm run report && npm run build && npm run doc" 20 | }, 21 | "files": [ 22 | "src", 23 | "lib" 24 | ], 25 | "devDependencies": { 26 | "ava": "^0.24.0", 27 | "axios": "^0.17.1", 28 | "babel-cli": "^6.26.0", 29 | "babel-core": "^6.26.0", 30 | "babel-eslint": "^8.2.1", 31 | "babel-loader": "^7.1.2", 32 | "babel-polyfill": "^6.26.0", 33 | "babel-preset-es2015": "^6.24.1", 34 | "babel-preset-stage-2": "^6.24.1", 35 | "bavaria-ipsum": "^1.0.3", 36 | "body-parser": "^1.18.2", 37 | "chromedriver": "^2.35.0", 38 | "codecov": "^3.0.0", 39 | "cross-env": "^5.1.3", 40 | "cross-spawn": "^6.0.3", 41 | "esdoc": "^1.0.4", 42 | "eslint": "^4.16.0", 43 | "eslint-config-airbnb": "^16.1.0", 44 | "eslint-plugin-html": "^4.0.2", 45 | "eslint-plugin-import": "^2.8.0", 46 | "eslint-plugin-jsx-a11y": "^6.0.3", 47 | "eslint-plugin-react": "^7.6.0", 48 | "express": "^4.16.2", 49 | "growl": "^1.10.3", 50 | "mkdirp": "^0.5.1", 51 | "nightwatch": "^0.9.19", 52 | "nightwatch-helpers": "^1.2.0", 53 | "nyc": "^11.4.1", 54 | "phantomjs-prebuilt": "^2.1.16", 55 | "rimraf": "^2.6.2", 56 | "selenium-server": "^2.53.1", 57 | "sinon": "^4.2.1", 58 | "vue": "^2.5.13", 59 | "vue-loader": "^13.7.0", 60 | "vue-router": "^3.0.1", 61 | "vue-template-compiler": "^2.5.13", 62 | "vuex": "^3.0.1", 63 | "vuex-router-sync": "^5.0.0", 64 | "webpack": "^3.10.0", 65 | "webpack-dev-middleware": "^2.0.4", 66 | "webpack-hot-middleware": "^2.21.0" 67 | }, 68 | "peerDependencies": { 69 | "axios": "^0.17", 70 | "vue": "^2.5" 71 | }, 72 | "ava": { 73 | "files": [ 74 | "test/unit/**/*.spec.js" 75 | ], 76 | "source": [ 77 | "src/**/*.js" 78 | ], 79 | "concurrency": 5, 80 | "failFast": true, 81 | "tap": false, 82 | "powerAssert": false, 83 | "babel": "inherit", 84 | "require": [ 85 | "babel-register" 86 | ] 87 | }, 88 | "nyc": { 89 | "include": [ 90 | "src/**/*.js" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const express = require('express'); 3 | const webpack = require('webpack'); 4 | const webpackDevMiddleware = require('webpack-dev-middleware'); 5 | const webpackHotMiddleware = require('webpack-hot-middleware'); 6 | const WebpackConfig = require('./webpack.config'); 7 | const Ipsum = require('bavaria-ipsum'); 8 | const bodyParser = require('body-parser'); 9 | /* eslint-enable */ 10 | 11 | const app = express(); 12 | const compiler = webpack(WebpackConfig); 13 | const ipsum = new Ipsum(); 14 | 15 | const articles = [ 16 | { 17 | id: 1, 18 | title: ipsum.generateSentence(), 19 | content: ipsum.generateParagraph() 20 | }, 21 | { 22 | id: 2, 23 | title: ipsum.generateSentence(), 24 | content: ipsum.generateParagraph() 25 | }, 26 | { 27 | id: 3, 28 | title: ipsum.generateSentence(), 29 | content: ipsum.generateParagraph() 30 | } 31 | ]; 32 | 33 | app.use(webpackDevMiddleware(compiler, { 34 | publicPath: '/__build__/', 35 | stats: { 36 | colors: true, 37 | chunks: false 38 | } 39 | })); 40 | 41 | app.use(webpackHotMiddleware(compiler)); 42 | 43 | app.use(express.static(__dirname)); 44 | 45 | app.use(bodyParser.json()); 46 | 47 | app.get('/api/articles', (req, res) => { 48 | res.json(articles); 49 | }); 50 | 51 | app.get('/api/articles/:id', (req, res) => { 52 | const article = articles.find(a => a.id.toString() === req.params.id); 53 | const index = articles.indexOf(article); 54 | 55 | res.json(articles[index]); 56 | }); 57 | 58 | app.patch('/api/articles/:id', (req, res) => { 59 | const { body } = req; 60 | const article = articles.find(a => a.id.toString() === req.params.id); 61 | const index = articles.indexOf(article); 62 | 63 | if (index >= 0) { 64 | article.title = body.title; 65 | article.content = body.content; 66 | articles[index] = article; 67 | } 68 | 69 | res.json(article); 70 | }); 71 | 72 | app.put('/api/articles/:id', (req, res) => { 73 | const { body } = req; 74 | const article = articles.find(a => a.id.toString() === req.params.id); 75 | const index = articles.indexOf(article); 76 | 77 | if (index >= 0) { 78 | article.title = body.title; 79 | article.content = body.content; 80 | articles[index] = article; 81 | } 82 | 83 | res.json(article); 84 | }); 85 | 86 | app.delete('/api/articles/:id', (req, res) => { 87 | const article = articles.find(a => a.id.toString() === req.params.id); 88 | const index = articles.indexOf(article); 89 | 90 | if (index >= 0) articles.splice(index, 1); 91 | 92 | res.status(202).send(); 93 | }); 94 | 95 | app.post('/api/articles', (req, res) => { 96 | const id = articles[articles.length - 1].id + 1; 97 | const { body } = req; 98 | 99 | const article = { 100 | id, 101 | title: body.title, 102 | content: body.content 103 | }; 104 | 105 | articles.push(article); 106 | 107 | res.json(article); 108 | }); 109 | 110 | const port = process.env.PORT || 8080; 111 | 112 | module.exports = app.listen(port, () => { 113 | /* eslint-disable no-console */ 114 | console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`); 115 | /* eslint-enable no-console */ 116 | }); 117 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import defaultClient from './vuex-crud/client'; 2 | import createActions from './vuex-crud/createActions'; 3 | import createGetters from './vuex-crud/createGetters'; 4 | import createMutations from './vuex-crud/createMutations'; 5 | import createState from './vuex-crud/createState'; 6 | 7 | /** 8 | * Creates new Vuex CRUD module. 9 | * 10 | * @param {Object} configuration 11 | * @property {String} idAttribute The name of ID attribute. 12 | * @property {String} resource The name of the resource. 13 | * @property {String} urlRoot The root url. 14 | * @property {Function} customUrlFn A custom getter for more complex URL to request data from API. 15 | * @property {Object} state The default state (will override generated state). 16 | * @property {Object} actions The default actions (will override generated actions object). 17 | * @property {Object} mutations The default mutations (will override generated mutations object). 18 | * @property {Object} getters The default getters (will override generated getters object). 19 | * @property {Object} client The client that should be used to do API requests. 20 | * @property {Function} onFetchListStart Mutation method called after collection fetch start. 21 | * @property {Function} onFetchListSuccess Mutation method called after collection fetch success. 22 | * @property {Function} onFetchListError Mutation method called after collection fetch error. 23 | * @property {Function} onFetchSingleStart Mutation method called after single fetch start. 24 | * @property {Function} onFetchSingleSuccess Mutation method called after single fetch success. 25 | * @property {Function} onFetchSingleError Mutation method called after single fetch error. 26 | * @property {Function} onCreateStart Mutation method called after create state. 27 | * @property {Function} onCreateSuccess Mutation method called after create success. 28 | * @property {Function} onCreateError Mutation method called after create error. 29 | * @property {Function} onUpdateStart Mutation method called after update state. 30 | * @property {Function} onUpdateSuccess Mutation method called after update success. 31 | * @property {Function} onUpdateError Mutation method called after update error. 32 | * @property {Function} onReplaceStart Mutation method called after replace state. 33 | * @property {Function} onReplaceSuccess Mutation method called after replace success. 34 | * @property {Function} onReplaceError Mutation method called after replace error. 35 | * @property {Function} onDestroyStart Mutation method called after destroy state. 36 | * @property {Function} onDestroySuccess Mutation method called after destroy success. 37 | * @property {Function} onDestroyError Mutation method called after destroy error. 38 | * @property {Array} only A list of CRUD actions that should be available. 39 | * @property {Function} parseList A method used to parse list of resources. 40 | * @property {Function} parseSingle A method used to parse singe resource. 41 | * @property {Function} parseError A method used to parse error responses. 42 | * @return {Object} A Vuex module. 43 | */ 44 | const createCrud = ({ 45 | idAttribute = 'id', 46 | resource, 47 | urlRoot, 48 | customUrlFn = null, 49 | state = {}, 50 | actions = {}, 51 | mutations = {}, 52 | getters = {}, 53 | client = defaultClient, 54 | onFetchListStart = () => {}, 55 | onFetchListSuccess = () => {}, 56 | onFetchListError = () => {}, 57 | onFetchSingleStart = () => {}, 58 | onFetchSingleSuccess = () => {}, 59 | onFetchSingleError = () => {}, 60 | onCreateStart = () => {}, 61 | onCreateSuccess = () => {}, 62 | onCreateError = () => {}, 63 | onUpdateStart = () => {}, 64 | onUpdateSuccess = () => {}, 65 | onUpdateError = () => {}, 66 | onReplaceStart = () => {}, 67 | onReplaceSuccess = () => {}, 68 | onReplaceError = () => {}, 69 | onDestroyStart = () => {}, 70 | onDestroySuccess = () => {}, 71 | onDestroyError = () => {}, 72 | only = ['FETCH_LIST', 'FETCH_SINGLE', 'CREATE', 'UPDATE', 'REPLACE', 'DESTROY'], 73 | parseList = res => res, 74 | parseSingle = res => res, 75 | parseError = res => res 76 | } = {}) => { 77 | if (!resource) { 78 | throw new Error('Resource name must be specified'); 79 | } 80 | 81 | let rootUrl; 82 | 83 | /** 84 | * Create root url for API requests. By default it is: /api/. 85 | * Use custom url getter if given. 86 | */ 87 | if (typeof customUrlFn === 'function') { 88 | rootUrl = customUrlFn; 89 | } else if (typeof urlRoot === 'string') { 90 | rootUrl = ((url) => { 91 | const lastCharacter = url.substr(-1); 92 | 93 | return lastCharacter === '/' ? url.slice(0, -1) : url; 94 | })(urlRoot); 95 | } else { 96 | rootUrl = `/api/${resource}`; 97 | } 98 | 99 | return { 100 | namespaced: true, 101 | 102 | state: createState({ state, only }), 103 | 104 | actions: createActions({ 105 | actions, 106 | rootUrl, 107 | only, 108 | client, 109 | parseList, 110 | parseSingle, 111 | parseError 112 | }), 113 | 114 | mutations: createMutations({ 115 | mutations, 116 | idAttribute, 117 | only, 118 | onFetchListStart, 119 | onFetchListSuccess, 120 | onFetchListError, 121 | onFetchSingleStart, 122 | onFetchSingleSuccess, 123 | onFetchSingleError, 124 | onCreateStart, 125 | onCreateSuccess, 126 | onCreateError, 127 | onUpdateStart, 128 | onUpdateSuccess, 129 | onUpdateError, 130 | onReplaceStart, 131 | onReplaceSuccess, 132 | onReplaceError, 133 | onDestroyStart, 134 | onDestroySuccess, 135 | onDestroyError 136 | }), 137 | 138 | getters: createGetters({ getters }) 139 | }; 140 | }; 141 | 142 | export default createCrud; 143 | 144 | /** 145 | * Export client in case user want's to add interceptors. 146 | */ 147 | export { defaultClient as client }; 148 | -------------------------------------------------------------------------------- /src/vuex-crud/createMutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | /** 4 | * Create default mutations and merge them with mutations defined by a user. 5 | */ 6 | const createMutations = ({ 7 | mutations, 8 | only, 9 | idAttribute, 10 | onFetchListStart, 11 | onFetchListSuccess, 12 | onFetchListError, 13 | onFetchSingleStart, 14 | onFetchSingleSuccess, 15 | onFetchSingleError, 16 | onCreateStart, 17 | onCreateSuccess, 18 | onCreateError, 19 | onUpdateStart, 20 | onUpdateSuccess, 21 | onUpdateError, 22 | onReplaceStart, 23 | onReplaceSuccess, 24 | onReplaceError, 25 | onDestroyStart, 26 | onDestroySuccess, 27 | onDestroyError 28 | }) => { 29 | const crudMutations = {}; 30 | 31 | if (only.includes('FETCH_LIST')) { 32 | Object.assign(crudMutations, { 33 | fetchListStart(state) { 34 | state.isFetchingList = true; 35 | onFetchListStart(state); 36 | }, 37 | 38 | fetchListSuccess(state, response) { 39 | const { data } = response; 40 | 41 | data.forEach((m) => { 42 | Vue.set(state.entities, m[idAttribute].toString(), m); 43 | }); 44 | state.list = data.map(m => m[idAttribute].toString()); 45 | state.isFetchingList = false; 46 | state.fetchListError = null; 47 | onFetchListSuccess(state, response); 48 | }, 49 | 50 | fetchListError(state, err) { 51 | state.list = []; 52 | state.fetchListError = err; 53 | state.isFetchingList = false; 54 | onFetchListError(state, err); 55 | } 56 | }); 57 | } 58 | 59 | if (only.includes('FETCH_SINGLE')) { 60 | Object.assign(crudMutations, { 61 | fetchSingleStart(state) { 62 | state.isFetchingSingle = true; 63 | onFetchSingleStart(state); 64 | }, 65 | 66 | fetchSingleSuccess(state, response) { 67 | const { data } = response; 68 | const id = data[idAttribute].toString(); 69 | 70 | Vue.set(state.entities, id, data); 71 | state.isFetchingSingle = false; 72 | state.fetchSingleError = null; 73 | onFetchSingleSuccess(state, response); 74 | }, 75 | 76 | fetchSingleError(state, err) { 77 | state.fetchSingleError = err; 78 | state.isFetchingSingle = false; 79 | onFetchSingleError(state, err); 80 | } 81 | }); 82 | } 83 | 84 | if (only.includes('CREATE')) { 85 | Object.assign(crudMutations, { 86 | createStart(state) { 87 | state.isCreating = true; 88 | onCreateStart(state); 89 | }, 90 | 91 | createSuccess(state, response) { 92 | const { data } = response; 93 | if (data) { 94 | const id = data[idAttribute].toString(); 95 | Vue.set(state.entities, id, data); 96 | } 97 | state.isCreating = false; 98 | state.createError = null; 99 | onCreateSuccess(state, response); 100 | }, 101 | 102 | createError(state, err) { 103 | state.createError = err; 104 | state.isCreating = false; 105 | onCreateError(state, err); 106 | } 107 | }); 108 | } 109 | 110 | if (only.includes('UPDATE')) { 111 | Object.assign(crudMutations, { 112 | updateStart(state) { 113 | state.isUpdating = true; 114 | onUpdateStart(state); 115 | }, 116 | 117 | updateSuccess(state, response) { 118 | const { data } = response; 119 | 120 | const id = data[idAttribute].toString(); 121 | 122 | Vue.set(state.entities, id, data); 123 | 124 | const listIndex = state.list.indexOf(id); 125 | 126 | if (listIndex >= 0) { 127 | Vue.set(state.list, listIndex, id); 128 | } 129 | 130 | state.isUpdating = false; 131 | state.updateError = null; 132 | onUpdateSuccess(state, response); 133 | }, 134 | 135 | updateError(state, err) { 136 | state.updateError = err; 137 | state.isUpdating = false; 138 | onUpdateError(state, err); 139 | } 140 | }); 141 | } 142 | 143 | if (only.includes('REPLACE')) { 144 | Object.assign(crudMutations, { 145 | replaceStart(state) { 146 | state.isReplacing = true; 147 | onReplaceStart(state); 148 | }, 149 | 150 | replaceSuccess(state, response) { 151 | const { data } = response; 152 | 153 | const id = data[idAttribute].toString(); 154 | 155 | Vue.set(state.entities, id, data); 156 | 157 | const listIndex = state.list.indexOf(id); 158 | 159 | if (listIndex >= 0) { 160 | Vue.set(state.list, listIndex, id); 161 | } 162 | 163 | state.isReplacing = false; 164 | state.replaceError = null; 165 | onReplaceSuccess(state, response); 166 | }, 167 | 168 | replaceError(state, err) { 169 | state.replaceError = err; 170 | state.isReplacing = false; 171 | onReplaceError(state, err); 172 | } 173 | }); 174 | } 175 | 176 | if (only.includes('DESTROY')) { 177 | Object.assign(crudMutations, { 178 | destroyStart(state) { 179 | state.isDestroying = true; 180 | onDestroyStart(state); 181 | }, 182 | 183 | destroySuccess(state, id, response) { 184 | const listIndex = state.list.indexOf(id.toString()); 185 | 186 | if (listIndex >= 0) { 187 | Vue.delete(state.list, listIndex); 188 | } 189 | 190 | Vue.delete(state.entities, id.toString()); 191 | 192 | state.isDestroying = false; 193 | state.destroyError = null; 194 | onDestroySuccess(state, response); 195 | }, 196 | 197 | destroyError(state, err) { 198 | state.destroyError = err; 199 | state.isDestroying = false; 200 | onDestroyError(state, err); 201 | } 202 | }); 203 | } 204 | 205 | return Object.assign(crudMutations, mutations); 206 | }; 207 | 208 | export default createMutations; 209 | -------------------------------------------------------------------------------- /src/vuex-crud/createActions.js: -------------------------------------------------------------------------------- 1 | const createActions = ({ 2 | actions, 3 | rootUrl, 4 | client, 5 | only, 6 | parseList, 7 | parseSingle, 8 | parseError 9 | }) => { 10 | const [ 11 | FETCH_LIST, 12 | FETCH_SINGLE, 13 | CREATE, 14 | UPDATE, 15 | REPLACE, 16 | DESTROY 17 | ] = ['FETCH_LIST', 'FETCH_SINGLE', 'CREATE', 'UPDATE', 'REPLACE', 'DESTROY']; 18 | const crudActions = {}; 19 | const isUsingCustomUrlGetter = typeof rootUrl === 'function'; 20 | 21 | const urlGetter = ({ 22 | customUrl, 23 | customUrlFnArgs, 24 | id, 25 | type 26 | }) => { 27 | if (typeof customUrl === 'string') { 28 | return customUrl; 29 | } else if (isUsingCustomUrlGetter) { 30 | const argsArray = Array.isArray(customUrlFnArgs) ? customUrlFnArgs : [customUrlFnArgs]; 31 | const args = [id || null, type || null].concat(argsArray); 32 | return rootUrl(...args); 33 | } 34 | 35 | return id ? `${rootUrl}/${id}` : rootUrl; 36 | }; 37 | 38 | if (only.includes(FETCH_LIST)) { 39 | Object.assign(crudActions, { 40 | /** 41 | * GET /api/ 42 | * 43 | * Fetch list of resources. 44 | */ 45 | fetchList({ commit }, { config, customUrl, customUrlFnArgs = [] } = {}) { 46 | commit('fetchListStart'); 47 | 48 | return client.get(urlGetter({ customUrl, customUrlFnArgs, type: FETCH_LIST }), config) 49 | .then((res) => { 50 | const parsedResponse = parseList(res); 51 | 52 | commit('fetchListSuccess', parsedResponse); 53 | 54 | return parsedResponse; 55 | }) 56 | .catch((err) => { 57 | const parsedError = parseError(err); 58 | 59 | commit('fetchListError', parsedError); 60 | 61 | return Promise.reject(parsedError); 62 | }); 63 | } 64 | }); 65 | } 66 | 67 | if (only.includes(FETCH_SINGLE)) { 68 | Object.assign(crudActions, { 69 | /** 70 | * GET /api//:id 71 | * 72 | * Fetch single resource. 73 | */ 74 | fetchSingle({ commit }, { 75 | id, 76 | config, 77 | customUrl, 78 | customUrlFnArgs = [] 79 | } = {}) { 80 | commit('fetchSingleStart'); 81 | 82 | return client.get(urlGetter({ 83 | customUrl, 84 | customUrlFnArgs, 85 | type: FETCH_SINGLE, 86 | id 87 | }), config) 88 | .then((res) => { 89 | const parsedResponse = parseSingle(res); 90 | 91 | commit('fetchSingleSuccess', parsedResponse); 92 | 93 | return res; 94 | }) 95 | .catch((err) => { 96 | const parsedError = parseError(err); 97 | 98 | commit('fetchSingleError', parsedError); 99 | 100 | return Promise.reject(parsedError); 101 | }); 102 | } 103 | }); 104 | } 105 | 106 | if (only.includes(CREATE)) { 107 | Object.assign(crudActions, { 108 | /** 109 | * POST /api/ 110 | * 111 | * Create a new reource. 112 | */ 113 | create({ commit }, { 114 | data, 115 | config, 116 | customUrl, 117 | customUrlFnArgs = [] 118 | } = {}) { 119 | commit('createStart'); 120 | 121 | return client.post(urlGetter({ customUrl, customUrlFnArgs, type: CREATE }), data, config) 122 | .then((res) => { 123 | const parsedResponse = parseSingle(res); 124 | 125 | commit('createSuccess', parsedResponse); 126 | 127 | return parsedResponse; 128 | }) 129 | .catch((err) => { 130 | const parsedError = parseError(err); 131 | 132 | commit('createError', parsedError); 133 | 134 | return Promise.reject(parsedError); 135 | }); 136 | } 137 | }); 138 | } 139 | 140 | if (only.includes(UPDATE)) { 141 | Object.assign(crudActions, { 142 | /** 143 | * PATCH /api//:id 144 | * 145 | * Update a single resource. 146 | */ 147 | update({ commit }, { 148 | id, 149 | data, 150 | config, 151 | customUrl, 152 | customUrlFnArgs = [] 153 | } = {}) { 154 | commit('updateStart'); 155 | 156 | return client.patch(urlGetter({ 157 | customUrl, 158 | customUrlFnArgs, 159 | type: UPDATE, 160 | id 161 | }), data, config) 162 | .then((res) => { 163 | const parsedResponse = parseSingle(res); 164 | 165 | commit('updateSuccess', parsedResponse); 166 | 167 | return parsedResponse; 168 | }) 169 | .catch((err) => { 170 | const parsedError = parseError(err); 171 | 172 | commit('updateError', parsedError); 173 | 174 | return Promise.reject(parsedError); 175 | }); 176 | } 177 | }); 178 | } 179 | 180 | if (only.includes(REPLACE)) { 181 | Object.assign(crudActions, { 182 | /** 183 | * PUT /api//:id 184 | * 185 | * Update a single resource. 186 | */ 187 | replace({ commit }, { 188 | id, 189 | data, 190 | config, 191 | customUrl, 192 | customUrlFnArgs = [] 193 | } = {}) { 194 | commit('replaceStart'); 195 | 196 | return client.put(urlGetter({ 197 | customUrl, 198 | customUrlFnArgs, 199 | type: REPLACE, 200 | id 201 | }), data, config) 202 | .then((res) => { 203 | const parsedResponse = parseSingle(res); 204 | 205 | commit('replaceSuccess', parsedResponse); 206 | 207 | return parsedResponse; 208 | }) 209 | .catch((err) => { 210 | const parsedError = parseError(err); 211 | 212 | commit('replaceError', parsedError); 213 | 214 | return Promise.reject(parsedError); 215 | }); 216 | } 217 | }); 218 | } 219 | 220 | if (only.includes(DESTROY)) { 221 | Object.assign(crudActions, { 222 | /** 223 | * DELETE /api//:id 224 | * 225 | * Destroy a single resource. 226 | */ 227 | destroy({ commit }, { 228 | id, 229 | config, 230 | customUrl, 231 | customUrlFnArgs = [] 232 | } = {}) { 233 | commit('destroyStart'); 234 | 235 | return client.delete(urlGetter({ 236 | customUrl, 237 | customUrlFnArgs, 238 | type: DESTROY, 239 | id 240 | }), config) 241 | .then((res) => { 242 | const parsedResponse = parseSingle(res); 243 | 244 | commit('destroySuccess', id, parsedResponse); 245 | 246 | return parsedResponse; 247 | }) 248 | .catch((err) => { 249 | const parsedError = parseError(err); 250 | 251 | commit('destroyError', parsedError); 252 | 253 | return Promise.reject(parsedError); 254 | }); 255 | } 256 | }); 257 | } 258 | 259 | return Object.assign(crudActions, actions); 260 | }; 261 | 262 | export default createActions; 263 | -------------------------------------------------------------------------------- /test/e2e/specs/blog.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | blog(browser) { 3 | browser 4 | .url('http://localhost:8080/blog/') 5 | .waitForElementVisible('#vue-app', 1000) 6 | .assert.elementPresent('button.fetch-articles') 7 | .assert.elementPresent('button.create-article') 8 | 9 | // Fetch List Tests 10 | .click('button.fetch-articles') 11 | .waitForElementVisible('#articles', 1000) 12 | .assert.elementPresent('article.article-1') 13 | .assert.elementPresent('article.article-2') 14 | .assert.elementPresent('article.article-3') 15 | .assert.elementNotPresent('article.article-4') 16 | 17 | // Create Tests 18 | .click('button.create-article') 19 | .click('button.fetch-articles') 20 | .waitForElementVisible('article.article-4', 1000) 21 | .assert.elementPresent('article.article-1') 22 | .assert.elementPresent('article.article-2') 23 | .assert.elementPresent('article.article-3') 24 | .assert.elementPresent('article.article-4') 25 | 26 | // Update Tests 27 | .getText('article.article-1 h1', (result) => { 28 | browser.expect.element('article.article-1 h1').text.to.equal(result.value); 29 | }) 30 | .getText('article.article-1 h1', (result) => { 31 | browser.click('article.article-1 .edit-article'); 32 | browser.expect.element('article.article-1 h1').text.to.not.equal(result.value); 33 | }) 34 | .getText('article.article-1 p.content', (result) => { 35 | browser.expect.element('article.article-1 p.content').text.to.equal(result.value); 36 | }) 37 | .getText('article.article-1 p.content', (result) => { 38 | browser.click('article.article-1 .edit-article'); 39 | browser.expect.element('article.article-1 p.content').text.to.not.equal(result.value); 40 | }) 41 | 42 | // Replace Tests 43 | .getText('article.article-2 h1', (result) => { 44 | browser.expect.element('article.article-2 h1').text.to.equal(result.value); 45 | }) 46 | .getText('article.article-2 h1', (result) => { 47 | browser.click('article.article-2 .replace-article'); 48 | browser.expect.element('article.article-2 h1').text.to.not.equal(result.value); 49 | }) 50 | .getText('article.article-2 p.content', (result) => { 51 | browser.expect.element('article.article-2 p.content').text.to.equal(result.value); 52 | }) 53 | .getText('article.article-2 p.content', (result) => { 54 | browser.click('article.article-2 .replace-article'); 55 | browser.expect.element('article.article-2 p.content').text.to.not.equal(result.value); 56 | }) 57 | 58 | // Delete Test 59 | .click('article.article-4 .delete-article') 60 | .assert.elementNotPresent('article.article-4') 61 | 62 | // Navigate to single article 63 | .click('article.article-3 a') 64 | .waitForElementVisible('#article', 1000) 65 | 66 | .assert.elementNotPresent('article.article-1') 67 | .assert.elementNotPresent('article.article-2') 68 | .assert.elementPresent('article.article-3') 69 | .assert.elementNotPresent('article.article-4') 70 | 71 | // Update Single Tests 72 | .getText('article.article-3 h1', (result) => { 73 | browser.expect.element('article.article-3 h1').text.to.equal(result.value); 74 | }) 75 | .getText('article.article-3 h1', (result) => { 76 | browser.click('article.article-3 .edit-article'); 77 | browser.expect.element('article.article-3 h1').text.to.not.equal(result.value); 78 | }) 79 | .getText('article.article-3 p.content', (result) => { 80 | browser.expect.element('article.article-3 p.content').text.to.equal(result.value); 81 | }) 82 | .getText('article.article-3 p.content', (result) => { 83 | browser.click('article.article-3 .edit-article'); 84 | browser.expect.element('article.article-3 p.content').text.to.not.equal(result.value); 85 | }) 86 | 87 | // Replace Single Tests 88 | .getText('article.article-3 h1', (result) => { 89 | browser.expect.element('article.article-3 h1').text.to.equal(result.value); 90 | }) 91 | .getText('article.article-3 h1', (result) => { 92 | browser.click('article.article-3 .replace-article'); 93 | browser.expect.element('article.article-3 h1').text.to.not.equal(result.value); 94 | }) 95 | .getText('article.article-3 p.content', (result) => { 96 | browser.expect.element('article.article-3 p.content').text.to.equal(result.value); 97 | }) 98 | .getText('article.article-3 p.content', (result) => { 99 | browser.click('article.article-3 .replace-article'); 100 | browser.expect.element('article.article-3 p.content').text.to.not.equal(result.value); 101 | }) 102 | 103 | // Delete Single Test 104 | .click('article.article-3 .delete-article') 105 | .assert.elementNotPresent('article.article-3') 106 | .click('.back a') 107 | .waitForElementVisible('#articles', 1000) 108 | .assert.elementNotPresent('article.article-3') 109 | 110 | // Single fetch tests 111 | .url('http://localhost:8080/blog/#/articles/1') 112 | .refresh() 113 | .waitForElementVisible('#article', 1000) 114 | 115 | // Update Single Tests 116 | .getText('article.article-1 h1', (result) => { 117 | browser.expect.element('article.article-1 h1').text.to.equal(result.value); 118 | }) 119 | .getText('article.article-1 h1', (result) => { 120 | browser.click('article.article-1 .edit-article'); 121 | browser.expect.element('article.article-1 h1').text.to.not.equal(result.value); 122 | }) 123 | .getText('article.article-1 p.content', (result) => { 124 | browser.expect.element('article.article-1 p.content').text.to.equal(result.value); 125 | }) 126 | .getText('article.article-1 p.content', (result) => { 127 | browser.click('article.article-1 .edit-article'); 128 | browser.expect.element('article.article-1 p.content').text.to.not.equal(result.value); 129 | }) 130 | 131 | // Replace Single Tests 132 | .getText('article.article-1 h1', (result) => { 133 | browser.expect.element('article.article-1 h1').text.to.equal(result.value); 134 | }) 135 | .getText('article.article-1 h1', (result) => { 136 | browser.click('article.article-1 .replace-article'); 137 | browser.expect.element('article.article-1 h1').text.to.not.equal(result.value); 138 | }) 139 | .getText('article.article-1 p.content', (result) => { 140 | browser.expect.element('article.article-1 p.content').text.to.equal(result.value); 141 | }) 142 | .getText('article.article-1 p.content', (result) => { 143 | browser.click('article.article-1 .replace-article'); 144 | browser.expect.element('article.article-1 p.content').text.to.not.equal(result.value); 145 | }) 146 | 147 | // Delete Single Test 148 | .click('article.article-1 .delete-article') 149 | .assert.elementNotPresent('article.article-1') 150 | .click('.back a') 151 | .waitForElementVisible('#vue-app', 1000) 152 | .assert.elementNotPresent('#articles') 153 | .click('button.fetch-articles') 154 | .waitForElementVisible('#articles', 1000) 155 | .assert.elementNotPresent('article.article-1') 156 | .assert.elementPresent('article.article-2') 157 | .assert.elementNotPresent('article.article-3') 158 | .assert.elementNotPresent('article.article-4') 159 | 160 | .end(); 161 | } 162 | }; 163 | -------------------------------------------------------------------------------- /test/unit/vuex-crud/createGetters.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Vue from 'vue'; 3 | import Vuex from 'vuex'; 4 | 5 | import createGetters from '../../../src/vuex-crud/createGetters'; 6 | 7 | test('has list getter', (t) => { 8 | const { list } = createGetters(); 9 | 10 | t.truthy(list); 11 | }); 12 | 13 | test('has byId getter', (t) => { 14 | const { byId } = createGetters(); 15 | 16 | t.truthy(byId); 17 | }); 18 | 19 | test('has isLoading getter', (t) => { 20 | const { isLoading } = createGetters(); 21 | 22 | t.truthy(isLoading); 23 | }); 24 | 25 | test('has isError getter', (t) => { 26 | const { isError } = createGetters(); 27 | 28 | t.truthy(isError); 29 | }); 30 | 31 | // List 32 | 33 | test('returns list of resources', (t) => { 34 | const state = { 35 | list: [ 36 | '1', '2', '3' 37 | ], 38 | entities: { 39 | 1: { 40 | id: 1, 41 | name: 'John' 42 | }, 43 | 2: { 44 | id: 2, 45 | name: 'Bob' 46 | }, 47 | 3: { 48 | id: 3, 49 | name: 'Jane' 50 | } 51 | } 52 | }; 53 | 54 | const { list } = createGetters(); 55 | 56 | t.deepEqual(list(state), [ 57 | { 58 | id: 1, 59 | name: 'John' 60 | }, 61 | { 62 | id: 2, 63 | name: 'Bob' 64 | }, 65 | { 66 | id: 3, 67 | name: 'Jane' 68 | } 69 | ]); 70 | }); 71 | 72 | test('returns empty list when no reources in the state list', (t) => { 73 | const state = { 74 | list: [], 75 | entities: { 76 | 1: { 77 | id: 1, 78 | name: 'John' 79 | }, 80 | 2: { 81 | id: 2, 82 | name: 'Bob' 83 | }, 84 | 3: { 85 | id: 3, 86 | name: 'Jane' 87 | } 88 | } 89 | }; 90 | 91 | const { list } = createGetters({}, 'id'); 92 | 93 | t.deepEqual(list(state), []); 94 | }); 95 | 96 | // By ID 97 | 98 | test('returns a resource by ID', (t) => { 99 | const state = { 100 | entities: { 101 | 1: { 102 | id: 1, 103 | name: 'John' 104 | }, 105 | 2: { 106 | id: 2, 107 | name: 'Bob' 108 | }, 109 | 3: { 110 | id: 3, 111 | name: 'Jane' 112 | } 113 | } 114 | }; 115 | 116 | Vue.use(Vuex); 117 | 118 | const { getters } = new Vuex.Store({ 119 | state, 120 | getters: createGetters({ idAttribute: 'id' }) 121 | }); 122 | 123 | t.deepEqual(getters.byId(1), { id: 1, name: 'John' }); 124 | t.deepEqual(getters.byId(2), { id: 2, name: 'Bob' }); 125 | t.deepEqual(getters.byId(3), { id: 3, name: 'Jane' }); 126 | }); 127 | 128 | test('finds resource by slug', (t) => { 129 | const state = { 130 | entities: { 131 | 1: { 132 | slug: 1, 133 | name: 'John' 134 | }, 135 | 2: { 136 | slug: 2, 137 | name: 'Bob' 138 | }, 139 | 3: { 140 | slug: 3, 141 | name: 'Jane' 142 | } 143 | } 144 | }; 145 | 146 | Vue.use(Vuex); 147 | 148 | const { getters } = new Vuex.Store({ 149 | state, 150 | getters: createGetters({ idAttribute: 'slug' }) 151 | }); 152 | 153 | t.deepEqual(getters.byId(1), { slug: 1, name: 'John' }); 154 | t.deepEqual(getters.byId(2), { slug: 2, name: 'Bob' }); 155 | t.deepEqual(getters.byId(3), { slug: 3, name: 'Jane' }); 156 | }); 157 | 158 | // Loading 159 | 160 | test('returns true if isFetchingList is true', (t) => { 161 | const state = { 162 | isFetchingList: true, 163 | isFetchingSingle: false, 164 | isCreating: false, 165 | isUpdating: false, 166 | isReplacing: false, 167 | isDestroying: false 168 | }; 169 | 170 | Vue.use(Vuex); 171 | 172 | const { getters } = new Vuex.Store({ 173 | state, 174 | getters: createGetters() 175 | }); 176 | 177 | t.true(getters.isLoading); 178 | }); 179 | 180 | test('returns true if isFetchingSingle is true', (t) => { 181 | const state = { 182 | isFetchingList: false, 183 | isFetchingSingle: true, 184 | isCreating: false, 185 | isUpdating: false, 186 | isReplacing: false, 187 | isDestroying: false 188 | }; 189 | 190 | Vue.use(Vuex); 191 | 192 | const { getters } = new Vuex.Store({ 193 | state, 194 | getters: createGetters() 195 | }); 196 | 197 | t.true(getters.isLoading); 198 | }); 199 | 200 | test('returns true if isCreating is true', (t) => { 201 | const state = { 202 | isFetchingList: false, 203 | isFetchingSingle: false, 204 | isCreating: true, 205 | isUpdating: false, 206 | isReplacing: false, 207 | isDestroying: false 208 | }; 209 | 210 | Vue.use(Vuex); 211 | 212 | const { getters } = new Vuex.Store({ 213 | state, 214 | getters: createGetters() 215 | }); 216 | 217 | t.true(getters.isLoading); 218 | }); 219 | 220 | test('returns true if isUpdating is true', (t) => { 221 | const state = { 222 | isFetchingList: false, 223 | isFetchingSingle: false, 224 | isCreating: false, 225 | isUpdating: true, 226 | isReplacing: false, 227 | isDestroying: false 228 | }; 229 | 230 | Vue.use(Vuex); 231 | 232 | const { getters } = new Vuex.Store({ 233 | state, 234 | getters: createGetters() 235 | }); 236 | 237 | t.true(getters.isLoading); 238 | }); 239 | 240 | test('returns true if isReplacing is true', (t) => { 241 | const state = { 242 | isFetchingList: false, 243 | isFetchingSingle: false, 244 | isCreating: false, 245 | isUpdating: false, 246 | isReplacing: true, 247 | isDestroying: false 248 | }; 249 | 250 | Vue.use(Vuex); 251 | 252 | const { getters } = new Vuex.Store({ 253 | state, 254 | getters: createGetters() 255 | }); 256 | 257 | t.true(getters.isLoading); 258 | }); 259 | 260 | test('returns true if isDestroying is true', (t) => { 261 | const state = { 262 | isFetchingList: false, 263 | isFetchingSingle: false, 264 | isCreating: false, 265 | isUpdating: false, 266 | isReplacing: false, 267 | isDestroying: true 268 | }; 269 | 270 | Vue.use(Vuex); 271 | 272 | const { getters } = new Vuex.Store({ 273 | state, 274 | getters: createGetters() 275 | }); 276 | 277 | t.true(getters.isLoading); 278 | }); 279 | 280 | test('returns false not loading', (t) => { 281 | const state = { 282 | isFetchingList: false, 283 | isFetchingSingle: false, 284 | isCreating: false, 285 | isUpdating: false, 286 | isReplacing: false, 287 | isDestroying: false 288 | }; 289 | 290 | Vue.use(Vuex); 291 | 292 | const { getters } = new Vuex.Store({ 293 | state, 294 | getters: createGetters() 295 | }); 296 | 297 | t.false(getters.isLoading); 298 | }); 299 | 300 | // Error 301 | 302 | test('returns true if fetchListError is present', (t) => { 303 | const state = { 304 | fetchListError: {}, 305 | fetchSingleError: null, 306 | createError: null, 307 | updateError: null, 308 | replaceError: null, 309 | destroyError: null 310 | }; 311 | 312 | Vue.use(Vuex); 313 | 314 | const { getters } = new Vuex.Store({ 315 | state, 316 | getters: createGetters() 317 | }); 318 | 319 | t.true(getters.isError); 320 | }); 321 | 322 | test('returns true if fetchSingleError is present', (t) => { 323 | const state = { 324 | fetchListError: null, 325 | fetchSingleError: {}, 326 | createError: null, 327 | updateError: null, 328 | replaceError: null, 329 | destroyError: null 330 | }; 331 | 332 | Vue.use(Vuex); 333 | 334 | const { getters } = new Vuex.Store({ 335 | state, 336 | getters: createGetters() 337 | }); 338 | 339 | t.true(getters.isError); 340 | }); 341 | 342 | test('returns true if createError is present', (t) => { 343 | const state = { 344 | fetchListError: null, 345 | fetchSingleError: null, 346 | createError: {}, 347 | updateError: null, 348 | replaceError: null, 349 | destroyError: null 350 | }; 351 | 352 | Vue.use(Vuex); 353 | 354 | const { getters } = new Vuex.Store({ 355 | state, 356 | getters: createGetters() 357 | }); 358 | 359 | t.true(getters.isError); 360 | }); 361 | 362 | test('returns true if updateError is present', (t) => { 363 | const state = { 364 | fetchListError: null, 365 | fetchSingleError: null, 366 | createError: null, 367 | updateError: {}, 368 | replaceError: null, 369 | destroyError: null 370 | }; 371 | 372 | Vue.use(Vuex); 373 | 374 | const { getters } = new Vuex.Store({ 375 | state, 376 | getters: createGetters() 377 | }); 378 | 379 | t.true(getters.isError); 380 | }); 381 | 382 | test('returns true if replaceError is present', (t) => { 383 | const state = { 384 | fetchListError: null, 385 | fetchSingleError: null, 386 | createError: null, 387 | updateError: null, 388 | replaceError: {}, 389 | destroyError: null 390 | }; 391 | 392 | Vue.use(Vuex); 393 | 394 | const { getters } = new Vuex.Store({ 395 | state, 396 | getters: createGetters() 397 | }); 398 | 399 | t.true(getters.isError); 400 | }); 401 | 402 | test('returns true if destroyError is present', (t) => { 403 | const state = { 404 | fetchListError: null, 405 | fetchSingleError: null, 406 | createError: null, 407 | updateError: null, 408 | replaceError: null, 409 | destroyError: {} 410 | }; 411 | 412 | Vue.use(Vuex); 413 | 414 | const { getters } = new Vuex.Store({ 415 | state, 416 | getters: createGetters() 417 | }); 418 | 419 | t.true(getters.isError); 420 | }); 421 | 422 | test('returns false if there is no error', (t) => { 423 | const state = { 424 | fetchListError: null, 425 | fetchSingleError: null, 426 | createError: null, 427 | updateError: null, 428 | replaceError: null, 429 | destroyError: null 430 | }; 431 | 432 | Vue.use(Vuex); 433 | 434 | const { getters } = new Vuex.Store({ 435 | state, 436 | getters: createGetters() 437 | }); 438 | 439 | t.false(getters.isError); 440 | }); 441 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vuex-CRUD 2 | 3 | [![Build Status](https://travis-ci.org/JiriChara/vuex-crud.svg?branch=master)](https://travis-ci.org/JiriChara/vuex-crud) 4 | [![codecov](https://codecov.io/gh/JiriChara/vuex-crud/branch/master/graph/badge.svg)](https://codecov.io/gh/JiriChara/vuex-crud) 5 | [![npm](https://img.shields.io/npm/v/vuex-crud.svg)](https://www.npmjs.com/package/vuex-crud) 6 | [![npm](https://img.shields.io/npm/dm/vuex-crud.svg)](https://www.npmjs.com/package/vuex-crud) 7 | 8 | ## Introduction 9 | 10 | **Vuex-CRUD** is a library for Vuex which helps you to build CRUD modules easily. 11 | 12 | ## Installation 13 | 14 | ``` 15 | yarn add vuex-crud 16 | ``` 17 | 18 | OR 19 | 20 | ``` 21 | npm install vuex-crud 22 | ``` 23 | 24 | **Vuex-CRUD** uses `Array.prototype.includes`, `Object.assign` and `Promise`. Make sure hat your project use polyfill for those if you want to support older browsers! Look at here: https://babeljs.io/docs/usage/polyfill/ for more info. 25 | 26 | ## Basic Usage 27 | 28 | 1) Create your first CRUD resource: 29 | 30 | ```js 31 | // src/store/articles 32 | 33 | import createCrudModule from 'vuex-crud'; 34 | 35 | export default createCrudModule({ 36 | resource: 'articles' 37 | }); 38 | ``` 39 | 40 | 2) Register your CRUD resource in your store: 41 | 42 | ```js 43 | // src/store/index.js 44 | 45 | import Vue from 'vue'; 46 | import Vuex from 'vuex'; 47 | 48 | import articles from '@/store/articles'; 49 | 50 | Vue.use(Vuex); 51 | 52 | export default new Vuex.Store({ 53 | state: {}, 54 | 55 | modules: { 56 | articles 57 | } 58 | }); 59 | ``` 60 | 61 | 3) Use it in your components: 62 | 63 | ```vue 64 | 65 | 66 | 74 | 75 | 110 | ``` 111 | 112 | ```vue 113 | 114 | 115 | 123 | 124 | 165 | ``` 166 | 167 | ## Advanced Usage 168 | 169 | The following options are available when creating new Vuex-CRUD module: 170 | 171 | ```js 172 | import createCrudModule, { client } from 'vuex-crud'; 173 | 174 | export default createCrudModule({ 175 | resource: 'articles', // The name of your CRUD resource (mandatory) 176 | idAttribute: 'id', // What should be used as ID 177 | urlRoot: '/api/articles', // The url to fetch the resource 178 | state: {}, // Initial state. It will override state generated by Vuex-CRUD 179 | actions: {}, // Initial actions It will override actions generated by Vuex-CRUD 180 | mutations: {}, // Initial mutations. It will override mutations generated by Vuex-CRUD 181 | getters: {}, // Initial getters. It will override getters generated by Vuex-CRUD 182 | client, // Axios client that should be used for API request 183 | onFetchListStart: () => {}, // Callback for collection GET start 184 | onFetchListSuccess: () => {}, // Callback for collection GET success 185 | onFetchListError: () => {}, // Callback for collection GET error 186 | onFetchSingleStart: () => {}, // Callback for single GET start 187 | onFetchSingleSuccess: () => {}, // Callback for single GET success 188 | onFetchSingleError: () => {}, // Callback for single GET error 189 | onCreateStart: () => {}, // Callback for POST start 190 | onCreateSuccess: () => {}, // Callback for POST success 191 | onCreateError: () => {}, // Callback for POST error 192 | onUpdateStart: () => {}, // Callback for PATCH start 193 | onUpdateSuccess: () => {}, // Callback for PATCH success 194 | onUpdateError: () => {}, // Callback for PATCH error 195 | onReplaceStart: () => {}, // Callback for PUT start 196 | onReplaceSuccess: () => {}, // Callback for PUT success 197 | onReplaceError: () => {}, // Callback for PUT error 198 | onDestroyStart: () => {}, // Callback for DELETE start 199 | onDestroySuccess: () => {}, // Callback for DELETE success 200 | onDestroyError: () => {}, // Callback for DELETE error 201 | only: [ 202 | 'FETCH_LIST', 203 | 'FETCH_SINGLE', 204 | 'CREATE', 205 | 'UPDATE', 206 | 'REPLACE', 207 | 'DESTROY' 208 | ], // What CRUD actions should be available 209 | parseList: res => res, // Method used to parse collection 210 | parseSingle: res => res, // Method used to parse single resource 211 | parseError: res => res // Method used to parse error 212 | }); 213 | ``` 214 | 215 | ### Nested Resources 216 | 217 | **Vuex-CRUD** is designed mainly for flatten APIs like: 218 | 219 | ``` 220 | /api/articles/ 221 | /api/users/1 222 | /api/pages?byBook=1 223 | ``` 224 | 225 | but it also supports nested resources like: 226 | 227 | ``` 228 | /api/books/1/pages/10 229 | /api/users/john/tasks/15 230 | ``` 231 | 232 | However your store will always be flattened and will look similar to this: 233 | 234 | ```js 235 | { 236 | books: { 237 | entities: { 238 | '1': { 239 | // ... 240 | } 241 | } 242 | }, 243 | pages: { 244 | entities: { 245 | '1': { 246 | // ... 247 | }, 248 | '2': { 249 | // ... 250 | }, 251 | '3': { 252 | // ... 253 | } 254 | }, 255 | list: ['1', '2', '3'] 256 | }, 257 | } 258 | ``` 259 | 260 | There are 2 possible ways to implement provide custom URL: 261 | 262 | 1) Provide custom url for each request: 263 | 264 | ```js 265 | fetchList({ customUrl: '/api/books/1/pages' }); 266 | fetchSingle({ customUrl: '/api/books/1/pages/1' }); 267 | create({ data: { content: '...' }, customUrl: '/api/books/1/pages' }); 268 | update({ data: { content: '...' }, customUrl: '/api/books/1/pages/1' }); 269 | replace({ data: { content: '...' }, customUrl: '/api/books/1/pages/1' }); 270 | destroy({ customUrl: '/api/books/1/pages/1' }); 271 | ``` 272 | 273 | 2) Define a getter for custom url: 274 | 275 | ```js 276 | import createCrudModule from 'vuex-crud'; 277 | 278 | export default createCrudModule({ 279 | resource: 'pages', 280 | customUrlFn(id, type, bookId) { 281 | // id will only be available when doing request to single resource, otherwise null 282 | // type is the actions you are dispatching: FETCH_LIST, FETCH_SINGLE, CREATE, UPDATE, REPLACE, DESTROY 283 | const rootUrl = `/api/books/${bookId}`; 284 | return id ? `rootUrl/${id}` : rootUrl; 285 | } 286 | }); 287 | ``` 288 | 289 | and your requests will look this: 290 | 291 | ```js 292 | const id = 2; 293 | const bookId = 1; 294 | 295 | fetchList({ customUrlFnArgs: bookId }); 296 | fetchSingle({ id, customUrlFnArgs: bookId }); 297 | create({ data: { content: '...' }, customUrlFnArgs: bookId }); 298 | update({ id, data: { content: '...' }, customUrlFnArgs: bookId }); 299 | replace({ id, data: { content: '...' }, customUrlFnArgs: bookId }); 300 | destroy({ id, customUrlFnArgs: bookId }); 301 | ``` 302 | 303 | 304 | ### Custom client 305 | 306 | **Vuex-CRUD** is using axios for API requests. If you want to customize interceptors or something else, then you can do following: 307 | 308 | ```js 309 | import createCrudModule, { client } from 'vuex-crud'; 310 | import authInterceptor from './authInterceptor'; 311 | 312 | client.interceptors.request.use(authInterceptor); 313 | 314 | createCrudModule({ 315 | resource: 'comments', 316 | client 317 | }); 318 | ``` 319 | 320 | ### Parsing Data from Your API 321 | 322 | You can provide a custom methods to parse data from your API: 323 | 324 | ```js 325 | import createCrudModule from 'vuex-crud'; 326 | 327 | createCrudModule({ 328 | resource: 'articles', 329 | 330 | parseList(response) { 331 | const { data } = response; 332 | 333 | return Object.assign({}, response, { 334 | data: data.result // expecting array of objects with IDs 335 | }); 336 | }, 337 | 338 | parseSingle(response) { 339 | const { data } = response; 340 | 341 | return Object.assign({}, response, { 342 | data: data.result // expecting object with ID 343 | }); 344 | } 345 | }); 346 | ``` 347 | 348 | ### Getters 349 | 350 | Vuex-CRUD ships with following getters: 351 | 352 | * `list()` returns list of resources 353 | * `byId(id)` returns resource by ID 354 | 355 | ### Actions 356 | 357 | Vuex-CRUD ships with following actions (config is configuration for axios): 358 | 359 | ```js 360 | { 361 | // GET /api/ 362 | fetchList({ commit }, { config } = {}) { 363 | // ... 364 | }, 365 | 366 | // GET /api//:id 367 | fetchSingle({ commit }, { id, config } = {}) { 368 | // ... 369 | }, 370 | 371 | // POST /api/ 372 | create({ commit }, { data, config } = {}) { 373 | // ... 374 | }, 375 | 376 | // PATCH /api//:id 377 | update({ commit }, { id, data, config } = {}) { 378 | // ... 379 | }, 380 | 381 | // PUT /api//:id 382 | replace({ commit }, { id, data, config } = {}) { 383 | // ... 384 | }, 385 | 386 | // DELETE /api//:id 387 | destroy({ commit }, { id, config } = {}) { 388 | // ... 389 | }, 390 | } 391 | ``` 392 | 393 | ## Usage with Nuxt 394 | 395 | `vuex-crud` works with Nuxt modules system as well. You can simply define your Nuxt modules as following: 396 | 397 | ```js 398 | import createCRUDModule from 'vuex-crud'; 399 | 400 | const crudModule = createCRUDModule({ 401 | resource: 'articles' 402 | }); 403 | 404 | const state = () => crudModule.state; 405 | 406 | const { actions, mutations, getters } = crudModule; 407 | 408 | export { 409 | state, 410 | actions, 411 | mutations, 412 | getters 413 | }; 414 | ``` 415 | 416 | and then use it in your component: 417 | 418 | ```js 419 | export default { 420 | computed: { 421 | ...mapGetters('articles', { 422 | articleList: 'list' 423 | }) 424 | }, 425 | 426 | fetch({ store }) { 427 | store.dispatch('articles/fetchList'); 428 | } 429 | }; 430 | ``` 431 | 432 | ## License 433 | 434 | The MIT License (MIT) - See file 'LICENSE' in this project 435 | 436 | ## Copyright 437 | 438 | Copyright © 2017 Jiří Chára. All Rights Reserved. 439 | -------------------------------------------------------------------------------- /test/unit/index.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | 4 | import createCrud, { client } from '../../src'; 5 | import * as createStateObj from '../../src/vuex-crud/createState'; 6 | import * as createActionsObj from '../../src/vuex-crud/createActions'; 7 | import * as createMutationsObj from '../../src/vuex-crud/createMutations'; 8 | import * as createGettersObj from '../../src/vuex-crud/createGetters'; 9 | import clientImpl from '../../src/vuex-crud/client'; 10 | 11 | const createState = createStateObj.default; 12 | const createActions = createActionsObj.default; 13 | const createMutations = createMutationsObj.default; 14 | const createGetters = createGettersObj.default; 15 | 16 | test('creates namespaced module', (t) => { 17 | t.true(createCrud({ resource: 'articles' }).namespaced); 18 | }); 19 | 20 | test('exports client', (t) => { 21 | t.is(client, clientImpl); 22 | }); 23 | 24 | test('throws an error if resource name not specified', (t) => { 25 | const error = t.throws(() => createCrud(), Error); 26 | 27 | t.is(error.message, 'Resource name must be specified'); 28 | }); 29 | 30 | test('creates state', (t) => { 31 | t.deepEqual( 32 | createCrud({ resource: 'articles' }).state, 33 | createState({ 34 | state: {}, 35 | only: ['FETCH_LIST', 'FETCH_SINGLE', 'CREATE', 'UPDATE', 'REPLACE', 'DESTROY'] 36 | }) 37 | ); 38 | }); 39 | 40 | test('creates state with given options', (t) => { 41 | t.deepEqual( 42 | createCrud({ resource: 'articles', only: ['CREATE'], state: { foo: 'bar' } }).state, 43 | createState({ only: ['CREATE'], state: { foo: 'bar' } }) 44 | ); 45 | }); 46 | 47 | test('calls createState with correct arguments', (t) => { 48 | const spy = sinon.spy(createStateObj, 'default'); 49 | const resource = 'foo'; 50 | const only = ['CREATE']; 51 | const state = { foo: 'bar' }; 52 | 53 | const crud = createCrud({ resource, only, state }).state; 54 | 55 | const arg = spy.getCalls(0)[0].args[0]; 56 | 57 | t.truthy(crud); // eslint 58 | t.truthy(spy.called); 59 | 60 | t.is(arg.state, state); 61 | t.is(arg.only, only); 62 | 63 | createStateObj.default.restore(); 64 | }); 65 | 66 | test('creates actions', (t) => { 67 | const crudActions = createCrud({ resource: 'articles' }).actions; 68 | const actions = createActions({ 69 | actions: {}, 70 | urlRoot: '/api/articles', 71 | only: ['FETCH_LIST', 'FETCH_SINGLE', 'CREATE', 'UPDATE', 'REPLACE', 'DESTROY'], 72 | clientImpl 73 | }); 74 | 75 | t.is(crudActions.fetchList.toString(), actions.fetchList.toString()); 76 | t.is(crudActions.fetchSingle.toString(), actions.fetchSingle.toString()); 77 | t.is(crudActions.create.toString(), actions.create.toString()); 78 | t.is(crudActions.update.toString(), actions.update.toString()); 79 | t.is(crudActions.replace.toString(), actions.replace.toString()); 80 | t.is(crudActions.destroy.toString(), actions.destroy.toString()); 81 | 82 | t.is(JSON.stringify(crudActions), JSON.stringify(actions)); 83 | }); 84 | 85 | test('creates actions with given options', (t) => { 86 | const customAction = () => null; 87 | const customClient = () => null; 88 | 89 | const crudActions = createCrud({ 90 | resource: 'articles', 91 | actions: { 92 | customAction 93 | }, 94 | rootUrl: '/articles', 95 | only: ['FETCH_LIST'], 96 | client: customClient 97 | }).actions; 98 | 99 | const actions = createActions({ 100 | actions: { 101 | customAction 102 | }, 103 | urlRoot: '/articles', 104 | only: ['FETCH_LIST'], 105 | customClient 106 | }); 107 | 108 | t.is(crudActions.fetchList.toString(), actions.fetchList.toString()); 109 | t.falsy(crudActions.fetchSingle); 110 | t.falsy(crudActions.create); 111 | t.falsy(crudActions.update); 112 | t.falsy(crudActions.replace); 113 | t.falsy(crudActions.destroy); 114 | 115 | t.is(JSON.stringify(crudActions), JSON.stringify(actions)); 116 | }); 117 | 118 | test('calls createActions with correct arguments', (t) => { 119 | const spy = sinon.spy(createActionsObj, 'default'); 120 | 121 | const actions = {}; 122 | const customClient = () => null; 123 | const only = ['FETCH_LIST']; 124 | const parseList = res => res; 125 | const parseSingle = res => res; 126 | const parseError = err => err; 127 | 128 | const crud = createCrud({ 129 | resource: 'articles', 130 | actions, 131 | urlRoot: '/articles', 132 | only, 133 | client: customClient, 134 | parseList, 135 | parseSingle, 136 | parseError 137 | }).actions; 138 | 139 | const arg = spy.getCalls(0)[0].args[0]; 140 | 141 | t.truthy(crud); // eslint 142 | t.truthy(spy.called); 143 | 144 | t.is(arg.actions, actions); 145 | t.is(arg.rootUrl, '/articles'); 146 | t.is(arg.only, only); 147 | t.is(arg.client, customClient); 148 | 149 | createActionsObj.default.restore(); 150 | }); 151 | 152 | test('calls createActions with correct arguments when customUrlFn provided', (t) => { 153 | const spy = sinon.spy(createActionsObj, 'default'); 154 | 155 | const actions = {}; 156 | const customClient = () => null; 157 | const only = ['FETCH_LIST']; 158 | const parseList = res => res; 159 | const parseSingle = res => res; 160 | const parseError = err => err; 161 | const customUrlFn = id => ( 162 | id ? '/api/foo' : `/api/foo/${id}` 163 | ); 164 | 165 | const crud = createCrud({ 166 | resource: 'articles', 167 | actions, 168 | urlRoot: '/articles', 169 | customUrlFn, 170 | only, 171 | client: customClient, 172 | parseList, 173 | parseSingle, 174 | parseError 175 | }).actions; 176 | 177 | const arg = spy.getCalls(0)[0].args[0]; 178 | 179 | t.truthy(crud); // eslint 180 | t.truthy(spy.called); 181 | 182 | t.is(arg.actions, actions); 183 | t.is(arg.rootUrl, customUrlFn); 184 | t.is(arg.only, only); 185 | t.is(arg.client, customClient); 186 | 187 | createActionsObj.default.restore(); 188 | }); 189 | 190 | test('removes trailing slash from url', (t) => { 191 | const spy = sinon.spy(createActionsObj, 'default'); 192 | 193 | const crud = createCrud({ 194 | resource: 'articles', 195 | urlRoot: '/articles/' 196 | }).actions; 197 | 198 | const arg = spy.getCalls(0)[0].args[0]; 199 | 200 | t.truthy(crud); // eslint 201 | t.truthy(spy.called); 202 | 203 | t.is(arg.rootUrl, '/articles'); 204 | 205 | createActionsObj.default.restore(); 206 | }); 207 | 208 | test('creates mutations', (t) => { 209 | const crudMutations = createCrud({ resource: 'articles' }).mutations; 210 | const mutations = createMutations({ 211 | mutations: {}, 212 | idAttribute: 'id', 213 | only: ['FETCH_LIST', 'FETCH_SINGLE', 'CREATE', 'UPDATE', 'REPLACE', 'DESTROY'], 214 | onFetchListStart() {}, 215 | onFetchListSuccess() {}, 216 | onFetchListError() {}, 217 | onFetchSingleStart() {}, 218 | onFetchSingleSuccess() {}, 219 | onFetchSingleError() {}, 220 | onCreateStart() {}, 221 | onCreateSuccess() {}, 222 | onCreateError() {}, 223 | onUpdateStart() {}, 224 | onUpdateSuccess() {}, 225 | onUpdateError() {}, 226 | onReplaceStart() {}, 227 | onReplaceSuccess() {}, 228 | onReplaceError() {}, 229 | onDestroyStart() {}, 230 | onDestroySuccess() {}, 231 | onDestroyError() {} 232 | }); 233 | 234 | t.is(crudMutations.fetchListStart.toString(), mutations.fetchListStart.toString()); 235 | t.is(crudMutations.fetchListSuccess.toString(), mutations.fetchListSuccess.toString()); 236 | t.is(crudMutations.fetchListError.toString(), mutations.fetchListError.toString()); 237 | 238 | t.is(crudMutations.fetchSingleStart.toString(), mutations.fetchSingleStart.toString()); 239 | t.is(crudMutations.fetchSingleSuccess.toString(), mutations.fetchSingleSuccess.toString()); 240 | t.is(crudMutations.fetchSingleError.toString(), mutations.fetchSingleError.toString()); 241 | 242 | t.is(crudMutations.createStart.toString(), mutations.createStart.toString()); 243 | t.is(crudMutations.createSuccess.toString(), mutations.createSuccess.toString()); 244 | t.is(crudMutations.createError.toString(), mutations.createError.toString()); 245 | 246 | t.is(crudMutations.updateStart.toString(), mutations.updateStart.toString()); 247 | t.is(crudMutations.updateSuccess.toString(), mutations.updateSuccess.toString()); 248 | t.is(crudMutations.updateError.toString(), mutations.updateError.toString()); 249 | 250 | t.is(crudMutations.replaceStart.toString(), mutations.replaceStart.toString()); 251 | t.is(crudMutations.replaceSuccess.toString(), mutations.replaceSuccess.toString()); 252 | t.is(crudMutations.replaceError.toString(), mutations.replaceError.toString()); 253 | 254 | t.is(crudMutations.destroyStart.toString(), mutations.destroyStart.toString()); 255 | t.is(crudMutations.destroySuccess.toString(), mutations.destroySuccess.toString()); 256 | t.is(crudMutations.destroyError.toString(), mutations.destroyError.toString()); 257 | 258 | t.is(JSON.stringify(crudMutations), JSON.stringify(mutations)); 259 | }); 260 | 261 | test('creates mutations with given options', (t) => { 262 | const customMutations = { 263 | foo() {} 264 | }; 265 | 266 | const crudMutations = createCrud({ 267 | resource: 'articles', 268 | mutations: customMutations, 269 | idAttribute: 'slug', 270 | only: ['CREATE'] 271 | }).mutations; 272 | 273 | const mutations = createMutations({ 274 | mutations: customMutations, 275 | idAttribute: 'slug', 276 | only: ['CREATE'], 277 | onFetchListStart() {}, 278 | onFetchListSuccess() {}, 279 | onFetchListError() {}, 280 | onFetchSingleStart() {}, 281 | onFetchSingleSuccess() {}, 282 | onFetchSingleError() {}, 283 | onCreateStart() {}, 284 | onCreateSuccess() {}, 285 | onCreateError() {}, 286 | onUpdateStart() {}, 287 | onUpdateSuccess() {}, 288 | onUpdateError() {}, 289 | onReplaceStart() {}, 290 | onReplaceSuccess() {}, 291 | onReplaceError() {}, 292 | onDestroyStart() {}, 293 | onDestroySuccess() {}, 294 | onDestroyError() {} 295 | }); 296 | 297 | t.truthy(customMutations.foo); 298 | 299 | t.falsy(crudMutations.fetchListStart); 300 | t.falsy(crudMutations.fetchListSuccess); 301 | t.falsy(crudMutations.fetchListError); 302 | 303 | t.falsy(crudMutations.fetchSingleStart); 304 | t.falsy(crudMutations.fetchSingleSuccess); 305 | t.falsy(crudMutations.fetchSingleError); 306 | 307 | t.is(crudMutations.createStart.toString(), mutations.createStart.toString()); 308 | t.is(crudMutations.createSuccess.toString(), mutations.createSuccess.toString()); 309 | t.is(crudMutations.createError.toString(), mutations.createError.toString()); 310 | 311 | t.falsy(crudMutations.updateStart); 312 | t.falsy(crudMutations.updateSuccess); 313 | t.falsy(crudMutations.updateError); 314 | 315 | t.falsy(crudMutations.replaceStart); 316 | t.falsy(crudMutations.replaceSuccess); 317 | t.falsy(crudMutations.replaceError); 318 | 319 | t.falsy(crudMutations.destroyStart); 320 | t.falsy(crudMutations.destroySuccess); 321 | t.falsy(crudMutations.destroyError); 322 | 323 | t.is(JSON.stringify(crudMutations), JSON.stringify(mutations)); 324 | }); 325 | 326 | test('calls createMutations with correct arguments', (t) => { 327 | const spy = sinon.spy(createMutationsObj, 'default'); 328 | 329 | const onFetchListStart = () => {}; 330 | const onFetchListSuccess = () => {}; 331 | const onFetchListError = () => {}; 332 | const onFetchSingleStart = () => {}; 333 | const onFetchSingleSuccess = () => {}; 334 | const onFetchSingleError = () => {}; 335 | const onCreateStart = () => {}; 336 | const onCreateSuccess = () => {}; 337 | const onCreateError = () => {}; 338 | const onUpdateStart = () => {}; 339 | const onUpdateSuccess = () => {}; 340 | const onUpdateError = () => {}; 341 | const onReplaceStart = () => {}; 342 | const onReplaceSuccess = () => {}; 343 | const onReplaceError = () => {}; 344 | const onDestroyStart = () => {}; 345 | const onDestroySuccess = () => {}; 346 | const onDestroyError = () => {}; 347 | 348 | const customMutations = { 349 | foo() {} 350 | }; 351 | 352 | const only = ['CREATE']; 353 | 354 | const crud = createCrud({ 355 | resource: 'articles', 356 | mutations: customMutations, 357 | idAttribute: 'slug', 358 | only, 359 | onFetchListStart, 360 | onFetchListSuccess, 361 | onFetchListError, 362 | onFetchSingleStart, 363 | onFetchSingleSuccess, 364 | onFetchSingleError, 365 | onCreateStart, 366 | onCreateSuccess, 367 | onCreateError, 368 | onUpdateStart, 369 | onUpdateSuccess, 370 | onUpdateError, 371 | onReplaceStart, 372 | onReplaceSuccess, 373 | onReplaceError, 374 | onDestroyStart, 375 | onDestroySuccess, 376 | onDestroyError 377 | }); 378 | 379 | const arg = spy.getCalls(0)[0].args[0]; 380 | 381 | t.truthy(crud); // eslint 382 | t.truthy(spy.called); 383 | 384 | t.is(arg.mutations, customMutations); 385 | t.is(arg.only, only); 386 | t.is(arg.idAttribute, 'slug'); 387 | t.is(arg.onFetchListStart, onFetchListStart); 388 | t.is(arg.onFetchListSuccess, onFetchListSuccess); 389 | t.is(arg.onFetchListError, onFetchListError); 390 | t.is(arg.onFetchSingleStart, onFetchSingleStart); 391 | t.is(arg.onFetchSingleSuccess, onFetchSingleSuccess); 392 | t.is(arg.onFetchSingleError, onFetchSingleError); 393 | t.is(arg.onCreateStart, onCreateStart); 394 | t.is(arg.onCreateSuccess, onCreateSuccess); 395 | t.is(arg.onCreateError, onCreateError); 396 | t.is(arg.onUpdateStart, onUpdateStart); 397 | t.is(arg.onUpdateSuccess, onUpdateSuccess); 398 | t.is(arg.onUpdateError, onUpdateError); 399 | t.is(arg.onReplaceStart, onReplaceStart); 400 | t.is(arg.onReplaceSuccess, onReplaceSuccess); 401 | t.is(arg.onReplaceError, onReplaceError); 402 | t.is(arg.onDestroyStart, onDestroyStart); 403 | t.is(arg.onDestroySuccess, onDestroySuccess); 404 | t.is(arg.onDestroyError, onDestroyError); 405 | 406 | createMutationsObj.default.restore(); 407 | }); 408 | 409 | test('creates getters', (t) => { 410 | const crudGetters = createCrud({ resource: 'articles' }).getters; 411 | 412 | const getters = createGetters({ 413 | getters: {}, 414 | idAttribute: 'id' 415 | }); 416 | 417 | t.is(crudGetters.list.toString(), getters.list.toString()); 418 | t.is(crudGetters.byId.toString(), getters.byId.toString()); 419 | t.is(crudGetters.isError.toString(), getters.isError.toString()); 420 | t.is(crudGetters.isLoading.toString(), getters.isLoading.toString()); 421 | 422 | t.is(JSON.stringify(crudGetters), JSON.stringify(getters)); 423 | }); 424 | 425 | test('creates getters with given options', (t) => { 426 | const customGetters = { 427 | foo() {} 428 | }; 429 | 430 | const crudGetters = createCrud({ 431 | resource: 'articles', 432 | getters: customGetters, 433 | idAttribute: 'slug' 434 | }).getters; 435 | 436 | const getters = createGetters({ 437 | getters: customGetters, 438 | idAttribute: 'slug' 439 | }); 440 | 441 | t.is(crudGetters.list.toString(), getters.list.toString()); 442 | t.is(crudGetters.byId.toString(), getters.byId.toString()); 443 | t.is(crudGetters.isError.toString(), getters.isError.toString()); 444 | t.is(crudGetters.isLoading.toString(), getters.isLoading.toString()); 445 | t.is(crudGetters.foo, customGetters.foo); 446 | 447 | t.is(JSON.stringify(crudGetters), JSON.stringify(getters)); 448 | }); 449 | 450 | test('calls createGetters with correct arguments', (t) => { 451 | const spy = sinon.spy(createGettersObj, 'default'); 452 | 453 | const customGetters = { 454 | foo() {} 455 | }; 456 | 457 | const crud = createCrud({ 458 | resource: 'articles', 459 | getters: customGetters, 460 | idAttribute: 'slug' 461 | }).getters; 462 | 463 | 464 | const arg = spy.getCalls(0)[0].args[0]; 465 | 466 | t.truthy(crud); // eslint 467 | t.truthy(spy.called); 468 | 469 | t.is(arg.getters, customGetters); 470 | 471 | createGettersObj.default.restore(); 472 | }); 473 | -------------------------------------------------------------------------------- /test/unit/vuex-crud/createActions.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | 4 | import client from '../fakeClient'; 5 | import createActions from '../../../src/vuex-crud/createActions'; 6 | 7 | test.beforeEach(() => { 8 | client.successResponse = {}; 9 | client.errorResponse = {}; 10 | client.isSuccessful = true; 11 | }); 12 | 13 | // Fetch List 14 | 15 | test('creates actions with fetchList method', (t) => { 16 | const actions = createActions({ 17 | only: ['FETCH_LIST'], 18 | parseList: res => res, 19 | parseSingle: res => res, 20 | parseError: res => res 21 | }); 22 | 23 | t.truthy(actions.fetchList); 24 | 25 | t.falsy(actions.fetchSingle); 26 | t.falsy(actions.create); 27 | t.falsy(actions.update); 28 | t.falsy(actions.replace); 29 | t.falsy(actions.destroy); 30 | }); 31 | 32 | test('fetchList commits fetchListStart', (t) => { 33 | const { fetchList } = createActions({ 34 | only: ['FETCH_LIST'], 35 | client, 36 | parseList: res => res, 37 | parseSingle: res => res, 38 | parseError: res => res 39 | }); 40 | 41 | const commit = sinon.spy(); 42 | 43 | fetchList({ commit }); 44 | 45 | t.true(commit.calledWith('fetchListStart')); 46 | }); 47 | 48 | test.cb('fetchList commits fetchListSuccess', (t) => { 49 | const { fetchList } = createActions({ 50 | only: ['FETCH_LIST'], 51 | client, 52 | parseList: res => res, 53 | parseSingle: res => res, 54 | parseError: res => res 55 | }); 56 | 57 | const commit = sinon.spy(); 58 | 59 | t.plan(2); 60 | 61 | fetchList({ commit }).then(() => { 62 | const { args } = commit.getCalls()[1]; 63 | 64 | t.is(args[0], 'fetchListSuccess'); 65 | t.deepEqual(args[1], client.successResponse); 66 | 67 | t.end(); 68 | }); 69 | }); 70 | 71 | test.cb('fetchList commits fetchListError', (t) => { 72 | client.isSuccessful = false; 73 | 74 | const { fetchList } = createActions({ 75 | only: ['FETCH_LIST'], 76 | client, 77 | parseList: res => res, 78 | parseSingle: res => res, 79 | parseError: res => res 80 | }); 81 | 82 | const commit = sinon.spy(); 83 | 84 | t.plan(2); 85 | 86 | fetchList({ commit }).catch(() => { 87 | const { args } = commit.getCalls()[1]; 88 | 89 | t.is(args[0], 'fetchListError'); 90 | t.deepEqual(args[1], client.errorResponse); 91 | 92 | t.end(); 93 | }); 94 | }); 95 | 96 | test('calls get with correct arguments', (t) => { 97 | const { fetchList } = createActions({ 98 | rootUrl: '/articles', 99 | only: ['FETCH_LIST'], 100 | client, 101 | parseList: res => res, 102 | parseSingle: res => res, 103 | parseError: res => res 104 | }); 105 | 106 | const config = { foo: 'bar' }; 107 | 108 | const commit = sinon.spy(); 109 | const spy = sinon.spy(client, 'get'); 110 | 111 | fetchList({ commit }, { config }); 112 | 113 | t.true(spy.calledWith('/articles', config)); 114 | 115 | client.get.restore(); 116 | }); 117 | 118 | test('fetch list supports customUrl', (t) => { 119 | const { fetchList } = createActions({ 120 | rootUrl: '/articles', 121 | only: ['FETCH_LIST'], 122 | client, 123 | parseList: res => res, 124 | parseSingle: res => res, 125 | parseError: res => res 126 | }); 127 | 128 | const config = { foo: 'bar' }; 129 | 130 | const commit = sinon.spy(); 131 | const spy = sinon.spy(client, 'get'); 132 | 133 | fetchList({ commit }, { config, customUrl: '/custom-articles' }); 134 | 135 | t.true(spy.calledWith('/custom-articles', config)); 136 | 137 | client.get.restore(); 138 | }); 139 | 140 | test('fetch list supports customUrlFnArgs', (t) => { 141 | const { fetchList } = createActions({ 142 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles`; }, 143 | only: ['FETCH_LIST'], 144 | client, 145 | parseList: res => res, 146 | parseSingle: res => res, 147 | parseError: res => res 148 | }); 149 | 150 | const config = { foo: 'bar' }; 151 | 152 | const commit = sinon.spy(); 153 | const spy = sinon.spy(client, 'get'); 154 | 155 | fetchList({ commit }, { config, customUrlFnArgs: '123' }); 156 | 157 | t.true(spy.calledWith('/users/123/articles', config)); 158 | 159 | client.get.restore(); 160 | }); 161 | 162 | test('fetch list supports customUrlFnArgs as array', (t) => { 163 | const { fetchList } = createActions({ 164 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles`; }, 165 | only: ['FETCH_LIST'], 166 | client, 167 | parseList: res => res, 168 | parseSingle: res => res, 169 | parseError: res => res 170 | }); 171 | 172 | const config = { foo: 'bar' }; 173 | 174 | const commit = sinon.spy(); 175 | const spy = sinon.spy(client, 'get'); 176 | 177 | fetchList({ commit }, { config, customUrlFnArgs: ['123'] }); 178 | 179 | t.true(spy.calledWith('/users/123/articles', config)); 180 | 181 | client.get.restore(); 182 | }); 183 | 184 | // Fetch Single 185 | 186 | test('creates actions with fetchSingle method', (t) => { 187 | const actions = createActions({ 188 | only: ['FETCH_SINGLE'], 189 | parseList: res => res, 190 | parseSingle: res => res, 191 | parseError: res => res 192 | }); 193 | 194 | t.truthy(actions.fetchSingle); 195 | 196 | t.falsy(actions.fetchList); 197 | t.falsy(actions.create); 198 | t.falsy(actions.update); 199 | t.falsy(actions.replace); 200 | t.falsy(actions.destroy); 201 | }); 202 | 203 | test('fetchSingle commits fetchSingleStart', (t) => { 204 | const { fetchSingle } = createActions({ 205 | only: ['FETCH_SINGLE'], 206 | client, 207 | parseList: res => res, 208 | parseSingle: res => res, 209 | parseError: res => res 210 | }); 211 | 212 | const commit = sinon.spy(); 213 | 214 | fetchSingle({ commit }); 215 | 216 | t.true(commit.calledWith('fetchSingleStart')); 217 | }); 218 | 219 | test.cb('fetchSingle commits fetchSingleSuccess', (t) => { 220 | const { fetchSingle } = createActions({ 221 | only: ['FETCH_SINGLE'], 222 | client, 223 | parseList: res => res, 224 | parseSingle: res => res, 225 | parseError: res => res 226 | }); 227 | 228 | const commit = sinon.spy(); 229 | 230 | t.plan(2); 231 | 232 | fetchSingle({ commit }).then(() => { 233 | const { args } = commit.getCalls()[1]; 234 | 235 | t.is(args[0], 'fetchSingleSuccess'); 236 | t.deepEqual(args[1], client.successResponse); 237 | 238 | t.end(); 239 | }); 240 | }); 241 | 242 | test.cb('fetchSingle commits fetchSingleError', (t) => { 243 | client.isSuccessful = false; 244 | 245 | const { fetchSingle } = createActions({ 246 | only: ['FETCH_SINGLE'], 247 | client, 248 | parseList: res => res, 249 | parseSingle: res => res, 250 | parseError: res => res 251 | }); 252 | 253 | const commit = sinon.spy(); 254 | 255 | t.plan(2); 256 | 257 | fetchSingle({ commit }).catch(() => { 258 | const { args } = commit.getCalls()[1]; 259 | 260 | t.is(args[0], 'fetchSingleError'); 261 | t.deepEqual(args[1], client.errorResponse); 262 | 263 | t.end(); 264 | }); 265 | }); 266 | 267 | test('calls get with correct arguments', (t) => { 268 | const { fetchSingle } = createActions({ 269 | rootUrl: '/articles', 270 | only: ['FETCH_SINGLE'], 271 | client, 272 | parseList: res => res, 273 | parseSingle: res => res, 274 | parseError: res => res 275 | }); 276 | 277 | const id = 123; 278 | const config = { foo: 'bar' }; 279 | 280 | const commit = sinon.spy(); 281 | const spy = sinon.spy(client, 'get'); 282 | 283 | fetchSingle({ commit }, { id, config }); 284 | 285 | t.true(spy.calledWith(`/articles/${id}`, config)); 286 | 287 | client.get.restore(); 288 | }); 289 | 290 | test('fetch single supports customUrl', (t) => { 291 | const { fetchSingle } = createActions({ 292 | rootUrl: '/articles', 293 | only: ['FETCH_SINGLE'], 294 | client, 295 | parseList: res => res, 296 | parseSingle: res => res, 297 | parseError: res => res 298 | }); 299 | 300 | const config = { foo: 'bar' }; 301 | 302 | const commit = sinon.spy(); 303 | const spy = sinon.spy(client, 'get'); 304 | 305 | fetchSingle({ commit }, { config, customUrl: '/custom-articles/123' }); 306 | 307 | t.true(spy.calledWith('/custom-articles/123', config)); 308 | 309 | client.get.restore(); 310 | }); 311 | 312 | test('fetch single supports customUrlFnArgs', (t) => { 313 | const { fetchSingle } = createActions({ 314 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles/${id}`; }, 315 | only: ['FETCH_SINGLE'], 316 | client, 317 | parseList: res => res, 318 | parseSingle: res => res, 319 | parseError: res => res 320 | }); 321 | 322 | const id = 123; 323 | const config = { foo: 'bar' }; 324 | 325 | const commit = sinon.spy(); 326 | const spy = sinon.spy(client, 'get'); 327 | 328 | fetchSingle({ commit }, { id, config, customUrlFnArgs: '456' }); 329 | 330 | t.true(spy.calledWith('/users/456/articles/123', config)); 331 | 332 | client.get.restore(); 333 | }); 334 | 335 | test('fetch single supports customUrlFnArgs as array', (t) => { 336 | const { fetchSingle } = createActions({ 337 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles/${id}`; }, 338 | only: ['FETCH_SINGLE'], 339 | client, 340 | parseList: res => res, 341 | parseSingle: res => res, 342 | parseError: res => res 343 | }); 344 | 345 | const id = 123; 346 | const config = { foo: 'bar' }; 347 | 348 | const commit = sinon.spy(); 349 | const spy = sinon.spy(client, 'get'); 350 | 351 | fetchSingle({ commit }, { id, config, customUrlFnArgs: ['456'] }); 352 | 353 | t.true(spy.calledWith('/users/456/articles/123', config)); 354 | 355 | client.get.restore(); 356 | }); 357 | 358 | // Create 359 | 360 | test('creates actions with create method', (t) => { 361 | const actions = createActions({ 362 | only: ['CREATE'], 363 | parseList: res => res, 364 | parseSingle: res => res, 365 | parseError: res => res 366 | }); 367 | 368 | t.truthy(actions.create); 369 | 370 | t.falsy(actions.fetchList); 371 | t.falsy(actions.fetchSingle); 372 | t.falsy(actions.update); 373 | t.falsy(actions.replace); 374 | t.falsy(actions.destroy); 375 | }); 376 | 377 | test('create commits createStart', (t) => { 378 | const { create } = createActions({ 379 | only: ['CREATE'], 380 | client, 381 | parseList: res => res, 382 | parseSingle: res => res, 383 | parseError: res => res 384 | }); 385 | 386 | const commit = sinon.spy(); 387 | 388 | create({ commit }); 389 | 390 | t.true(commit.calledWith('createStart')); 391 | }); 392 | 393 | test.cb('create commits createSuccess', (t) => { 394 | const { create } = createActions({ 395 | only: ['CREATE'], 396 | client, 397 | parseList: res => res, 398 | parseSingle: res => res, 399 | parseError: res => res 400 | }); 401 | 402 | const commit = sinon.spy(); 403 | 404 | t.plan(2); 405 | 406 | create({ commit }).then(() => { 407 | const { args } = commit.getCalls()[1]; 408 | 409 | 410 | t.is(args[0], 'createSuccess'); 411 | t.deepEqual(args[1], client.successResponse); 412 | 413 | t.end(); 414 | }); 415 | }); 416 | 417 | test.cb('create commits createError', (t) => { 418 | client.isSuccessful = false; 419 | 420 | const { create } = createActions({ 421 | only: ['CREATE'], 422 | client, 423 | parseList: res => res, 424 | parseSingle: res => res, 425 | parseError: res => res 426 | }); 427 | 428 | const commit = sinon.spy(); 429 | 430 | t.plan(2); 431 | 432 | create({ commit }).catch(() => { 433 | const { args } = commit.getCalls()[1]; 434 | 435 | t.is(args[0], 'createError'); 436 | t.deepEqual(args[1], client.errorResponse); 437 | 438 | t.end(); 439 | }); 440 | }); 441 | 442 | test('calls post with correct arguments', (t) => { 443 | const { create } = createActions({ 444 | rootUrl: '/articles', 445 | only: ['CREATE'], 446 | client, 447 | parseList: res => res, 448 | parseSingle: res => res, 449 | parseError: res => res 450 | }); 451 | 452 | const data = { some: 'data' }; 453 | const config = { foo: 'bar' }; 454 | 455 | const commit = sinon.spy(); 456 | const spy = sinon.spy(client, 'post'); 457 | 458 | create({ commit }, { data, config }); 459 | 460 | t.true(spy.calledWith('/articles', data, config)); 461 | 462 | client.post.restore(); 463 | }); 464 | 465 | test('create supports customUrl', (t) => { 466 | const { create } = createActions({ 467 | rootUrl: '/articles', 468 | only: ['CREATE'], 469 | client, 470 | parseList: res => res, 471 | parseSingle: res => res, 472 | parseError: res => res 473 | }); 474 | 475 | const data = { some: 'data' }; 476 | const config = { foo: 'bar' }; 477 | 478 | const commit = sinon.spy(); 479 | const spy = sinon.spy(client, 'post'); 480 | 481 | create({ commit }, { data, config, customUrl: '/custom-articles' }); 482 | 483 | t.true(spy.calledWith('/custom-articles', data, config)); 484 | 485 | client.post.restore(); 486 | }); 487 | 488 | test('create supports customUrlFnArgs', (t) => { 489 | const { create } = createActions({ 490 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles`; }, 491 | only: ['CREATE'], 492 | client, 493 | parseList: res => res, 494 | parseSingle: res => res, 495 | parseError: res => res 496 | }); 497 | 498 | const data = { some: 'data' }; 499 | const config = { foo: 'bar' }; 500 | 501 | const commit = sinon.spy(); 502 | const spy = sinon.spy(client, 'post'); 503 | 504 | create({ commit }, { data, config, customUrlFnArgs: '123' }); 505 | 506 | t.true(spy.calledWith('/users/123/articles', data, config)); 507 | 508 | client.post.restore(); 509 | }); 510 | 511 | test('create supports customUrlFnArgs as array', (t) => { 512 | const { create } = createActions({ 513 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles`; }, 514 | only: ['CREATE'], 515 | client, 516 | parseList: res => res, 517 | parseSingle: res => res, 518 | parseError: res => res 519 | }); 520 | 521 | const data = { some: 'data' }; 522 | const config = { foo: 'bar' }; 523 | 524 | const commit = sinon.spy(); 525 | const spy = sinon.spy(client, 'post'); 526 | 527 | create({ commit }, { data, config, customUrlFnArgs: ['123'] }); 528 | 529 | t.true(spy.calledWith('/users/123/articles', data, config)); 530 | 531 | client.post.restore(); 532 | }); 533 | 534 | // Update 535 | 536 | test('creates actions with update method', (t) => { 537 | const actions = createActions({ 538 | only: ['UPDATE'], 539 | parseList: res => res, 540 | parseSingle: res => res, 541 | parseError: res => res 542 | }); 543 | 544 | t.truthy(actions.update); 545 | 546 | t.falsy(actions.fetchList); 547 | t.falsy(actions.fetchSingle); 548 | t.falsy(actions.create); 549 | t.falsy(actions.replace); 550 | t.falsy(actions.destroy); 551 | }); 552 | 553 | test('update commits updateStart', (t) => { 554 | const { update } = createActions({ 555 | only: ['UPDATE'], 556 | client, 557 | parseList: res => res, 558 | parseSingle: res => res, 559 | parseError: res => res 560 | }); 561 | 562 | const commit = sinon.spy(); 563 | 564 | update({ commit }); 565 | 566 | t.true(commit.calledWith('updateStart')); 567 | }); 568 | 569 | test.cb('update commits updateSuccess', (t) => { 570 | const { update } = createActions({ 571 | only: ['UPDATE'], 572 | client, 573 | parseList: res => res, 574 | parseSingle: res => res, 575 | parseError: res => res 576 | }); 577 | 578 | const commit = sinon.spy(); 579 | 580 | t.plan(2); 581 | 582 | update({ commit }).then(() => { 583 | const { args } = commit.getCalls()[1]; 584 | 585 | t.is(args[0], 'updateSuccess'); 586 | t.deepEqual(args[1], client.successResponse); 587 | 588 | t.end(); 589 | }); 590 | }); 591 | 592 | test.cb('update commits updateError', (t) => { 593 | client.isSuccessful = false; 594 | 595 | const { update } = createActions({ 596 | only: ['UPDATE'], 597 | client, 598 | parseList: res => res, 599 | parseSingle: res => res, 600 | parseError: res => res 601 | }); 602 | 603 | const commit = sinon.spy(); 604 | 605 | t.plan(2); 606 | 607 | update({ commit }).catch(() => { 608 | const { args } = commit.getCalls()[1]; 609 | 610 | t.is(args[0], 'updateError'); 611 | t.deepEqual(args[1], client.errorResponse); 612 | 613 | t.end(); 614 | }); 615 | }); 616 | 617 | test('calls patch with correct arguments', (t) => { 618 | const { update } = createActions({ 619 | rootUrl: '/articles', 620 | only: ['UPDATE'], 621 | client, 622 | parseList: res => res, 623 | parseSingle: res => res, 624 | parseError: res => res 625 | }); 626 | 627 | const id = 123; 628 | const data = { some: 'data' }; 629 | const config = { foo: 'bar' }; 630 | 631 | const commit = sinon.spy(); 632 | const spy = sinon.spy(client, 'patch'); 633 | 634 | update({ commit }, { id, data, config }); 635 | 636 | t.true(spy.calledWith(`/articles/${id}`, data, config)); 637 | 638 | client.patch.restore(); 639 | }); 640 | 641 | test('update supports customUrl', (t) => { 642 | const { update } = createActions({ 643 | rootUrl: '/articles', 644 | only: ['UPDATE'], 645 | client, 646 | parseList: res => res, 647 | parseSingle: res => res, 648 | parseError: res => res 649 | }); 650 | 651 | const data = { some: 'data' }; 652 | const config = { foo: 'bar' }; 653 | 654 | const commit = sinon.spy(); 655 | const spy = sinon.spy(client, 'patch'); 656 | 657 | update({ commit }, { data, config, customUrl: '/custom-articles/123' }); 658 | 659 | t.true(spy.calledWith('/custom-articles/123', data, config)); 660 | 661 | client.patch.restore(); 662 | }); 663 | 664 | test('update supports customUrlFnArgs', (t) => { 665 | const { update } = createActions({ 666 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles/${id}`; }, 667 | only: ['UPDATE'], 668 | client, 669 | parseList: res => res, 670 | parseSingle: res => res, 671 | parseError: res => res 672 | }); 673 | 674 | const id = 123; 675 | const data = { some: 'data' }; 676 | const config = { foo: 'bar' }; 677 | 678 | const commit = sinon.spy(); 679 | const spy = sinon.spy(client, 'patch'); 680 | 681 | update({ commit }, { 682 | id, 683 | data, 684 | config, 685 | customUrlFnArgs: '456' 686 | }); 687 | 688 | t.true(spy.calledWith('/users/456/articles/123', data, config)); 689 | 690 | client.patch.restore(); 691 | }); 692 | 693 | test('update supports customUrlFnArgs as array', (t) => { 694 | const { update } = createActions({ 695 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles/${id}`; }, 696 | only: ['UPDATE'], 697 | client, 698 | parseList: res => res, 699 | parseSingle: res => res, 700 | parseError: res => res 701 | }); 702 | 703 | const id = 123; 704 | const data = { some: 'data' }; 705 | const config = { foo: 'bar' }; 706 | 707 | const commit = sinon.spy(); 708 | const spy = sinon.spy(client, 'patch'); 709 | 710 | update({ commit }, { 711 | id, 712 | data, 713 | config, 714 | customUrlFnArgs: ['456'] 715 | }); 716 | 717 | t.true(spy.calledWith('/users/456/articles/123', data, config)); 718 | 719 | client.patch.restore(); 720 | }); 721 | 722 | // Replace 723 | 724 | test('creates actions with replace method', (t) => { 725 | const actions = createActions({ 726 | only: ['REPLACE'], 727 | parseList: res => res, 728 | parseSingle: res => res, 729 | parseError: res => res 730 | }); 731 | 732 | t.truthy(actions.replace); 733 | 734 | t.falsy(actions.fetchList); 735 | t.falsy(actions.fetchSingle); 736 | t.falsy(actions.create); 737 | t.falsy(actions.update); 738 | t.falsy(actions.destroy); 739 | }); 740 | 741 | test('replace commits replaceStart', (t) => { 742 | const { replace } = createActions({ 743 | only: ['REPLACE'], 744 | client, 745 | parseList: res => res, 746 | parseSingle: res => res, 747 | parseError: res => res 748 | }); 749 | 750 | const commit = sinon.spy(); 751 | 752 | replace({ commit }); 753 | 754 | t.true(commit.calledWith('replaceStart')); 755 | }); 756 | 757 | test.cb('replace commits replaceSuccess', (t) => { 758 | const { replace } = createActions({ 759 | only: ['REPLACE'], 760 | client, 761 | parseList: res => res, 762 | parseSingle: res => res, 763 | parseError: res => res 764 | }); 765 | 766 | const commit = sinon.spy(); 767 | 768 | t.plan(2); 769 | 770 | replace({ commit }).then(() => { 771 | const { args } = commit.getCalls()[1]; 772 | 773 | t.is(args[0], 'replaceSuccess'); 774 | t.deepEqual(args[1], client.successResponse); 775 | 776 | t.end(); 777 | }); 778 | }); 779 | 780 | test.cb('replace commits replaceError', (t) => { 781 | client.isSuccessful = false; 782 | 783 | const { replace } = createActions({ 784 | only: ['REPLACE'], 785 | client, 786 | parseList: res => res, 787 | parseSingle: res => res, 788 | parseError: res => res 789 | }); 790 | 791 | const commit = sinon.spy(); 792 | 793 | t.plan(2); 794 | 795 | replace({ commit }).catch(() => { 796 | const { args } = commit.getCalls()[1]; 797 | 798 | t.is(args[0], 'replaceError'); 799 | t.deepEqual(args[1], client.errorResponse); 800 | 801 | t.end(); 802 | }); 803 | }); 804 | 805 | test('calls put with correct arguments', (t) => { 806 | const { replace } = createActions({ 807 | rootUrl: '/articles', 808 | only: ['REPLACE'], 809 | client, 810 | parseList: res => res, 811 | parseSingle: res => res, 812 | parseError: res => res 813 | }); 814 | 815 | const id = 123; 816 | const data = { some: 'data' }; 817 | const config = { foo: 'bar' }; 818 | 819 | const commit = sinon.spy(); 820 | const spy = sinon.spy(client, 'put'); 821 | 822 | replace({ commit }, { id, data, config }); 823 | 824 | t.true(spy.calledWith(`/articles/${id}`, data, config)); 825 | 826 | client.put.restore(); 827 | }); 828 | 829 | test('replace supports customUrl', (t) => { 830 | const { replace } = createActions({ 831 | rootUrl: '/articles', 832 | only: ['REPLACE'], 833 | client, 834 | parseList: res => res, 835 | parseSingle: res => res, 836 | parseError: res => res 837 | }); 838 | 839 | const data = { some: 'data' }; 840 | const config = { foo: 'bar' }; 841 | 842 | const commit = sinon.spy(); 843 | const spy = sinon.spy(client, 'put'); 844 | 845 | replace({ commit }, { data, config, customUrl: '/custom-articles/123' }); 846 | 847 | t.true(spy.calledWith('/custom-articles/123', data, config)); 848 | 849 | client.put.restore(); 850 | }); 851 | 852 | test('replace supports customUrlFnArgs', (t) => { 853 | const { replace } = createActions({ 854 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles/${id}`; }, 855 | only: ['REPLACE'], 856 | client, 857 | parseList: res => res, 858 | parseSingle: res => res, 859 | parseError: res => res 860 | }); 861 | 862 | const id = 123; 863 | const data = { some: 'data' }; 864 | const config = { foo: 'bar' }; 865 | 866 | const commit = sinon.spy(); 867 | const spy = sinon.spy(client, 'put'); 868 | 869 | replace({ commit }, { 870 | id, 871 | data, 872 | config, 873 | customUrlFnArgs: '456' 874 | }); 875 | 876 | t.true(spy.calledWith('/users/456/articles/123', data, config)); 877 | 878 | client.put.restore(); 879 | }); 880 | 881 | test('replace supports customUrlFnArgs as array', (t) => { 882 | const { replace } = createActions({ 883 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles/${id}`; }, 884 | only: ['REPLACE'], 885 | client, 886 | parseList: res => res, 887 | parseSingle: res => res, 888 | parseError: res => res 889 | }); 890 | 891 | const id = 123; 892 | const data = { some: 'data' }; 893 | const config = { foo: 'bar' }; 894 | 895 | const commit = sinon.spy(); 896 | const spy = sinon.spy(client, 'put'); 897 | 898 | replace({ commit }, { 899 | id, 900 | data, 901 | config, 902 | customUrlFnArgs: ['456'] 903 | }); 904 | 905 | t.true(spy.calledWith('/users/456/articles/123', data, config)); 906 | 907 | client.put.restore(); 908 | }); 909 | 910 | // Destroy 911 | 912 | test('creates actions with destroy method', (t) => { 913 | const actions = createActions({ 914 | only: ['DESTROY'], 915 | parseList: res => res, 916 | parseSingle: res => res, 917 | parseError: res => res 918 | }); 919 | 920 | t.truthy(actions.destroy); 921 | 922 | t.falsy(actions.fetchList); 923 | t.falsy(actions.fetchSingle); 924 | t.falsy(actions.create); 925 | t.falsy(actions.update); 926 | t.falsy(actions.replace); 927 | }); 928 | 929 | test('destroy commits destroyStart', (t) => { 930 | const { destroy } = createActions({ 931 | only: ['DESTROY'], 932 | client, 933 | parseList: res => res, 934 | parseSingle: res => res, 935 | parseError: res => res 936 | }); 937 | 938 | const commit = sinon.spy(); 939 | 940 | destroy({ commit }); 941 | 942 | t.true(commit.calledWith('destroyStart')); 943 | }); 944 | 945 | test.cb('destroy commits destroySuccess', (t) => { 946 | const { destroy } = createActions({ 947 | only: ['DESTROY'], 948 | client, 949 | parseList: res => res, 950 | parseSingle: res => res, 951 | parseError: res => res 952 | }); 953 | 954 | const commit = sinon.spy(); 955 | const id = 1; 956 | 957 | t.plan(3); 958 | 959 | destroy({ commit }, { id }).then(() => { 960 | const { args } = commit.getCalls()[1]; 961 | 962 | t.is(args[0], 'destroySuccess'); 963 | t.deepEqual(args[1], id); 964 | t.deepEqual(args[2], client.successResponse); 965 | 966 | t.end(); 967 | }); 968 | }); 969 | 970 | test.cb('destroy commits destroyError', (t) => { 971 | client.isSuccessful = false; 972 | 973 | const { destroy } = createActions({ 974 | only: ['DESTROY'], 975 | client, 976 | parseList: res => res, 977 | parseSingle: res => res, 978 | parseError: res => res 979 | }); 980 | 981 | const commit = sinon.spy(); 982 | 983 | t.plan(2); 984 | 985 | destroy({ commit }).catch(() => { 986 | const { args } = commit.getCalls()[1]; 987 | 988 | t.is(args[0], 'destroyError'); 989 | t.deepEqual(args[1], client.errorResponse); 990 | 991 | t.end(); 992 | }); 993 | }); 994 | 995 | test('calls delete with correct arguments', (t) => { 996 | const { destroy } = createActions({ 997 | rootUrl: '/articles', 998 | only: ['DESTROY'], 999 | client, 1000 | parseList: res => res, 1001 | parseSingle: res => res, 1002 | parseError: res => res 1003 | }); 1004 | 1005 | const id = 123; 1006 | const config = { foo: 'bar' }; 1007 | 1008 | const commit = sinon.spy(); 1009 | const spy = sinon.spy(client, 'delete'); 1010 | 1011 | destroy({ commit }, { id, config }); 1012 | 1013 | t.true(spy.calledWith(`/articles/${id}`, config)); 1014 | 1015 | client.delete.restore(); 1016 | }); 1017 | 1018 | test('destroy supports customUrl', (t) => { 1019 | const { destroy } = createActions({ 1020 | rootUrl: '/articles', 1021 | only: ['DESTROY'], 1022 | client, 1023 | parseList: res => res, 1024 | parseSingle: res => res, 1025 | parseError: res => res 1026 | }); 1027 | 1028 | const config = { foo: 'bar' }; 1029 | 1030 | const commit = sinon.spy(); 1031 | const spy = sinon.spy(client, 'delete'); 1032 | 1033 | destroy({ commit }, { config, customUrl: '/custom-articles/123' }); 1034 | 1035 | t.true(spy.calledWith('/custom-articles/123', config)); 1036 | 1037 | client.delete.restore(); 1038 | }); 1039 | 1040 | test('destroy supports customUrlFnArgs', (t) => { 1041 | const { destroy } = createActions({ 1042 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles/${id}`; }, 1043 | only: ['DESTROY'], 1044 | client, 1045 | parseList: res => res, 1046 | parseSingle: res => res, 1047 | parseError: res => res 1048 | }); 1049 | 1050 | const id = 123; 1051 | const config = { foo: 'bar' }; 1052 | 1053 | const commit = sinon.spy(); 1054 | const spy = sinon.spy(client, 'delete'); 1055 | 1056 | destroy({ commit }, { 1057 | id, 1058 | config, 1059 | customUrlFnArgs: '456' 1060 | }); 1061 | 1062 | t.true(spy.calledWith('/users/456/articles/123', config)); 1063 | 1064 | client.delete.restore(); 1065 | }); 1066 | 1067 | test('destroy supports customUrlFnArgs as array', (t) => { 1068 | const { destroy } = createActions({ 1069 | rootUrl(id, type, parentId) { return `/users/${parentId}/articles/${id}`; }, 1070 | only: ['DESTROY'], 1071 | client, 1072 | parseList: res => res, 1073 | parseSingle: res => res, 1074 | parseError: res => res 1075 | }); 1076 | 1077 | const id = 123; 1078 | const config = { foo: 'bar' }; 1079 | 1080 | const commit = sinon.spy(); 1081 | const spy = sinon.spy(client, 'delete'); 1082 | 1083 | destroy({ commit }, { 1084 | id, 1085 | config, 1086 | customUrlFnArgs: ['456'] 1087 | }); 1088 | 1089 | t.true(spy.calledWith('/users/456/articles/123', config)); 1090 | 1091 | client.delete.restore(); 1092 | }); 1093 | -------------------------------------------------------------------------------- /test/unit/vuex-crud/createMutations.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | 4 | import createMutations from '../../../src/vuex-crud/createMutations'; 5 | 6 | // FETCH_LIST 7 | 8 | test('add mutations for fetch list', (t) => { 9 | const mutations = createMutations({ 10 | only: ['FETCH_LIST'] 11 | }); 12 | 13 | t.truthy(mutations.fetchListStart); 14 | t.truthy(mutations.fetchListSuccess); 15 | t.truthy(mutations.fetchListError); 16 | 17 | t.falsy(mutations.fetchSingleStart); 18 | t.falsy(mutations.fetchSingleSuccess); 19 | t.falsy(mutations.fetchSingleError); 20 | 21 | t.falsy(mutations.createStart); 22 | t.falsy(mutations.createSuccess); 23 | t.falsy(mutations.createError); 24 | 25 | t.falsy(mutations.updateStart); 26 | t.falsy(mutations.updateSuccess); 27 | t.falsy(mutations.updateError); 28 | 29 | t.falsy(mutations.replaceStart); 30 | t.falsy(mutations.replaceSuccess); 31 | t.falsy(mutations.replaceError); 32 | 33 | t.falsy(mutations.destroyStart); 34 | t.falsy(mutations.destroySuccess); 35 | t.falsy(mutations.destroyError); 36 | }); 37 | 38 | test('fetch list start', (t) => { 39 | const onFetchListStart = sinon.spy(); 40 | 41 | const { fetchListStart } = createMutations({ 42 | only: ['FETCH_LIST'], 43 | onFetchListStart, 44 | idAttribute: 'id' 45 | }); 46 | 47 | const initialState = { 48 | list: [], 49 | entities: {} 50 | }; 51 | 52 | fetchListStart(initialState); 53 | 54 | t.is(initialState.isFetchingList, true); 55 | }); 56 | 57 | test('fetch list start calls onFetchListStart', (t) => { 58 | const onFetchListStart = sinon.spy(); 59 | 60 | const { fetchListStart } = createMutations({ 61 | only: ['FETCH_LIST'], 62 | onFetchListStart, 63 | idAttribute: 'id' 64 | }); 65 | 66 | const initialState = { 67 | list: [], 68 | entities: {} 69 | }; 70 | 71 | fetchListStart(initialState); 72 | 73 | t.true(onFetchListStart.calledWith(initialState)); 74 | }); 75 | 76 | test('fetch list success', (t) => { 77 | const onFetchListSuccess = sinon.spy(); 78 | 79 | const { fetchListSuccess } = createMutations({ 80 | only: ['FETCH_LIST'], 81 | onFetchListSuccess, 82 | idAttribute: 'id' 83 | }); 84 | 85 | const initialState = { 86 | list: ['1', '5', '6'], 87 | entities: {} 88 | }; 89 | 90 | const data = [ 91 | { id: 1 }, 92 | { id: 2 } 93 | ]; 94 | 95 | fetchListSuccess(initialState, { data }); 96 | 97 | t.is(initialState.isFetchingList, false); 98 | 99 | t.is(initialState.fetchListError, null); 100 | 101 | t.is(initialState.entities['1'], data[0]); 102 | t.is(initialState.entities['2'], data[1]); 103 | 104 | t.deepEqual(initialState.list, ['1', '2']); 105 | }); 106 | 107 | test('fetch list success calls onFetchListSuccess', (t) => { 108 | const onFetchListSuccess = sinon.spy(); 109 | 110 | const { fetchListSuccess } = createMutations({ 111 | only: ['FETCH_LIST'], 112 | onFetchListSuccess, 113 | idAttribute: 'id' 114 | }); 115 | 116 | const initialState = { 117 | list: [], 118 | entities: {} 119 | }; 120 | 121 | const data = [ 122 | { id: 1 }, 123 | { id: 2 } 124 | ]; 125 | 126 | fetchListSuccess(initialState, { data }); 127 | 128 | t.true(onFetchListSuccess.calledWith(initialState)); 129 | }); 130 | 131 | test('fetch list error', (t) => { 132 | const onFetchListError = sinon.spy(); 133 | 134 | const { fetchListError } = createMutations({ 135 | only: ['FETCH_LIST'], 136 | onFetchListError, 137 | idAttribute: 'id' 138 | }); 139 | 140 | const initialState = { 141 | list: ['1', '5', '6'], 142 | entities: {} 143 | }; 144 | 145 | const error = { message: 'Something went wrong' }; 146 | 147 | fetchListError(initialState, error); 148 | 149 | t.is(initialState.isFetchingList, false); 150 | 151 | t.is(initialState.fetchListError, error); 152 | 153 | t.deepEqual(initialState.list, []); 154 | }); 155 | 156 | test('fetch list error calls onFetchListError', (t) => { 157 | const onFetchListError = sinon.spy(); 158 | 159 | const { fetchListError } = createMutations({ 160 | only: ['FETCH_LIST'], 161 | onFetchListError, 162 | idAttribute: 'id' 163 | }); 164 | 165 | const initialState = { 166 | list: [], 167 | entities: {} 168 | }; 169 | 170 | const error = { message: 'Something went wrong' }; 171 | 172 | fetchListError(initialState, error); 173 | 174 | t.true(onFetchListError.calledWith(initialState, error)); 175 | }); 176 | 177 | // FETCH_SINGLE 178 | 179 | test('add mutations for fetch single', (t) => { 180 | const mutations = createMutations({ 181 | only: ['FETCH_SINGLE'] 182 | }); 183 | 184 | t.falsy(mutations.fetchListStart); 185 | t.falsy(mutations.fetchListSuccess); 186 | t.falsy(mutations.fetchListError); 187 | 188 | t.truthy(mutations.fetchSingleStart); 189 | t.truthy(mutations.fetchSingleSuccess); 190 | t.truthy(mutations.fetchSingleError); 191 | 192 | t.falsy(mutations.createStart); 193 | t.falsy(mutations.createSuccess); 194 | t.falsy(mutations.createError); 195 | 196 | t.falsy(mutations.updateStart); 197 | t.falsy(mutations.updateSuccess); 198 | t.falsy(mutations.updateError); 199 | 200 | t.falsy(mutations.replaceStart); 201 | t.falsy(mutations.replaceSuccess); 202 | t.falsy(mutations.replaceError); 203 | 204 | t.falsy(mutations.destroyStart); 205 | t.falsy(mutations.destroySuccess); 206 | t.falsy(mutations.destroyError); 207 | }); 208 | 209 | test('fetch single start', (t) => { 210 | const onFetchSingleStart = sinon.spy(); 211 | 212 | const { fetchSingleStart } = createMutations({ 213 | only: ['FETCH_SINGLE'], 214 | onFetchSingleStart, 215 | idAttribute: 'id' 216 | }); 217 | 218 | const initialState = { 219 | list: [], 220 | entities: {} 221 | }; 222 | 223 | fetchSingleStart(initialState); 224 | 225 | t.is(initialState.isFetchingSingle, true); 226 | }); 227 | 228 | test('fetch single start calls onFetchSingleStart', (t) => { 229 | const onFetchSingleStart = sinon.spy(); 230 | 231 | const { fetchSingleStart } = createMutations({ 232 | only: ['FETCH_SINGLE'], 233 | onFetchSingleStart, 234 | idAttribute: 'id' 235 | }); 236 | 237 | const initialState = { 238 | list: [], 239 | entities: {} 240 | }; 241 | 242 | fetchSingleStart(initialState); 243 | 244 | t.true(onFetchSingleStart.calledWith(initialState)); 245 | }); 246 | 247 | test('fetch single success not in the list', (t) => { 248 | const onFetchSingleSuccess = sinon.spy(); 249 | 250 | const { fetchSingleSuccess } = createMutations({ 251 | only: ['FETCH_SINGLE'], 252 | onFetchSingleSuccess, 253 | idAttribute: 'id' 254 | }); 255 | 256 | const initialState = { 257 | list: ['1', '5', '6'], 258 | entities: {} 259 | }; 260 | 261 | const data = { id: 1 }; 262 | 263 | fetchSingleSuccess(initialState, { data }); 264 | 265 | t.is(initialState.isFetchingSingle, false); 266 | 267 | t.is(initialState.fetchSingleError, null); 268 | 269 | t.is(initialState.entities['1'], data); 270 | 271 | t.deepEqual(initialState.list, ['1', '5', '6']); 272 | }); 273 | 274 | test('fetch single success in the list', (t) => { 275 | const onFetchSingleSuccess = sinon.spy(); 276 | 277 | const { fetchSingleSuccess } = createMutations({ 278 | only: ['FETCH_SINGLE'], 279 | onFetchSingleSuccess, 280 | idAttribute: 'id' 281 | }); 282 | 283 | const initialState = { 284 | list: ['1', '5', '6'], 285 | entities: {} 286 | }; 287 | 288 | const data = { id: 1 }; 289 | 290 | fetchSingleSuccess(initialState, { data }); 291 | 292 | t.is(initialState.isFetchingSingle, false); 293 | 294 | t.is(initialState.fetchSingleError, null); 295 | 296 | t.is(initialState.entities['1'], data); 297 | 298 | t.deepEqual(initialState.list, ['1', '5', '6']); 299 | }); 300 | 301 | test('fetch single success calls onFetchSingleSuccess', (t) => { 302 | const onFetchSingleSuccess = sinon.spy(); 303 | 304 | const { fetchSingleSuccess } = createMutations({ 305 | only: ['FETCH_SINGLE'], 306 | onFetchSingleSuccess, 307 | idAttribute: 'id' 308 | }); 309 | 310 | const initialState = { 311 | list: [], 312 | entities: {} 313 | }; 314 | 315 | const data = { id: 1 }; 316 | 317 | fetchSingleSuccess(initialState, { data }); 318 | 319 | t.true(onFetchSingleSuccess.calledWith(initialState, { data })); 320 | }); 321 | 322 | test('fetch single error', (t) => { 323 | const onFetchSingleError = sinon.spy(); 324 | 325 | const { fetchSingleError } = createMutations({ 326 | only: ['FETCH_SINGLE'], 327 | onFetchSingleError, 328 | idAttribute: 'id' 329 | }); 330 | 331 | const initialState = { 332 | list: ['1', '5', '6'], 333 | entities: {} 334 | }; 335 | 336 | const error = { message: 'Something went wrong' }; 337 | 338 | fetchSingleError(initialState, error); 339 | 340 | t.is(initialState.isFetchingSingle, false); 341 | 342 | t.is(initialState.fetchSingleError, error); 343 | 344 | t.deepEqual(initialState.list, initialState.list); 345 | t.deepEqual(initialState.entities, initialState.entities); 346 | }); 347 | 348 | test('fetch single error calls onFetchSingleError', (t) => { 349 | const onFetchSingleError = sinon.spy(); 350 | 351 | const { fetchSingleError } = createMutations({ 352 | only: ['FETCH_SINGLE'], 353 | onFetchSingleError, 354 | idAttribute: 'id' 355 | }); 356 | 357 | const initialState = { 358 | list: [], 359 | entities: {} 360 | }; 361 | 362 | const error = { message: 'Something went wrong' }; 363 | 364 | fetchSingleError(initialState, error); 365 | 366 | t.true(onFetchSingleError.calledWith(initialState, error)); 367 | }); 368 | 369 | // CREATE 370 | 371 | test('add mutations for create', (t) => { 372 | const mutations = createMutations({ 373 | only: ['CREATE'] 374 | }); 375 | 376 | t.falsy(mutations.fetchListStart); 377 | t.falsy(mutations.fetchListSuccess); 378 | t.falsy(mutations.fetchListError); 379 | 380 | t.falsy(mutations.fetchSingleStart); 381 | t.falsy(mutations.fetchSingleSuccess); 382 | t.falsy(mutations.fetchSingleError); 383 | 384 | t.truthy(mutations.createStart); 385 | t.truthy(mutations.createSuccess); 386 | t.truthy(mutations.createError); 387 | 388 | t.falsy(mutations.updateStart); 389 | t.falsy(mutations.updateSuccess); 390 | t.falsy(mutations.updateError); 391 | 392 | t.falsy(mutations.replaceStart); 393 | t.falsy(mutations.replaceSuccess); 394 | t.falsy(mutations.replaceError); 395 | 396 | t.falsy(mutations.destroyStart); 397 | t.falsy(mutations.destroySuccess); 398 | t.falsy(mutations.destroyError); 399 | }); 400 | 401 | test('create start', (t) => { 402 | const onCreateStart = sinon.spy(); 403 | 404 | const { createStart } = createMutations({ 405 | only: ['CREATE'], 406 | onCreateStart, 407 | idAttribute: 'id' 408 | }); 409 | 410 | const initialState = { 411 | list: [], 412 | entities: {} 413 | }; 414 | 415 | createStart(initialState); 416 | 417 | t.is(initialState.isCreating, true); 418 | }); 419 | 420 | test('create start calls onCreateStart', (t) => { 421 | const onCreateStart = sinon.spy(); 422 | 423 | const { createStart } = createMutations({ 424 | only: ['CREATE'], 425 | onCreateStart, 426 | idAttribute: 'id' 427 | }); 428 | 429 | const initialState = { 430 | list: [], 431 | entities: {} 432 | }; 433 | 434 | createStart(initialState); 435 | 436 | t.true(onCreateStart.calledWith(initialState)); 437 | }); 438 | 439 | test('create success', (t) => { 440 | const onCreateSuccess = sinon.spy(); 441 | 442 | const { createSuccess } = createMutations({ 443 | only: ['CREATE'], 444 | onCreateSuccess, 445 | idAttribute: 'id' 446 | }); 447 | 448 | const initialState = { 449 | list: ['1', '5', '6'], 450 | entities: {} 451 | }; 452 | 453 | const data = { id: 2 }; 454 | 455 | createSuccess(initialState, { data }); 456 | 457 | t.is(initialState.isCreating, false); 458 | 459 | t.is(initialState.createError, null); 460 | 461 | t.is(initialState.entities['2'], data); 462 | 463 | t.deepEqual(initialState.list, ['1', '5', '6']); 464 | }); 465 | 466 | test('create success without providing a data object', (t) => { 467 | const onCreateSuccess = sinon.spy(); 468 | 469 | const { createSuccess } = createMutations({ 470 | only: ['CREATE'], 471 | onCreateSuccess, 472 | idAttribute: 'id' 473 | }); 474 | 475 | const initialState = { 476 | list: ['1', '5', '6'], 477 | entities: {} 478 | }; 479 | 480 | const data = null; 481 | 482 | createSuccess(initialState, { data }); 483 | 484 | t.is(initialState.isCreating, false); 485 | 486 | t.is(initialState.createError, null); 487 | 488 | t.deepEqual(initialState.list, ['1', '5', '6']); 489 | }); 490 | 491 | test('create success calls onCreateSuccess', (t) => { 492 | const onCreateSuccess = sinon.spy(); 493 | 494 | const { createSuccess } = createMutations({ 495 | only: ['CREATE'], 496 | onCreateSuccess, 497 | idAttribute: 'id' 498 | }); 499 | 500 | const initialState = { 501 | list: [], 502 | entities: {} 503 | }; 504 | 505 | const data = { id: 1 }; 506 | 507 | createSuccess(initialState, { data }); 508 | 509 | t.true(onCreateSuccess.calledWith(initialState, { data })); 510 | }); 511 | 512 | test('create success calls onCreateSuccess without providing a data object', (t) => { 513 | const onCreateSuccess = sinon.spy(); 514 | 515 | const { createSuccess } = createMutations({ 516 | only: ['CREATE'], 517 | onCreateSuccess, 518 | idAttribute: 'id' 519 | }); 520 | 521 | const initialState = { 522 | list: [], 523 | entities: {} 524 | }; 525 | 526 | const data = null; 527 | 528 | createSuccess(initialState, { data }); 529 | 530 | t.true(onCreateSuccess.calledWith(initialState, { data })); 531 | }); 532 | 533 | test('create error', (t) => { 534 | const onCreateError = sinon.spy(); 535 | 536 | const { createError } = createMutations({ 537 | only: ['CREATE'], 538 | onCreateError, 539 | idAttribute: 'id' 540 | }); 541 | 542 | const initialState = { 543 | list: ['1', '5', '6'], 544 | entities: {} 545 | }; 546 | 547 | const error = { message: 'Something went wrong' }; 548 | 549 | createError(initialState, error); 550 | 551 | t.is(initialState.isCreating, false); 552 | 553 | t.is(initialState.createError, error); 554 | 555 | t.deepEqual(initialState.list, initialState.list); 556 | t.deepEqual(initialState.entities, initialState.entities); 557 | }); 558 | 559 | test('create error calls onCreateError', (t) => { 560 | const onCreateError = sinon.spy(); 561 | 562 | const { createError } = createMutations({ 563 | only: ['CREATE'], 564 | onCreateError, 565 | idAttribute: 'id' 566 | }); 567 | 568 | const initialState = { 569 | list: [], 570 | entities: {} 571 | }; 572 | 573 | const error = { message: 'Something went wrong' }; 574 | 575 | createError(initialState, error); 576 | 577 | t.true(onCreateError.calledWith(initialState, error)); 578 | }); 579 | 580 | // UPDATE 581 | 582 | test('add mutations for update', (t) => { 583 | const mutations = createMutations({ 584 | only: ['UPDATE'] 585 | }); 586 | 587 | t.falsy(mutations.fetchListStart); 588 | t.falsy(mutations.fetchListSuccess); 589 | t.falsy(mutations.fetchListError); 590 | 591 | t.falsy(mutations.fetchSingleStart); 592 | t.falsy(mutations.fetchSingleSuccess); 593 | t.falsy(mutations.fetchSingleError); 594 | 595 | t.falsy(mutations.createStart); 596 | t.falsy(mutations.createSuccess); 597 | t.falsy(mutations.createError); 598 | 599 | t.truthy(mutations.updateStart); 600 | t.truthy(mutations.updateSuccess); 601 | t.truthy(mutations.updateError); 602 | 603 | t.falsy(mutations.replaceStart); 604 | t.falsy(mutations.replaceSuccess); 605 | t.falsy(mutations.replaceError); 606 | 607 | t.falsy(mutations.destroyStart); 608 | t.falsy(mutations.destroySuccess); 609 | t.falsy(mutations.destroyError); 610 | }); 611 | 612 | test('update start', (t) => { 613 | const onUpdateStart = sinon.spy(); 614 | 615 | const { updateStart } = createMutations({ 616 | only: ['UPDATE'], 617 | onUpdateStart, 618 | idAttribute: 'id' 619 | }); 620 | 621 | const initialState = { 622 | list: [], 623 | entities: {} 624 | }; 625 | 626 | updateStart(initialState); 627 | 628 | t.is(initialState.isUpdating, true); 629 | }); 630 | 631 | test('update start calls onUpdateStart', (t) => { 632 | const onUpdateStart = sinon.spy(); 633 | 634 | const { updateStart } = createMutations({ 635 | only: ['UPDATE'], 636 | onUpdateStart, 637 | idAttribute: 'id' 638 | }); 639 | 640 | const initialState = { 641 | list: [], 642 | entities: {} 643 | }; 644 | 645 | updateStart(initialState); 646 | 647 | t.true(onUpdateStart.calledWith(initialState)); 648 | }); 649 | 650 | test('update success existing in list', (t) => { 651 | const onUpdateSuccess = sinon.spy(); 652 | 653 | const { updateSuccess } = createMutations({ 654 | only: ['UPDATE'], 655 | onUpdateSuccess, 656 | idAttribute: 'id' 657 | }); 658 | 659 | const initialState = { 660 | list: ['1', '5', '6'], 661 | entities: { 662 | 1: { 663 | name: 'Bob' 664 | } 665 | } 666 | }; 667 | 668 | const data = { id: 1, name: 'John' }; 669 | 670 | updateSuccess(initialState, { data }); 671 | 672 | t.is(initialState.isUpdating, false); 673 | 674 | t.is(initialState.updateError, null); 675 | 676 | t.is(initialState.entities['1'], data); 677 | 678 | t.deepEqual(initialState.list, ['1', '5', '6']); 679 | }); 680 | 681 | test('update success not existing in list', (t) => { 682 | const onUpdateSuccess = sinon.spy(); 683 | 684 | const { updateSuccess } = createMutations({ 685 | only: ['UPDATE'], 686 | onUpdateSuccess, 687 | idAttribute: 'id' 688 | }); 689 | 690 | const initialState = { 691 | list: ['1', '5', '6'], 692 | entities: { 693 | 1: { 694 | name: 'Bob' 695 | } 696 | } 697 | }; 698 | 699 | const data = { id: 2, name: 'John' }; 700 | 701 | updateSuccess(initialState, { data }); 702 | 703 | t.is(initialState.isUpdating, false); 704 | 705 | t.is(initialState.updateError, null); 706 | 707 | t.is(initialState.entities['2'], data); 708 | 709 | t.deepEqual(initialState.list, ['1', '5', '6']); 710 | }); 711 | 712 | test('update success calls onUpdateSuccess', (t) => { 713 | const onUpdateSuccess = sinon.spy(); 714 | 715 | const { updateSuccess } = createMutations({ 716 | only: ['UPDATE'], 717 | onUpdateSuccess, 718 | idAttribute: 'id' 719 | }); 720 | 721 | const initialState = { 722 | list: [], 723 | entities: {} 724 | }; 725 | 726 | const data = { id: 1, name: 'Bob' }; 727 | 728 | updateSuccess(initialState, { data }); 729 | 730 | t.true(onUpdateSuccess.calledWith(initialState, { data })); 731 | }); 732 | 733 | test('update error', (t) => { 734 | const onUpdateError = sinon.spy(); 735 | 736 | const { updateError } = createMutations({ 737 | only: ['UPDATE'], 738 | onUpdateError, 739 | idAttribute: 'id' 740 | }); 741 | 742 | const initialState = { 743 | list: ['1', '5', '6'], 744 | entities: {} 745 | }; 746 | 747 | const error = { message: 'Something went wrong' }; 748 | 749 | updateError(initialState, error); 750 | 751 | t.is(initialState.isUpdating, false); 752 | 753 | t.is(initialState.updateError, error); 754 | 755 | t.deepEqual(initialState.list, initialState.list); 756 | t.deepEqual(initialState.entities, initialState.entities); 757 | }); 758 | 759 | test('update error calls onUpdateError', (t) => { 760 | const onUpdateError = sinon.spy(); 761 | 762 | const { updateError } = createMutations({ 763 | only: ['UPDATE'], 764 | onUpdateError, 765 | idAttribute: 'id' 766 | }); 767 | 768 | const initialState = { 769 | list: [], 770 | entities: {} 771 | }; 772 | 773 | const error = { message: 'Something went wrong' }; 774 | 775 | updateError(initialState, error); 776 | 777 | t.true(onUpdateError.calledWith(initialState, error)); 778 | }); 779 | 780 | // REPLACE 781 | 782 | test('add mutations for replace', (t) => { 783 | const mutations = createMutations({ 784 | only: ['REPLACE'] 785 | }); 786 | 787 | t.falsy(mutations.fetchListStart); 788 | t.falsy(mutations.fetchListSuccess); 789 | t.falsy(mutations.fetchListError); 790 | 791 | t.falsy(mutations.fetchSingleStart); 792 | t.falsy(mutations.fetchSingleSuccess); 793 | t.falsy(mutations.fetchSingleError); 794 | 795 | t.falsy(mutations.createStart); 796 | t.falsy(mutations.createSuccess); 797 | t.falsy(mutations.createError); 798 | 799 | t.falsy(mutations.updateStart); 800 | t.falsy(mutations.updateSuccess); 801 | t.falsy(mutations.updateError); 802 | 803 | t.truthy(mutations.replaceStart); 804 | t.truthy(mutations.replaceSuccess); 805 | t.truthy(mutations.replaceError); 806 | 807 | t.falsy(mutations.destroyStart); 808 | t.falsy(mutations.destroySuccess); 809 | t.falsy(mutations.destroyError); 810 | }); 811 | 812 | test('replace start', (t) => { 813 | const onReplaceStart = sinon.spy(); 814 | 815 | const { replaceStart } = createMutations({ 816 | only: ['REPLACE'], 817 | onReplaceStart, 818 | idAttribute: 'id' 819 | }); 820 | 821 | const initialState = { 822 | list: [], 823 | entities: {} 824 | }; 825 | 826 | replaceStart(initialState); 827 | 828 | t.is(initialState.isReplacing, true); 829 | }); 830 | 831 | test('replace start calls onReplaceStart', (t) => { 832 | const onReplaceStart = sinon.spy(); 833 | 834 | const { replaceStart } = createMutations({ 835 | only: ['REPLACE'], 836 | onReplaceStart, 837 | idAttribute: 'id' 838 | }); 839 | 840 | const initialState = { 841 | list: [], 842 | entities: {} 843 | }; 844 | 845 | replaceStart(initialState); 846 | 847 | t.true(onReplaceStart.calledWith(initialState)); 848 | }); 849 | 850 | test('replace success existing in list', (t) => { 851 | const onReplaceSuccess = sinon.spy(); 852 | 853 | const { replaceSuccess } = createMutations({ 854 | only: ['REPLACE'], 855 | onReplaceSuccess, 856 | idAttribute: 'id' 857 | }); 858 | 859 | const initialState = { 860 | list: ['1', '5', '6'], 861 | entities: { 862 | 1: { 863 | name: 'Bob' 864 | } 865 | } 866 | }; 867 | 868 | const data = { id: 1, name: 'John' }; 869 | 870 | replaceSuccess(initialState, { data }); 871 | 872 | t.is(initialState.isReplacing, false); 873 | 874 | t.is(initialState.replaceError, null); 875 | 876 | t.is(initialState.entities['1'], data); 877 | 878 | t.deepEqual(initialState.list, ['1', '5', '6']); 879 | }); 880 | 881 | test('replace success not existing in list', (t) => { 882 | const onReplaceSuccess = sinon.spy(); 883 | 884 | const { replaceSuccess } = createMutations({ 885 | only: ['REPLACE'], 886 | onReplaceSuccess, 887 | idAttribute: 'id' 888 | }); 889 | 890 | const initialState = { 891 | list: ['1', '5', '6'], 892 | entities: { 893 | 1: { 894 | name: 'Bob' 895 | } 896 | } 897 | }; 898 | 899 | const data = { id: 2, name: 'John' }; 900 | 901 | replaceSuccess(initialState, { data }); 902 | 903 | t.is(initialState.isReplacing, false); 904 | 905 | t.is(initialState.replaceError, null); 906 | 907 | t.is(initialState.entities['2'], data); 908 | 909 | t.deepEqual(initialState.list, ['1', '5', '6']); 910 | }); 911 | 912 | test('replace success calls onReplaceSuccess', (t) => { 913 | const onReplaceSuccess = sinon.spy(); 914 | 915 | const { replaceSuccess } = createMutations({ 916 | only: ['REPLACE'], 917 | onReplaceSuccess, 918 | idAttribute: 'id' 919 | }); 920 | 921 | const initialState = { 922 | list: [], 923 | entities: {} 924 | }; 925 | 926 | const data = { id: 1, name: 'Bob' }; 927 | 928 | replaceSuccess(initialState, { data }); 929 | 930 | t.true(onReplaceSuccess.calledWith(initialState, { data })); 931 | }); 932 | 933 | test('replace error', (t) => { 934 | const onReplaceError = sinon.spy(); 935 | 936 | const { replaceError } = createMutations({ 937 | only: ['REPLACE'], 938 | onReplaceError, 939 | idAttribute: 'id' 940 | }); 941 | 942 | const initialState = { 943 | list: ['1', '5', '6'], 944 | entities: {} 945 | }; 946 | 947 | const error = { message: 'Something went wrong' }; 948 | 949 | replaceError(initialState, error); 950 | 951 | t.is(initialState.isReplacing, false); 952 | 953 | t.is(initialState.replaceError, error); 954 | 955 | t.deepEqual(initialState.list, initialState.list); 956 | t.deepEqual(initialState.entities, initialState.entities); 957 | }); 958 | 959 | test('replace error calls onReplaceError', (t) => { 960 | const onReplaceError = sinon.spy(); 961 | 962 | const { replaceError } = createMutations({ 963 | only: ['REPLACE'], 964 | onReplaceError, 965 | idAttribute: 'id' 966 | }); 967 | 968 | const initialState = { 969 | list: [], 970 | entities: {} 971 | }; 972 | 973 | const error = { message: 'Something went wrong' }; 974 | 975 | replaceError(initialState, error); 976 | 977 | t.true(onReplaceError.calledWith(initialState, error)); 978 | }); 979 | 980 | // DESTROY 981 | 982 | test('add mutations for destroy', (t) => { 983 | const mutations = createMutations({ 984 | only: ['DESTROY'] 985 | }); 986 | 987 | t.falsy(mutations.fetchListStart); 988 | t.falsy(mutations.fetchListSuccess); 989 | t.falsy(mutations.fetchListError); 990 | 991 | t.falsy(mutations.fetchSingleStart); 992 | t.falsy(mutations.fetchSingleSuccess); 993 | t.falsy(mutations.fetchSingleError); 994 | 995 | t.falsy(mutations.createStart); 996 | t.falsy(mutations.createSuccess); 997 | t.falsy(mutations.createError); 998 | 999 | t.falsy(mutations.updateStart); 1000 | t.falsy(mutations.updateSuccess); 1001 | t.falsy(mutations.updateError); 1002 | 1003 | t.falsy(mutations.replaceStart); 1004 | t.falsy(mutations.replaceSuccess); 1005 | t.falsy(mutations.replaceError); 1006 | 1007 | t.truthy(mutations.destroyStart); 1008 | t.truthy(mutations.destroySuccess); 1009 | t.truthy(mutations.destroyError); 1010 | }); 1011 | 1012 | test('destroy start', (t) => { 1013 | const onDestroyStart = sinon.spy(); 1014 | 1015 | const { destroyStart } = createMutations({ 1016 | only: ['DESTROY'], 1017 | onDestroyStart, 1018 | idAttribute: 'id' 1019 | }); 1020 | 1021 | const initialState = { 1022 | list: [], 1023 | entities: {} 1024 | }; 1025 | 1026 | destroyStart(initialState); 1027 | 1028 | t.is(initialState.isDestroying, true); 1029 | }); 1030 | 1031 | test('destroy start calls onDestroyStart', (t) => { 1032 | const onDestroyStart = sinon.spy(); 1033 | 1034 | const { destroyStart } = createMutations({ 1035 | only: ['DESTROY'], 1036 | onDestroyStart, 1037 | idAttribute: 'id' 1038 | }); 1039 | 1040 | const initialState = { 1041 | list: [], 1042 | entities: {} 1043 | }; 1044 | 1045 | destroyStart(initialState); 1046 | 1047 | t.true(onDestroyStart.calledWith(initialState)); 1048 | }); 1049 | 1050 | test('destroy success existing in list', (t) => { 1051 | const onDestroySuccess = sinon.spy(); 1052 | 1053 | const { destroySuccess } = createMutations({ 1054 | only: ['DESTROY'], 1055 | onDestroySuccess, 1056 | idAttribute: 'id' 1057 | }); 1058 | 1059 | const initialState = { 1060 | list: ['1', '5', '6'], 1061 | entities: { 1062 | 1: { 1063 | id: 1, 1064 | name: 'Bob' 1065 | }, 1066 | 1067 | 5: { 1068 | id: 5, 1069 | name: 'John' 1070 | }, 1071 | 1072 | 6: { 1073 | id: 6, 1074 | name: 'Jane' 1075 | } 1076 | } 1077 | }; 1078 | 1079 | const id = 1; 1080 | 1081 | destroySuccess(initialState, id); 1082 | 1083 | t.is(initialState.isDestroying, false); 1084 | 1085 | t.is(initialState.destroyError, null); 1086 | 1087 | t.falsy(initialState.entities['1']); 1088 | 1089 | t.deepEqual(initialState.list, ['5', '6']); 1090 | }); 1091 | 1092 | test('destroy success not existing in list', (t) => { 1093 | const onDestroySuccess = sinon.spy(); 1094 | 1095 | const { destroySuccess } = createMutations({ 1096 | only: ['DESTROY'], 1097 | onDestroySuccess, 1098 | idAttribute: 'id' 1099 | }); 1100 | 1101 | const initialState = { 1102 | list: ['1', '5', '6'], 1103 | entities: { 1104 | 1: { 1105 | name: 'Bob' 1106 | } 1107 | } 1108 | }; 1109 | 1110 | const id = 2; 1111 | 1112 | destroySuccess(initialState, id); 1113 | 1114 | t.is(initialState.isDestroying, false); 1115 | 1116 | t.is(initialState.destroyError, null); 1117 | 1118 | t.falsy(initialState.entities['2']); 1119 | 1120 | t.deepEqual(initialState.list, ['1', '5', '6']); 1121 | }); 1122 | 1123 | test('destroy success calls onDestroySuccess', (t) => { 1124 | const onDestroySuccess = sinon.spy(); 1125 | 1126 | const { destroySuccess } = createMutations({ 1127 | only: ['DESTROY'], 1128 | onDestroySuccess, 1129 | idAttribute: 'id' 1130 | }); 1131 | 1132 | const initialState = { 1133 | list: [], 1134 | entities: {} 1135 | }; 1136 | 1137 | const id = 1; 1138 | 1139 | destroySuccess(initialState, id, { id }); 1140 | 1141 | t.true(onDestroySuccess.calledWith(initialState, { id })); 1142 | }); 1143 | 1144 | test('destroy error', (t) => { 1145 | const onDestroyError = sinon.spy(); 1146 | 1147 | const { destroyError } = createMutations({ 1148 | only: ['DESTROY'], 1149 | onDestroyError, 1150 | idAttribute: 'id' 1151 | }); 1152 | 1153 | const initialState = { 1154 | list: ['1', '5', '6'], 1155 | entities: {} 1156 | }; 1157 | 1158 | const error = { message: 'Something went wrong' }; 1159 | 1160 | destroyError(initialState, error); 1161 | 1162 | t.is(initialState.isDestroying, false); 1163 | 1164 | t.is(initialState.destroyError, error); 1165 | 1166 | t.deepEqual(initialState.list, initialState.list); 1167 | t.deepEqual(initialState.entities, initialState.entities); 1168 | }); 1169 | 1170 | test('destroy error calls onDestroyError', (t) => { 1171 | const onDestroyError = sinon.spy(); 1172 | 1173 | const { destroyError } = createMutations({ 1174 | only: ['DESTROY'], 1175 | onDestroyError, 1176 | idAttribute: 'id' 1177 | }); 1178 | 1179 | const initialState = { 1180 | list: [], 1181 | entities: {} 1182 | }; 1183 | 1184 | const error = { message: 'Something went wrong' }; 1185 | 1186 | destroyError(initialState, error); 1187 | 1188 | t.true(onDestroyError.calledWith(initialState, error)); 1189 | }); 1190 | --------------------------------------------------------------------------------