├── 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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 | Back to Articles
5 |
6 |
7 |
8 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
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 |
2 |
3 |
4 | {{ article.title }}
5 |
6 |
7 |
8 | Edit (PATCH)
9 | |
10 | Replace (PUT)
11 | |
12 | Delete
13 |
14 |
15 | {{ article.content }}
16 |
17 |
18 |
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 | [](https://travis-ci.org/JiriChara/vuex-crud)
4 | [](https://codecov.io/gh/JiriChara/vuex-crud)
5 | [](https://www.npmjs.com/package/vuex-crud)
6 | [](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 |
67 |
68 |
69 | {{ article.title }}
70 | {{ article.content }}
71 |
72 |
73 |
74 |
75 |
110 | ```
111 |
112 | ```vue
113 |
114 |
115 |
116 |
117 |
118 | {{ currentArticle.title }}
119 | {{ currentArticle.content }}
120 |
121 |
122 |
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 |
--------------------------------------------------------------------------------