├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── actions.js ├── fetch.js ├── fetchAll.js ├── fetchAllEndpoint.js ├── fetchAllEndpointById.js ├── fetchById.js ├── fetchEndpoint.js ├── fetchEndpointById.js ├── helpers.js ├── index.js ├── reducer.js ├── request.js ├── requestAll.js ├── requestAllEndpoint.js ├── requestAllEndpointById.js ├── requestById.js ├── requestEndpoint.js ├── requestEndpointById.js └── requests.js ├── jestrc.js ├── package.json ├── src ├── actions.js ├── helpers.js ├── index.js ├── reducer.js └── requests.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "rules": { 4 | "func-names": 0, 5 | "no-tabs": 0, 6 | "class-methods-use-this": 0, 7 | "indent": ["error", "tab"] 8 | }, 9 | "globals": { 10 | "fetch": false 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/ 2 | /node_modules/ 3 | /dist/ 4 | /lib/ 5 | /npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: 5 | - "npm test" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v1.1.0 (2017-06-19) 4 | 5 | **Implemented enhancements:** 6 | 7 | - createActions and createRequests functions: `namespace` argument has been replaced with `args` to allow support multiple options, `namespace` argument becomes a part of `args` object. 8 | - createActions: added ability to skip some fetch functions generation. 9 | - createRequests: added ability to skip some request functions generation. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Eugene Manuilov 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-wordpress 2 | 3 | [![npm version](https://badge.fury.io/js/redux-wordpress.svg)](https://badge.fury.io/js/redux-wordpress) [![Build Status](https://travis-ci.org/eugene-manuilov/redux-wordpress.svg?branch=master)](https://travis-ci.org/eugene-manuilov/redux-wordpress) 4 | 5 | This package is intended to help you to build Redux actions and reducers for WordPress REST API endpoints. 6 | 7 | ## Installation 8 | 9 | You can add it to your project by running following NPM or Yarn command in your terminal: 10 | 11 | ``` 12 | npm install redux-wordpress --save 13 | ``` 14 | 15 | ``` 16 | yarn add redux-wordpress 17 | ``` 18 | 19 | This package uses ES6 fetch and promises to make AJAX requests, so you might also need to install `isomorphic-fetch` and `es6-promise` packages to make sure it works correctly during server rendering or in older browsers. 20 | 21 | ## Usage 22 | 23 | The package exports three function which you can use to create actions and build a reducer. 24 | 25 | ### createActions(name, host, endpoints, args) 26 | 27 | Returns an object with a set of function which you can use to fetch data from REST API. 28 | 29 | - **name** _(string)_ - Arbitrary name which will be used in action types to distinguish different actions. 30 | - **host** _(string)_ - URL address to your API's root. Usually it will look like: `http://mysite.com/wp-json/`. 31 | - **endpoints** _(array)_ - A list of endpoints which you want to build actions for. It could be something like `['posts', 'categories']`. 32 | - **args** _(object)_ - Optional. The options objects which supports following params: 33 | - **namespace** _(string)_ - The namespace for your endpoints. By default it is `wp/v2`. 34 | - **fetch** _(boolean)_ - Determines whether or not `fetch` function need to be generated. 35 | - **fetchEndpoint** _(boolean)_ - Determines whether or not `fetchEndpoint` function need to be generated. 36 | - **fetchById** _(boolean)_ - Determines whether or not `fetchById` function need to be generated. 37 | - **fetchEndpointById** _(boolean)_ - Determines whether or not `fetchEndpointById` function need to be generated. 38 | - **fetchAll** _(boolean)_ - Determines whether or not `fetchAll` function need to be generated. 39 | - **fetchAllEndpoint** _(boolean)_ - Determines whether or not `fetchAllEndpoint` function need to be generated. 40 | - **fetchAllEndpointById** _(boolean)_ - Determines whether or not `fetchAllEndpointById` function need to be generated. 41 | 42 | ```js 43 | // actionCreators.js 44 | 45 | import { createActions } from 'redux-wordpress'; 46 | 47 | const actions = createActions('my-api', 'http://mysite.test/wp-json/', ['books', 'authors']); 48 | export default actions; 49 | 50 | // will export: 51 | // 52 | // { 53 | // fetchBooks(params) { ... }, 54 | // fetchBooksEndpoint(endpoint, params) { ... }, 55 | // fetchBooksById(id, params) { ... }, 56 | // fetchBooksEndpointById(id, endpoint, params) { ... }, 57 | // fetchAllBooks(params) { ... }, 58 | // fetchAllBooksEndpoint(endpoint, params) { ... }, 59 | // fetchAllBooksEndpointById(id, endpoint, params) { ... }, 60 | // fetchAuthors(params) { ... }, 61 | // fetchAuthorsEndpoint(endpoint, params) { ... }, 62 | // fetchAuthorsById(id, params) { ... }, 63 | // fetchAuthorsEndpointById(id, endpoint, params) { ... }, 64 | // fetchAllAuthors(params) { ... }, 65 | // fetchAllAuthorsEndpoint(endpoint, params) { ... }, 66 | // fetchAllAuthorsEndpointById(id, endpoint, params) { ... } 67 | // } 68 | ``` 69 | 70 | ### createReducer(name) 71 | 72 | Returns a reducer function which you can use to catch data returned by a fetch action. 73 | 74 | - **name** _(string)_ - A name which will be used to catch proper actions. It should be the same name as you passed to `createActions` function. 75 | 76 | ```js 77 | // reducers.js 78 | 79 | import { createReducer } from 'redux-wordpress'; 80 | 81 | const rootReducer = combineReducers({ 82 | wp: createReducer('my-api') // name should match to what we passed to "createActions" function 83 | }); 84 | 85 | export default rootReducer; 86 | ``` 87 | ### createRequests(host, endpoints, args) 88 | 89 | Helper function which generates request functions to endpoints which you can use to group multiple requests into one action: 90 | 91 | - **host** _(string)_ - URL address to your API's root. Usually it will look like: `http://mysite.com/wp-json/`. 92 | - **endpoints** _(array)_ - A list of endpoints which you want to build actions for. It could be something like `['posts', 'categories']`. 93 | - **args** _(object)_ - Optional. The options objects which supports following params: 94 | - **namespace** _(string)_ - The namespace for your endpoints. By default it is `wp/v2`. 95 | - **request** _(boolean)_ - Determines whether or not `request` function need to be generated. 96 | - **requestEndpoint** _(boolean)_ - Determines whether or not `requestEndpoint` function need to be generated. 97 | - **requestById** _(boolean)_ - Determines whether or not `requestById` function need to be generated. 98 | - **requestEndpointById** _(boolean)_ - Determines whether or not `requestEndpointById` function need to be generated. 99 | - **requestAll** _(boolean)_ - Determines whether or not `requestAll` function need to be generated. 100 | - **requestAllEndpoint** _(boolean)_ - Determines whether or not `requestAllEndpoint` function need to be generated. 101 | - **requestAllEndpointById** _(boolean)_ - Determines whether or not `requestAllEndpointById` function need to be generated. 102 | 103 | ```js 104 | // actionCreators.js 105 | 106 | import { createRequests } from 'redux-wordpress'; 107 | 108 | const requests = createRequests('http://mysite.test/wp-json/', ['books', 'authors']); 109 | 110 | export function fetchInitialData() { 111 | return dispatch => { 112 | return Promise 113 | .all([ 114 | requests.requestBooks(...).then((data, response) => dispatch({action: 'books', data})), 115 | requests.requestAuthors(...).then((data, response) => dispatch({action: 'authors', data})) 116 | ]) 117 | .then(() => dispatch({action: 'loaded-initial-data'})); 118 | }; 119 | } 120 | ``` 121 | 122 | ## Contribute 123 | 124 | What to help or have a suggestion? Open a [new ticket](https://github.com/eugene-manuilov/redux-wordpress/issues/new) and we can discuss it or submit pull request. Please, make sure you run `npm test` before submitting a pull request. 125 | 126 | ## LICENSE 127 | 128 | The MIT License (MIT) 129 | -------------------------------------------------------------------------------- /__tests__/actions.js: -------------------------------------------------------------------------------- 1 | import { createActions } from '../lib/index'; 2 | 3 | test('Check actions created by createActions function', () => { 4 | const actions = createActions('test-rest-api', 'http://wordpress.test/wp-json/', ['books', 'authors']); 5 | 6 | expect(typeof actions.fetchBooks).toBe('function'); 7 | expect(typeof actions.fetchBooksEndpoint).toBe('function'); 8 | expect(typeof actions.fetchBooksById).toBe('function'); 9 | expect(typeof actions.fetchBooksEndpointById).toBe('function'); 10 | expect(typeof actions.fetchAllBooks).toBe('function'); 11 | expect(typeof actions.fetchAllBooksEndpoint).toBe('function'); 12 | expect(typeof actions.fetchAllBooksEndpointById).toBe('function'); 13 | 14 | expect(typeof actions.fetchAuthors).toBe('function'); 15 | expect(typeof actions.fetchAuthorsEndpoint).toBe('function'); 16 | expect(typeof actions.fetchAuthorsById).toBe('function'); 17 | expect(typeof actions.fetchAuthorsEndpointById).toBe('function'); 18 | expect(typeof actions.fetchAllAuthors).toBe('function'); 19 | expect(typeof actions.fetchAllAuthorsEndpoint).toBe('function'); 20 | expect(typeof actions.fetchAllAuthorsEndpointById).toBe('function'); 21 | }); 22 | 23 | test('Check ability to skip some actions', () => { 24 | const actions = createActions('test-rest-api', 'http://wordpress.test/wp-json/', ['books'], { 25 | fetch: false, 26 | fetchEndpoint: false, 27 | fetchById: false, 28 | fetchEndpointById: false, 29 | fetchAll: false, 30 | fetchAllEndpoint: false, 31 | fetchAllEndpointById: false, 32 | }); 33 | 34 | expect(typeof actions.fetchBooks).toBe('undefined'); 35 | expect(typeof actions.fetchBooksEndpoint).toBe('undefined'); 36 | expect(typeof actions.fetchBooksById).toBe('undefined'); 37 | expect(typeof actions.fetchBooksEndpointById).toBe('undefined'); 38 | expect(typeof actions.fetchAllBooks).toBe('undefined'); 39 | expect(typeof actions.fetchAllBooksEndpoint).toBe('undefined'); 40 | expect(typeof actions.fetchAllBooksEndpointById).toBe('undefined'); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/fetch.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('fetchBooks action creators', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('dispatches the correct action on successful fetch request', () => { 9 | const store = mockStore({}); 10 | const items = []; 11 | 12 | for (let i = 0, len = faker.random.number({ min: 1, max: 20 }); i < len; i++) { 13 | items.push({ 14 | "id": faker.random.number(), 15 | "date": "2017-04-13T20:02:35", 16 | "date_gmt": "2017-04-13T20:02:35", 17 | "guid": {"rendered": faker.internet.url()}, 18 | "modified": "2017-04-13T20:02:35", 19 | "modified_gmt": "2017-04-13T20:02:35", 20 | "slug": faker.lorem.slug(), 21 | "status": "publish", 22 | "type": "post", 23 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 24 | "title": { "rendered": faker.lorem.sentence() }, 25 | "content": { "rendered": faker.lorem.paragraphs(4) }, 26 | "excerpt": { "rendered": faker.lorem.paragraph() }, 27 | "author": faker.random.number(), 28 | "featured_media": faker.random.number(), 29 | "comment_status": "open", 30 | "ping_status": "open", 31 | "sticky": false, 32 | "template": "", 33 | "format": "standard", 34 | "meta": [], 35 | "categories": [faker.random.number()] 36 | }); 37 | } 38 | 39 | const mock = fetch.mockResponse(JSON.stringify(items), { 40 | status: 200, 41 | headers: new Headers({ 42 | 'X-WP-TotalPages': 1, 43 | 'X-WP-Total': items.length 44 | }), 45 | }); 46 | 47 | return store 48 | .dispatch(actions.fetchBooks(params)) 49 | .then(() => { 50 | const actions = store.getActions(); 51 | 52 | expect(actions.length).toBe(2); 53 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}?context=view`); 54 | 55 | expect(actions[0]).toEqual({ 56 | type: `@@wp/${name}/fetching/${endpoint}`, 57 | params 58 | }); 59 | 60 | expect(actions[1]).toEqual({ 61 | type: `@@wp/${name}/fetched/${endpoint}`, 62 | ok: true, 63 | total: items.length, 64 | totalPages: 1, 65 | results: items, 66 | params 67 | }); 68 | }); 69 | }); 70 | 71 | it('dispatches the correct action on 404 response', () => { 72 | const store = mockStore({}); 73 | const statusText = 'not-found'; 74 | 75 | const mock = fetch.mockResponse('', { 76 | status: 404, 77 | statusText: statusText, 78 | }); 79 | 80 | return store 81 | .dispatch(actions.fetchBooks(params)) 82 | .then(() => { 83 | const actions = store.getActions(); 84 | 85 | expect(actions.length).toBe(2); 86 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}?context=view`); 87 | 88 | expect(actions[0]).toEqual({ 89 | type: `@@wp/${name}/fetching/${endpoint}`, 90 | params 91 | }); 92 | 93 | expect(actions[1]).toEqual({ 94 | type: `@@wp/${name}/fetched/${endpoint}`, 95 | ok: false, 96 | message: statusText, 97 | params, 98 | }); 99 | }); 100 | }); 101 | }); -------------------------------------------------------------------------------- /__tests__/fetchAll.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('fetchAllBooks action creators', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('dispatches the correct action on successful fetch request', () => { 9 | const store = mockStore({}); 10 | const items = []; 11 | const per_page = faker.random.number({ min: 1, max: 10 }); 12 | const params = { context: 'view', per_page }; 13 | const pages = []; 14 | const mocks = []; 15 | 16 | for (let i = 0, len = faker.random.number({ min: 2, max: 10 }); i < len; i++) { 17 | const page = []; 18 | 19 | for (let j = 0; j < per_page; j++) { 20 | const item = { 21 | "id": faker.random.number(), 22 | "date": "2017-04-13T20:02:35", 23 | "date_gmt": "2017-04-13T20:02:35", 24 | "guid": { "rendered": faker.internet.url() }, 25 | "modified": "2017-04-13T20:02:35", 26 | "modified_gmt": "2017-04-13T20:02:35", 27 | "slug": faker.lorem.slug(), 28 | "status": "publish", 29 | "type": "post", 30 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 31 | "title": { "rendered": faker.lorem.sentence() }, 32 | "content": { "rendered": faker.lorem.paragraphs(4) }, 33 | "excerpt": { "rendered": faker.lorem.paragraph() }, 34 | "author": faker.random.number(), 35 | "featured_media": faker.random.number(), 36 | "comment_status": "open", 37 | "ping_status": "open", 38 | "sticky": false, 39 | "template": "", 40 | "format": "standard", 41 | "meta": [], 42 | "categories": [faker.random.number()] 43 | }; 44 | 45 | items.push(item); 46 | page.push(item); 47 | } 48 | 49 | pages.push(page); 50 | } 51 | 52 | pages.forEach((page) => { 53 | const mock = fetch.mockResponseOnce(JSON.stringify(page), { 54 | status: 200, 55 | headers: new Headers({ 56 | 'X-WP-TotalPages': pages.length, 57 | 'X-WP-Total': items.length 58 | }), 59 | }); 60 | 61 | mocks.push(mock); 62 | }); 63 | 64 | return store 65 | .dispatch(actions.fetchAllBooks(params)) 66 | .then(() => { 67 | const actions = store.getActions(); 68 | 69 | mocks.forEach((mock, i) => { 70 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}?context=view&page=${i + 1}&per_page=${per_page}`); 71 | }); 72 | 73 | expect(actions.length).toBe(2); 74 | 75 | expect(actions[0]).toEqual({ 76 | type: `@@wp/${name}/fetching-all/${endpoint}`, 77 | params 78 | }); 79 | 80 | expect(actions[1]).toEqual({ 81 | type: `@@wp/${name}/fetched-all/${endpoint}`, 82 | ok: true, 83 | total: items.length, 84 | totalPages: pages.length, 85 | results: items, 86 | params 87 | }); 88 | }); 89 | }); 90 | 91 | it('dispatches the correct action on 404 response', () => { 92 | const store = mockStore({}); 93 | const statusText = 'not-found'; 94 | 95 | const mock = fetch.mockResponse('', { 96 | status: 404, 97 | statusText: statusText, 98 | }); 99 | 100 | return store 101 | .dispatch(actions.fetchAllBooks(params)) 102 | .then(() => { 103 | const actions = store.getActions(); 104 | 105 | expect(actions.length).toBe(2); 106 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}?context=view&page=1&per_page=100`); 107 | 108 | expect(actions[0]).toEqual({ 109 | type: `@@wp/${name}/fetching-all/${endpoint}`, 110 | params 111 | }); 112 | 113 | expect(actions[1]).toEqual({ 114 | type: `@@wp/${name}/fetched-all/${endpoint}`, 115 | ok: false, 116 | message: statusText, 117 | params, 118 | }); 119 | }); 120 | }); 121 | }); -------------------------------------------------------------------------------- /__tests__/fetchAllEndpoint.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('fetchAllBooksEndpoint action creators', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('dispatches the correct action on successful fetch request', () => { 9 | const store = mockStore({}); 10 | const items = []; 11 | const per_page = faker.random.number({ min: 1, max: 10 }); 12 | const params = { context: 'view', per_page }; 13 | const pages = []; 14 | const mocks = []; 15 | 16 | for (let i = 0, len = faker.random.number({ min: 2, max: 10 }); i < len; i++) { 17 | const page = []; 18 | 19 | for (let j = 0; j < per_page; j++) { 20 | const item = { 21 | "id": faker.random.number(), 22 | "date": "2017-04-13T20:02:35", 23 | "date_gmt": "2017-04-13T20:02:35", 24 | "guid": { "rendered": faker.internet.url() }, 25 | "modified": "2017-04-13T20:02:35", 26 | "modified_gmt": "2017-04-13T20:02:35", 27 | "slug": faker.lorem.slug(), 28 | "status": "publish", 29 | "type": "post", 30 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 31 | "title": { "rendered": faker.lorem.sentence() }, 32 | "content": { "rendered": faker.lorem.paragraphs(4) }, 33 | "excerpt": { "rendered": faker.lorem.paragraph() }, 34 | "author": faker.random.number(), 35 | "featured_media": faker.random.number(), 36 | "comment_status": "open", 37 | "ping_status": "open", 38 | "sticky": false, 39 | "template": "", 40 | "format": "standard", 41 | "meta": [], 42 | "categories": [faker.random.number()] 43 | }; 44 | 45 | items.push(item); 46 | page.push(item); 47 | } 48 | 49 | pages.push(page); 50 | } 51 | 52 | pages.forEach((page) => { 53 | const mock = fetch.mockResponseOnce(JSON.stringify(page), { 54 | status: 200, 55 | headers: new Headers({ 56 | 'X-WP-TotalPages': pages.length, 57 | 'X-WP-Total': items.length 58 | }), 59 | }); 60 | 61 | mocks.push(mock); 62 | }); 63 | 64 | return store 65 | .dispatch(actions.fetchAllBooksEndpoint(endpoint2, params)) 66 | .then(() => { 67 | const actions = store.getActions(); 68 | 69 | mocks.forEach((mock, i) => { 70 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${endpoint2}?context=view&page=${i + 1}&per_page=${per_page}`); 71 | }); 72 | 73 | expect(actions.length).toBe(2); 74 | 75 | expect(actions[0]).toEqual({ 76 | type: `@@wp/${name}/fetching-all/${endpoint}/${endpoint2}`, 77 | params 78 | }); 79 | 80 | expect(actions[1]).toEqual({ 81 | type: `@@wp/${name}/fetched-all/${endpoint}/${endpoint2}`, 82 | ok: true, 83 | total: items.length, 84 | totalPages: pages.length, 85 | results: items, 86 | params 87 | }); 88 | }); 89 | }); 90 | 91 | it('dispatches the correct action on 404 response', () => { 92 | const store = mockStore({}); 93 | const statusText = 'not-found'; 94 | 95 | const mock = fetch.mockResponse('', { 96 | status: 404, 97 | statusText: statusText, 98 | }); 99 | 100 | return store 101 | .dispatch(actions.fetchAllBooksEndpoint(endpoint2, params)) 102 | .then(() => { 103 | const actions = store.getActions(); 104 | 105 | expect(actions.length).toBe(2); 106 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${endpoint2}?context=view&page=1&per_page=100`); 107 | 108 | expect(actions[0]).toEqual({ 109 | type: `@@wp/${name}/fetching-all/${endpoint}/${endpoint2}`, 110 | params 111 | }); 112 | 113 | expect(actions[1]).toEqual({ 114 | type: `@@wp/${name}/fetched-all/${endpoint}/${endpoint2}`, 115 | ok: false, 116 | message: statusText, 117 | params, 118 | }); 119 | }); 120 | }); 121 | }); -------------------------------------------------------------------------------- /__tests__/fetchAllEndpointById.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('fetchAllBooksEndpointById action creators', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('dispatches the correct action on successful fetch request', () => { 9 | const store = mockStore({}); 10 | const items = []; 11 | const per_page = faker.random.number({ min: 1, max: 10 }); 12 | const params = { context: 'view', per_page }; 13 | const pages = []; 14 | const mocks = []; 15 | const id = faker.random.number(); 16 | 17 | for (let i = 0, len = faker.random.number({ min: 2, max: 10 }); i < len; i++) { 18 | const page = []; 19 | 20 | for (let j = 0; j < per_page; j++) { 21 | const item = { 22 | "id": faker.random.number(), 23 | "date": "2017-04-13T20:02:35", 24 | "date_gmt": "2017-04-13T20:02:35", 25 | "guid": { "rendered": faker.internet.url() }, 26 | "modified": "2017-04-13T20:02:35", 27 | "modified_gmt": "2017-04-13T20:02:35", 28 | "slug": faker.lorem.slug(), 29 | "status": "publish", 30 | "type": "post", 31 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 32 | "title": { "rendered": faker.lorem.sentence() }, 33 | "content": { "rendered": faker.lorem.paragraphs(4) }, 34 | "excerpt": { "rendered": faker.lorem.paragraph() }, 35 | "author": faker.random.number(), 36 | "featured_media": faker.random.number(), 37 | "comment_status": "open", 38 | "ping_status": "open", 39 | "sticky": false, 40 | "template": "", 41 | "format": "standard", 42 | "meta": [], 43 | "categories": [faker.random.number()] 44 | }; 45 | 46 | items.push(item); 47 | page.push(item); 48 | } 49 | 50 | pages.push(page); 51 | } 52 | 53 | pages.forEach((page) => { 54 | const mock = fetch.mockResponseOnce(JSON.stringify(page), { 55 | status: 200, 56 | headers: new Headers({ 57 | 'X-WP-TotalPages': pages.length, 58 | 'X-WP-Total': items.length 59 | }), 60 | }); 61 | 62 | mocks.push(mock); 63 | }); 64 | 65 | return store 66 | .dispatch(actions.fetchAllBooksEndpointById(id, endpoint2, params)) 67 | .then(() => { 68 | const actions = store.getActions(); 69 | 70 | mocks.forEach((mock, i) => { 71 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}/${endpoint2}?context=view&page=${i + 1}&per_page=${per_page}`); 72 | }); 73 | 74 | expect(actions.length).toBe(2); 75 | 76 | expect(actions[0]).toEqual({ 77 | type: `@@wp/${name}/fetching-all-by-id/${endpoint}/${endpoint2}`, 78 | id, 79 | params, 80 | }); 81 | 82 | expect(actions[1]).toEqual({ 83 | type: `@@wp/${name}/fetched-all-by-id/${endpoint}/${endpoint2}`, 84 | ok: true, 85 | total: items.length, 86 | totalPages: pages.length, 87 | results: items, 88 | id, 89 | params, 90 | }); 91 | }); 92 | }); 93 | 94 | it('dispatches the correct action on 404 response', () => { 95 | const store = mockStore({}); 96 | const statusText = 'not-found'; 97 | const id = faker.random.number(); 98 | 99 | const mock = fetch.mockResponse('', { 100 | status: 404, 101 | statusText: statusText, 102 | }); 103 | 104 | return store 105 | .dispatch(actions.fetchAllBooksEndpointById(id, endpoint2, params)) 106 | .then(() => { 107 | const actions = store.getActions(); 108 | 109 | expect(actions.length).toBe(2); 110 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}/${endpoint2}?context=view&page=1&per_page=100`); 111 | 112 | expect(actions[0]).toEqual({ 113 | type: `@@wp/${name}/fetching-all-by-id/${endpoint}/${endpoint2}`, 114 | id, 115 | params, 116 | }); 117 | 118 | expect(actions[1]).toEqual({ 119 | type: `@@wp/${name}/fetched-all-by-id/${endpoint}/${endpoint2}`, 120 | ok: false, 121 | message: statusText, 122 | id, 123 | params, 124 | }); 125 | }); 126 | }); 127 | }); -------------------------------------------------------------------------------- /__tests__/fetchById.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('fetchBooksById action creators', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('dispatches the correct action on successful fetch request', () => { 9 | const store = mockStore({}); 10 | const item = { 11 | "id": faker.random.number(), 12 | "date": "2017-04-13T20:02:35", 13 | "date_gmt": "2017-04-13T20:02:35", 14 | "guid": {"rendered": faker.internet.url()}, 15 | "modified": "2017-04-13T20:02:35", 16 | "modified_gmt": "2017-04-13T20:02:35", 17 | "slug": faker.lorem.slug(), 18 | "status": "publish", 19 | "type": "post", 20 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 21 | "title": { "rendered": faker.lorem.sentence() }, 22 | "content": { "rendered": faker.lorem.paragraphs(4) }, 23 | "excerpt": { "rendered": faker.lorem.paragraph() }, 24 | "author": faker.random.number(), 25 | "featured_media": faker.random.number(), 26 | "comment_status": "open", 27 | "ping_status": "open", 28 | "sticky": false, 29 | "template": "", 30 | "format": "standard", 31 | "meta": [], 32 | "categories": [faker.random.number()] 33 | }; 34 | 35 | const mock = fetch.mockResponse(JSON.stringify(item), { status: 200 }); 36 | 37 | return store 38 | .dispatch(actions.fetchBooksById(item.id, params)) 39 | .then(() => { 40 | const actions = store.getActions(); 41 | 42 | expect(actions.length).toBe(2); 43 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${item.id}?context=view`); 44 | 45 | expect(actions[0]).toEqual({ 46 | type: `@@wp/${name}/fetching-by-id/${endpoint}`, 47 | id: item.id, 48 | params, 49 | }); 50 | 51 | expect(actions[1]).toEqual({ 52 | type: `@@wp/${name}/fetched-by-id/${endpoint}`, 53 | ok: true, 54 | results: item, 55 | id: item.id, 56 | params, 57 | }); 58 | }); 59 | }); 60 | 61 | it('dispatches the correct action on 404 response', () => { 62 | const store = mockStore({}); 63 | const statusText = 'not-found'; 64 | const id = faker.random.number(); 65 | 66 | const mock = fetch.mockResponse('', { 67 | status: 404, 68 | statusText: statusText, 69 | }); 70 | 71 | return store 72 | .dispatch(actions.fetchBooksById(id, params)) 73 | .then(() => { 74 | const actions = store.getActions(); 75 | 76 | expect(actions.length).toBe(2); 77 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}?context=view`); 78 | 79 | expect(actions[0]).toEqual({ 80 | type: `@@wp/${name}/fetching-by-id/${endpoint}`, 81 | id: id, 82 | params, 83 | }); 84 | 85 | expect(actions[1]).toEqual({ 86 | type: `@@wp/${name}/fetched-by-id/${endpoint}`, 87 | ok: false, 88 | id: id, 89 | message: statusText, 90 | params, 91 | }); 92 | }); 93 | }); 94 | }); -------------------------------------------------------------------------------- /__tests__/fetchEndpoint.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('fetchBooksEndpoint action creators', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('dispatches the correct action on successful fetch request', () => { 9 | const store = mockStore({}); 10 | const items = []; 11 | 12 | for (let i = 0, len = faker.random.number({ min: 1, max: 20 }); i < len; i++) { 13 | items.push({ 14 | "id": faker.random.number(), 15 | "title": faker.lorem.sentence() 16 | }); 17 | } 18 | 19 | const mock = fetch.mockResponse(JSON.stringify(items), { 20 | status: 200, 21 | headers: new Headers({ 22 | 'X-WP-TotalPages': 1, 23 | 'X-WP-Total': items.length 24 | }), 25 | }); 26 | 27 | return store 28 | .dispatch(actions.fetchBooksEndpoint(endpoint2, params)) 29 | .then(() => { 30 | const actions = store.getActions(); 31 | 32 | expect(actions.length).toBe(2); 33 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${endpoint2}?context=view`); 34 | 35 | expect(actions[0]).toEqual({ 36 | type: `@@wp/${name}/fetching/${endpoint}/${endpoint2}`, 37 | params, 38 | }); 39 | 40 | expect(actions[1]).toEqual({ 41 | type: `@@wp/${name}/fetched/${endpoint}/${endpoint2}`, 42 | ok: true, 43 | total: items.length, 44 | totalPages: 1, 45 | results: items, 46 | params, 47 | }); 48 | }); 49 | }); 50 | 51 | it('dispatches the correct action on 404 response', () => { 52 | const store = mockStore({}); 53 | const statusText = 'not-found'; 54 | 55 | const mock = fetch.mockResponse('', { 56 | status: 404, 57 | statusText: statusText, 58 | }); 59 | 60 | return store 61 | .dispatch(actions.fetchBooksEndpoint(endpoint2, params)) 62 | .then(() => { 63 | const actions = store.getActions(); 64 | 65 | expect(actions.length).toBe(2); 66 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${endpoint2}?context=view`); 67 | 68 | expect(actions[0]).toEqual({ 69 | type: `@@wp/${name}/fetching/${endpoint}/${endpoint2}`, 70 | params, 71 | }); 72 | 73 | expect(actions[1]).toEqual({ 74 | type: `@@wp/${name}/fetched/${endpoint}/${endpoint2}`, 75 | ok: false, 76 | message: statusText, 77 | params, 78 | }); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /__tests__/fetchEndpointById.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('fetchBooksEndpointById action creators', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('dispatches the correct action on successful fetch request', () => { 9 | const store = mockStore({}); 10 | const id = faker.random.number(); 11 | const item = { 12 | "id": faker.random.number(), 13 | "title": faker.lorem.sentence() 14 | }; 15 | 16 | const mock = fetch.mockResponse(JSON.stringify(item), { status: 200 }); 17 | 18 | return store 19 | .dispatch(actions.fetchBooksEndpointById(id, endpoint2, params)) 20 | .then(() => { 21 | const actions = store.getActions(); 22 | 23 | expect(actions.length).toBe(2); 24 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}/${endpoint2}?context=view`); 25 | 26 | expect(actions[0]).toEqual({ 27 | type: `@@wp/${name}/fetching-by-id/${endpoint}/${endpoint2}`, 28 | id, 29 | params, 30 | }); 31 | 32 | expect(actions[1]).toEqual({ 33 | type: `@@wp/${name}/fetched-by-id/${endpoint}/${endpoint2}`, 34 | ok: true, 35 | results: item, 36 | id, 37 | params 38 | }); 39 | }); 40 | }); 41 | 42 | it('dispatches the correct action on 404 response', () => { 43 | const store = mockStore({}); 44 | const statusText = 'not-found'; 45 | const id = faker.random.number(); 46 | 47 | const mock = fetch.mockResponse('', { 48 | status: 404, 49 | statusText: statusText, 50 | }); 51 | 52 | return store 53 | .dispatch(actions.fetchBooksEndpointById(id, endpoint2, params)) 54 | .then(() => { 55 | const actions = store.getActions(); 56 | 57 | expect(actions.length).toBe(2); 58 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}/${endpoint2}?context=view`); 59 | 60 | expect(actions[0]).toEqual({ 61 | type: `@@wp/${name}/fetching-by-id/${endpoint}/${endpoint2}`, 62 | id, 63 | params, 64 | }); 65 | 66 | expect(actions[1]).toEqual({ 67 | type: `@@wp/${name}/fetched-by-id/${endpoint}/${endpoint2}`, 68 | ok: false, 69 | message: statusText, 70 | id, 71 | params, 72 | }); 73 | }); 74 | }); 75 | }); -------------------------------------------------------------------------------- /__tests__/helpers.js: -------------------------------------------------------------------------------- 1 | import * as helpers from '../lib/helpers'; 2 | 3 | test('upperFirst function', () => { 4 | expect(helpers.upperFirst('first second third')).toBe('FirstSecondThird'); 5 | expect(helpers.upperFirst('first-second-third')).toBe('FirstSecondThird'); 6 | expect(helpers.upperFirst('first_second_third')).toBe('FirstSecondThird'); 7 | expect(helpers.upperFirst('first_second third')).toBe('FirstSecondThird'); 8 | expect(helpers.upperFirst('first_second-third')).toBe('FirstSecondThird'); 9 | expect(helpers.upperFirst('first second-third')).toBe('FirstSecondThird'); 10 | expect(helpers.upperFirst('firstsecondthird')).toBe('Firstsecondthird'); 11 | }); 12 | 13 | test('trimEnd function', () => { 14 | expect(helpers.trimEnd('http://test.com/', '/')).toBe('http://test.com'); 15 | expect(helpers.trimEnd('http://test.com/', ' ')).toBe('http://test.com/'); 16 | expect(helpers.trimEnd('http://test.com/ ', ' ')).toBe('http://test.com/'); 17 | expect(helpers.trimEnd(' http://test.com/ ', ' ')).toBe(' http://test.com/'); 18 | }); 19 | 20 | test('qs function', () => { 21 | expect(helpers.qs({a: 1, b: 2})).toBe('a=1&b=2'); 22 | expect(helpers.qs({b: 2, a: 1})).toBe('a=1&b=2'); 23 | expect(helpers.qs({a: 1, b: 2, a: 3})).toBe('a=3&b=2'); 24 | expect(helpers.qs({a: [1, 2, 3], b: 2})).toBe('a[0]=1&a[1]=2&a[2]=3&b=2'); 25 | expect(helpers.qs({a: {c: 1, d: 2}, b: {c: 3, d: 4}})).toBe('a[c]=1&a[d]=2&b[c]=3&b[d]=4'); 26 | expect(helpers.qs({a: {c: [1, 2, 3], d: 4}, b: {c: 5, d: 6}})).toBe('a[c][0]=1&a[c][1]=2&a[c][2]=3&a[d]=4&b[c]=5&b[d]=6'); 27 | expect(helpers.qs({})).toBe(''); 28 | }); -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | import ReduxWordPress from '../lib/index'; 2 | import * as reduxwp from '../lib/index'; 3 | 4 | test('Check default export', () => { 5 | expect(typeof ReduxWordPress.createReducer).toBe('function'); 6 | expect(typeof ReduxWordPress.createActions).toBe('function'); 7 | expect(typeof ReduxWordPress.createRequests).toBe('function'); 8 | }); 9 | 10 | test('Check named export', () => { 11 | expect(typeof reduxwp.createReducer).toBe('function'); 12 | expect(typeof reduxwp.createActions).toBe('function'); 13 | expect(typeof reduxwp.createRequests).toBe('function'); 14 | }); -------------------------------------------------------------------------------- /__tests__/reducer.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import { createReducer } from '../lib/index'; 3 | 4 | const name = 'wp-api'; 5 | const reducer = createReducer(name); 6 | 7 | const getTestData = () => { 8 | const data = []; 9 | 10 | for (let i = 0, len = faker.random.number({min: 1, max: 20}); i < len; i++) { 11 | data.push({id: faker.random.number(), title: faker.lorem.sentence()}); 12 | } 13 | 14 | return data; 15 | }; 16 | 17 | test('Test createReducer creates a function', () => { 18 | expect(typeof reducer).toBe('function'); 19 | }); 20 | 21 | test('Test original state has not changed on unknown action', () => { 22 | const state = {test: faker.random.number(), data: [1, 2, 3]}; 23 | expect(reducer(state, {type: 'unknown'})).toEqual(state); 24 | }); 25 | 26 | test('Test original state has not changed on unsuccessful result', () => { 27 | const state = {test: faker.random.number(), data: [1, 2, 3]}; 28 | 29 | expect(reducer(state, {type: `@@wp/${name}/fetched/books`, ok: false})).toEqual(state); 30 | expect(reducer(state, {type: `@@wp/${name}/fetched/books/chapters`, ok: false})).toEqual(state); 31 | expect(reducer(state, {type: `@@wp/${name}/fetched-all/books`, ok: false})).toEqual(state); 32 | expect(reducer(state, {type: `@@wp/${name}/fetched-all/books/chapters`, ok: false})).toEqual(state); 33 | expect(reducer(state, {type: `@@wp/${name}/fetched-by-id/books`, ok: false})).toEqual(state); 34 | expect(reducer(state, {type: `@@wp/${name}/fetched-by-id/books/chapters`, ok: false})).toEqual(state); 35 | expect(reducer(state, {type: `@@wp/${name}/fetched-all-by-id/books/chapters`, ok: false})).toEqual(state); 36 | }); 37 | 38 | describe('Fetch reducer', () => { 39 | const initialState = { 40 | authors: { 41 | data: [], 42 | total: faker.random.number(), 43 | totalPages: faker.random.number() 44 | } 45 | }; 46 | 47 | test('state change on fetch success', () => { 48 | const state = Object.assign({}, initialState); 49 | const totalPages = faker.random.number(); 50 | const data = getTestData(); 51 | 52 | const action = { 53 | type: `@@wp/${name}/fetched/books`, 54 | ok: true, 55 | results: data, 56 | total: data.length, 57 | totalPages 58 | }; 59 | 60 | const result = Object.assign({}, state, { 61 | books: { 62 | data: data, 63 | total: data.length, 64 | totalPages 65 | } 66 | }); 67 | 68 | expect(reducer(state, action)).toEqual(result); 69 | }); 70 | 71 | test('state change on fetch-all success', () => { 72 | const state = Object.assign({}, initialState); 73 | const totalPages = faker.random.number(); 74 | const data = getTestData(); 75 | 76 | const action = { 77 | type: `@@wp/${name}/fetched-all/books`, 78 | ok: true, 79 | results: data, 80 | total: data.length, 81 | totalPages 82 | }; 83 | 84 | const result = Object.assign({}, state, { 85 | books: { 86 | data: data, 87 | total: data.length, 88 | totalPages 89 | } 90 | }); 91 | 92 | expect(reducer(state, action)).toEqual(result); 93 | }); 94 | }); 95 | 96 | describe('Fetch-by-id reducer', () => { 97 | test('Test state change on fetch-by-id success', () => { 98 | const authors = { 99 | data: [], 100 | total: faker.random.number(), 101 | totalPages: faker.random.number() 102 | }; 103 | 104 | const state = { 105 | authors 106 | }; 107 | 108 | const id = faker.random.number(); 109 | const data = faker.lorem.sentence(); 110 | 111 | const action = { 112 | type: `@@wp/${name}/fetched-by-id/books`, 113 | ok: true, 114 | id: id, 115 | result: data 116 | }; 117 | 118 | const obj = {}; 119 | obj[`books/${id}`] = { data }; 120 | 121 | expect(reducer(state, action)).toEqual(Object.assign({}, state, obj)); 122 | }); 123 | }); 124 | 125 | describe('Fetch-endpoint reducer', () => { 126 | const initialState = { 127 | authors: { 128 | data: [], 129 | total: faker.random.number(), 130 | totalPages: faker.random.number() 131 | } 132 | }; 133 | 134 | test('state change on fetch-endpoint success', () => { 135 | const state = Object.assign({}, initialState); 136 | const totalPages = faker.random.number(); 137 | const data = getTestData(); 138 | 139 | const action = { 140 | type: `@@wp/${name}/fetched/books/chapters`, 141 | ok: true, 142 | results: data, 143 | total: data.length, 144 | totalPages 145 | }; 146 | 147 | const result = Object.assign({}, state, { 148 | "books/chapters": { 149 | data: data, 150 | total: data.length, 151 | totalPages 152 | } 153 | }); 154 | 155 | expect(reducer(state, action)).toEqual(result); 156 | }); 157 | 158 | test('state change on fetch-all-enpoint success', () => { 159 | const state = Object.assign({}, initialState); 160 | const totalPages = faker.random.number(); 161 | const data = getTestData(); 162 | 163 | const action = { 164 | type: `@@wp/${name}/fetched-all/books/chapters`, 165 | ok: true, 166 | results: data, 167 | total: data.length, 168 | totalPages 169 | }; 170 | 171 | const result = Object.assign({}, state, { 172 | "books/chapters": { 173 | data: data, 174 | total: data.length, 175 | totalPages 176 | } 177 | }); 178 | 179 | expect(reducer(state, action)).toEqual(result); 180 | }); 181 | }); 182 | 183 | describe('Fetch-endpoint-by-id reducer', () => { 184 | const initialState = { 185 | authors: { 186 | data: [], 187 | total: faker.random.number(), 188 | totalPages: faker.random.number() 189 | } 190 | }; 191 | 192 | test('state change on fetch-endpoint success', () => { 193 | const state = Object.assign({}, initialState); 194 | const id = faker.random.number(); 195 | const data = faker.lorem.sentence(); 196 | 197 | const action = { 198 | type: `@@wp/${name}/fetched-by-id/books/chapters`, 199 | ok: true, 200 | id: id, 201 | result: data 202 | }; 203 | 204 | const obj = {}; 205 | obj[`books/${id}/chapters`] = { data }; 206 | 207 | expect(reducer(state, action)).toEqual(Object.assign({}, state, obj)); 208 | }); 209 | 210 | test('state change on fetch-all-enpoint success', () => { 211 | const state = Object.assign({}, initialState); 212 | const id = faker.random.number(); 213 | const data = faker.lorem.sentence(); 214 | 215 | const action = { 216 | type: `@@wp/${name}/fetched-all-by-id/books/chapters`, 217 | ok: true, 218 | id: id, 219 | result: data 220 | }; 221 | 222 | const obj = {}; 223 | obj[`books/${id}/chapters`] = { data }; 224 | 225 | expect(reducer(state, action)).toEqual(Object.assign({}, state, obj)); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /__tests__/request.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('requestBooks function', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('resolves the correct json on successful request', () => { 9 | const items = []; 10 | 11 | for (let i = 0, len = faker.random.number({ min: 1, max: 20 }); i < len; i++) { 12 | items.push({ 13 | "id": faker.random.number(), 14 | "date": "2017-04-13T20:02:35", 15 | "date_gmt": "2017-04-13T20:02:35", 16 | "guid": {"rendered": faker.internet.url()}, 17 | "modified": "2017-04-13T20:02:35", 18 | "modified_gmt": "2017-04-13T20:02:35", 19 | "slug": faker.lorem.slug(), 20 | "status": "publish", 21 | "type": "post", 22 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 23 | "title": { "rendered": faker.lorem.sentence() }, 24 | "content": { "rendered": faker.lorem.paragraphs(4) }, 25 | "excerpt": { "rendered": faker.lorem.paragraph() }, 26 | "author": faker.random.number(), 27 | "featured_media": faker.random.number(), 28 | "comment_status": "open", 29 | "ping_status": "open", 30 | "sticky": false, 31 | "template": "", 32 | "format": "standard", 33 | "meta": [], 34 | "categories": [faker.random.number()] 35 | }); 36 | } 37 | 38 | const mock = fetch.mockResponse(JSON.stringify(items), { 39 | status: 200, 40 | headers: new Headers({ 41 | 'X-WP-TotalPages': 1, 42 | 'X-WP-Total': items.length 43 | }), 44 | }); 45 | 46 | return requests.requestBooks(params).then((data) => { 47 | expect(data.json).toBeDefined(); 48 | expect(data.json).toEqual(items); 49 | 50 | expect(data.response).toBeDefined(); 51 | 52 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}?context=view`); 53 | }); 54 | }); 55 | 56 | it('rejects request on error', () => { 57 | const statusText = 'not-found'; 58 | const mock = fetch.mockResponse('', { 59 | status: 404, 60 | statusText: statusText, 61 | }); 62 | 63 | return requests.requestBooks(params).catch((error) => { 64 | expect(error).toBe(statusText); 65 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}?context=view`); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /__tests__/requestAll.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('requestAllBooks function', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('resolves the correct json on successfull request', () => { 9 | const items = []; 10 | const per_page = faker.random.number({ min: 1, max: 10 }); 11 | const params = { context: 'view', per_page }; 12 | const pages = []; 13 | const mocks = []; 14 | 15 | for (let i = 0, len = faker.random.number({ min: 2, max: 10 }); i < len; i++) { 16 | const page = []; 17 | 18 | for (let j = 0; j < per_page; j++) { 19 | const item = { 20 | "id": faker.random.number(), 21 | "date": "2017-04-13T20:02:35", 22 | "date_gmt": "2017-04-13T20:02:35", 23 | "guid": { "rendered": faker.internet.url() }, 24 | "modified": "2017-04-13T20:02:35", 25 | "modified_gmt": "2017-04-13T20:02:35", 26 | "slug": faker.lorem.slug(), 27 | "status": "publish", 28 | "type": "post", 29 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 30 | "title": { "rendered": faker.lorem.sentence() }, 31 | "content": { "rendered": faker.lorem.paragraphs(4) }, 32 | "excerpt": { "rendered": faker.lorem.paragraph() }, 33 | "author": faker.random.number(), 34 | "featured_media": faker.random.number(), 35 | "comment_status": "open", 36 | "ping_status": "open", 37 | "sticky": false, 38 | "template": "", 39 | "format": "standard", 40 | "meta": [], 41 | "categories": [faker.random.number()] 42 | }; 43 | 44 | items.push(item); 45 | page.push(item); 46 | } 47 | 48 | pages.push(page); 49 | } 50 | 51 | pages.forEach((page) => { 52 | const mock = fetch.mockResponseOnce(JSON.stringify(page), { 53 | status: 200, 54 | headers: new Headers({ 55 | 'X-WP-TotalPages': pages.length, 56 | 'X-WP-Total': items.length 57 | }), 58 | }); 59 | 60 | mocks.push(mock); 61 | }); 62 | 63 | return requests.requestAllBooks(params).then((data) => { 64 | expect(data.json).toBeDefined(); 65 | expect(data.json).toEqual(items); 66 | 67 | expect(data.response).toBeDefined(); 68 | 69 | mocks.forEach((mock, i) => { 70 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}?context=view&page=${i + 1}&per_page=${per_page}`); 71 | }); 72 | }); 73 | }); 74 | 75 | it('rejects request on error', () => { 76 | const statusText = 'not-found'; 77 | 78 | const mock = fetch.mockResponse('', { 79 | status: 404, 80 | statusText: statusText, 81 | }); 82 | 83 | return requests.requestAllBooks(params).catch((error) => { 84 | expect(error).toBe(statusText); 85 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}?context=view&page=1&per_page=100`); 86 | }); 87 | }); 88 | }); -------------------------------------------------------------------------------- /__tests__/requestAllEndpoint.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('requestAllBooksEndpoint function', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('resolves the correct json on successful request', () => { 9 | const items = []; 10 | const per_page = faker.random.number({ min: 1, max: 10 }); 11 | const params = { context: 'view', per_page }; 12 | const pages = []; 13 | const mocks = []; 14 | 15 | for (let i = 0, len = faker.random.number({ min: 2, max: 10 }); i < len; i++) { 16 | const page = []; 17 | 18 | for (let j = 0; j < per_page; j++) { 19 | const item = { 20 | "id": faker.random.number(), 21 | "date": "2017-04-13T20:02:35", 22 | "date_gmt": "2017-04-13T20:02:35", 23 | "guid": { "rendered": faker.internet.url() }, 24 | "modified": "2017-04-13T20:02:35", 25 | "modified_gmt": "2017-04-13T20:02:35", 26 | "slug": faker.lorem.slug(), 27 | "status": "publish", 28 | "type": "post", 29 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 30 | "title": { "rendered": faker.lorem.sentence() }, 31 | "content": { "rendered": faker.lorem.paragraphs(4) }, 32 | "excerpt": { "rendered": faker.lorem.paragraph() }, 33 | "author": faker.random.number(), 34 | "featured_media": faker.random.number(), 35 | "comment_status": "open", 36 | "ping_status": "open", 37 | "sticky": false, 38 | "template": "", 39 | "format": "standard", 40 | "meta": [], 41 | "categories": [faker.random.number()] 42 | }; 43 | 44 | items.push(item); 45 | page.push(item); 46 | } 47 | 48 | pages.push(page); 49 | } 50 | 51 | pages.forEach((page) => { 52 | const mock = fetch.mockResponseOnce(JSON.stringify(page), { 53 | status: 200, 54 | headers: new Headers({ 55 | 'X-WP-TotalPages': pages.length, 56 | 'X-WP-Total': items.length 57 | }), 58 | }); 59 | 60 | mocks.push(mock); 61 | }); 62 | 63 | return requests.requestAllBooksEndpoint(endpoint2, params).then((data) => { 64 | expect(data.json).toBeDefined(); 65 | expect(data.json).toEqual(items); 66 | 67 | expect(data.response).toBeDefined(); 68 | 69 | mocks.forEach((mock, i) => { 70 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${endpoint2}?context=view&page=${i + 1}&per_page=${per_page}`); 71 | }); 72 | }); 73 | }); 74 | 75 | it('rejects request on error', () => { 76 | const statusText = 'not-found'; 77 | 78 | const mock = fetch.mockResponse('', { 79 | status: 404, 80 | statusText: statusText, 81 | }); 82 | 83 | return requests.requestAllBooksEndpoint(endpoint2, params).catch((error) => { 84 | expect(error).toBe(statusText); 85 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${endpoint2}?context=view&page=1&per_page=100`); 86 | }); 87 | }); 88 | }); -------------------------------------------------------------------------------- /__tests__/requestAllEndpointById.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('requestAllBooksEndpointById function', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('resolves the correct json on successful request', () => { 9 | const items = []; 10 | const per_page = faker.random.number({ min: 1, max: 10 }); 11 | const params = { context: 'view', per_page }; 12 | const pages = []; 13 | const mocks = []; 14 | const id = faker.random.number(); 15 | 16 | for (let i = 0, len = faker.random.number({ min: 2, max: 10 }); i < len; i++) { 17 | const page = []; 18 | 19 | for (let j = 0; j < per_page; j++) { 20 | const item = { 21 | "id": faker.random.number(), 22 | "date": "2017-04-13T20:02:35", 23 | "date_gmt": "2017-04-13T20:02:35", 24 | "guid": { "rendered": faker.internet.url() }, 25 | "modified": "2017-04-13T20:02:35", 26 | "modified_gmt": "2017-04-13T20:02:35", 27 | "slug": faker.lorem.slug(), 28 | "status": "publish", 29 | "type": "post", 30 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 31 | "title": { "rendered": faker.lorem.sentence() }, 32 | "content": { "rendered": faker.lorem.paragraphs(4) }, 33 | "excerpt": { "rendered": faker.lorem.paragraph() }, 34 | "author": faker.random.number(), 35 | "featured_media": faker.random.number(), 36 | "comment_status": "open", 37 | "ping_status": "open", 38 | "sticky": false, 39 | "template": "", 40 | "format": "standard", 41 | "meta": [], 42 | "categories": [faker.random.number()] 43 | }; 44 | 45 | items.push(item); 46 | page.push(item); 47 | } 48 | 49 | pages.push(page); 50 | } 51 | 52 | pages.forEach((page) => { 53 | const mock = fetch.mockResponseOnce(JSON.stringify(page), { 54 | status: 200, 55 | headers: new Headers({ 56 | 'X-WP-TotalPages': pages.length, 57 | 'X-WP-Total': items.length 58 | }), 59 | }); 60 | 61 | mocks.push(mock); 62 | }); 63 | 64 | return requests.requestAllBooksEndpointById(id, endpoint2, params).then((data) => { 65 | expect(data.json).toBeDefined(); 66 | expect(data.json).toEqual(items); 67 | 68 | expect(data.response).toBeDefined(); 69 | 70 | mocks.forEach((mock, i) => { 71 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}/${endpoint2}?context=view&page=${i + 1}&per_page=${per_page}`); 72 | }); 73 | }); 74 | }); 75 | 76 | it('rejects request on error', () => { 77 | const statusText = 'not-found'; 78 | const id = faker.random.number(); 79 | 80 | const mock = fetch.mockResponse('', { 81 | status: 404, 82 | statusText: statusText, 83 | }); 84 | 85 | return requests.requestAllBooksEndpointById(id, endpoint2, params).catch((error) => { 86 | expect(error).toBe(statusText); 87 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}/${endpoint2}?context=view&page=1&per_page=100`); 88 | }); 89 | }); 90 | }); -------------------------------------------------------------------------------- /__tests__/requestById.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('requestBooksById function', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('resolves the correct json on successful request', () => { 9 | const item = { 10 | "id": faker.random.number(), 11 | "date": "2017-04-13T20:02:35", 12 | "date_gmt": "2017-04-13T20:02:35", 13 | "guid": {"rendered": faker.internet.url()}, 14 | "modified": "2017-04-13T20:02:35", 15 | "modified_gmt": "2017-04-13T20:02:35", 16 | "slug": faker.lorem.slug(), 17 | "status": "publish", 18 | "type": "post", 19 | "link": `http://wordpress.test/${faker.lorem.slug()}/`, 20 | "title": { "rendered": faker.lorem.sentence() }, 21 | "content": { "rendered": faker.lorem.paragraphs(4) }, 22 | "excerpt": { "rendered": faker.lorem.paragraph() }, 23 | "author": faker.random.number(), 24 | "featured_media": faker.random.number(), 25 | "comment_status": "open", 26 | "ping_status": "open", 27 | "sticky": false, 28 | "template": "", 29 | "format": "standard", 30 | "meta": [], 31 | "categories": [faker.random.number()] 32 | }; 33 | 34 | const mock = fetch.mockResponse(JSON.stringify(item), { status: 200 }); 35 | 36 | return requests.requestBooksById(item.id, params).then((data) => { 37 | expect(data.json).toBeDefined(); 38 | expect(data.json).toEqual(item); 39 | 40 | expect(data.response).toBeDefined(); 41 | 42 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${item.id}?context=view`); 43 | }); 44 | }); 45 | 46 | it('rejects request on error', () => { 47 | const statusText = 'not-found'; 48 | const id = faker.random.number(); 49 | 50 | const mock = fetch.mockResponse('', { 51 | status: 404, 52 | statusText: statusText, 53 | }); 54 | 55 | return requests.requestBooksById(id, params).catch((error) => { 56 | expect(error).toBe(statusText); 57 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}?context=view`); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /__tests__/requestEndpoint.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('requestBooksEndpoint function', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('resolves the correct json on successful request', () => { 9 | const items = []; 10 | 11 | for (let i = 0, len = faker.random.number({ min: 1, max: 20 }); i < len; i++) { 12 | items.push({ 13 | "id": faker.random.number(), 14 | "title": faker.lorem.sentence() 15 | }); 16 | } 17 | 18 | const mock = fetch.mockResponse(JSON.stringify(items), { 19 | status: 200, 20 | headers: new Headers({ 21 | 'X-WP-TotalPages': 1, 22 | 'X-WP-Total': items.length 23 | }), 24 | }); 25 | 26 | return requests.requestBooksEndpoint(endpoint2, params).then((data) => { 27 | expect(data.json).toBeDefined(); 28 | expect(data.json).toEqual(items); 29 | 30 | expect(data.response).toBeDefined(); 31 | 32 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${endpoint2}?context=view`); 33 | }); 34 | }); 35 | 36 | it('rejects request on error', () => { 37 | const statusText = 'not-found'; 38 | 39 | const mock = fetch.mockResponse('', { 40 | status: 404, 41 | statusText: statusText, 42 | }); 43 | 44 | return requests.requestBooksEndpoint(endpoint2, params).catch((error) => { 45 | expect(error).toBe(statusText); 46 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${endpoint2}?context=view`); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/requestEndpointById.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | describe('requestBooksEndpointById function', () => { 4 | beforeEach(() => { 5 | fetch.resetMocks(); 6 | }); 7 | 8 | it('resolves the correct json on successful request', () => { 9 | const id = faker.random.number(); 10 | const item = { 11 | "id": faker.random.number(), 12 | "title": faker.lorem.sentence() 13 | }; 14 | 15 | const mock = fetch.mockResponse(JSON.stringify(item), { status: 200 }); 16 | 17 | return requests.requestBooksEndpointById(id, endpoint2, params).then((data) => { 18 | expect(data.json).toBeDefined(); 19 | expect(data.json).toEqual(item); 20 | 21 | expect(data.response).toBeDefined(); 22 | 23 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}/${endpoint2}?context=view`); 24 | }); 25 | }); 26 | 27 | it('rejects request on error', () => { 28 | const statusText = 'not-found'; 29 | const id = faker.random.number(); 30 | 31 | const mock = fetch.mockResponse('', { 32 | status: 404, 33 | statusText: statusText, 34 | }); 35 | 36 | return requests.requestBooksEndpointById(id, endpoint2, params).catch((error) => { 37 | expect(error).toBe(statusText); 38 | expect(mock).toHaveBeenCalledWith(`http://wordpress.test/wp-json/wp/v2/${endpoint}/${id}/${endpoint2}?context=view`); 39 | }); 40 | }); 41 | }); -------------------------------------------------------------------------------- /__tests__/requests.js: -------------------------------------------------------------------------------- 1 | import { createRequests } from '../lib/index'; 2 | 3 | test('Check requests created by createRequests function', () => { 4 | const requests = createRequests('http://wordpress.test/wp-json/', ['books', 'authors']); 5 | 6 | expect(typeof requests.requestBooks).toBe('function'); 7 | expect(typeof requests.requestBooksEndpoint).toBe('function'); 8 | expect(typeof requests.requestBooksById).toBe('function'); 9 | expect(typeof requests.requestBooksEndpointById).toBe('function'); 10 | expect(typeof requests.requestAllBooks).toBe('function'); 11 | expect(typeof requests.requestAllBooksEndpoint).toBe('function'); 12 | expect(typeof requests.requestAllBooksEndpointById).toBe('function'); 13 | 14 | expect(typeof requests.requestAuthors).toBe('function'); 15 | expect(typeof requests.requestAuthorsEndpoint).toBe('function'); 16 | expect(typeof requests.requestAuthorsById).toBe('function'); 17 | expect(typeof requests.requestAuthorsEndpointById).toBe('function'); 18 | expect(typeof requests.requestAllAuthors).toBe('function'); 19 | expect(typeof requests.requestAllAuthorsEndpoint).toBe('function'); 20 | expect(typeof requests.requestAllAuthorsEndpointById).toBe('function'); 21 | }); 22 | 23 | test('Check ability to skip some requests', () => { 24 | const requests = createRequests('http://wordpress.test/wp-json/', ['books'], { 25 | request: false, 26 | requestEndpoint: false, 27 | requestById: false, 28 | requestEndpointById: false, 29 | requestAll: false, 30 | requestAllEndpoint: false, 31 | requestAllEndpointById: false, 32 | }); 33 | 34 | expect(typeof requests.requestBooks).toBe('undefined'); 35 | expect(typeof requests.requestBooksEndpoint).toBe('undefined'); 36 | expect(typeof requests.requestBooksById).toBe('undefined'); 37 | expect(typeof requests.requestBooksEndpointById).toBe('undefined'); 38 | expect(typeof requests.requestAllBooks).toBe('undefined'); 39 | expect(typeof requests.requestAllBooksEndpoint).toBe('undefined'); 40 | expect(typeof requests.requestAllBooksEndpointById).toBe('undefined'); 41 | }); 42 | -------------------------------------------------------------------------------- /jestrc.js: -------------------------------------------------------------------------------- 1 | import configureStore from 'redux-mock-store'; 2 | import thunk from 'redux-thunk'; 3 | import { createActions, createRequests } from './lib/index'; 4 | 5 | global.fetch = require('jest-fetch-mock'); 6 | global.mockStore = configureStore([thunk]); 7 | 8 | global.name = 'test-rest-api'; 9 | global.endpoint = 'books'; 10 | global.endpoint2 = 'chapters'; 11 | global.params = { context: 'view' }; 12 | 13 | global.actions = createActions(name, 'http://wordpress.test/wp-json/', [global.endpoint]); 14 | global.requests = createRequests('http://wordpress.test/wp-json/', [global.endpoint]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-wordpress", 3 | "description": "Redux actions and reducer to work with WordPress REST API", 4 | "license": "MIT", 5 | "author": "Eugene Manuilov ", 6 | "homepage": "https://github.com/eugene-manuilov/redux-wordpress#readme", 7 | "bugs": { 8 | "url": "https://github.com/eugene-manuilov/redux-wordpress/issues" 9 | }, 10 | "version": "1.1.0", 11 | "main": "lib/index", 12 | "files": [ 13 | "*.md", 14 | "dist", 15 | "LICENSE", 16 | "lib", 17 | "src" 18 | ], 19 | "keywords": [ 20 | "redux", 21 | "wordpress", 22 | "wordpress-rest-api" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git@github.com:eugene-manuilov/redux-wordpress.git" 27 | }, 28 | "scripts": { 29 | "build": "npm run build:commonjs & npm run build:umd & npm run build:umd:min", 30 | "build:commonjs": "mkdir -p lib && babel ./src -d lib", 31 | "build:umd": "webpack dist/redux-wordpress.js", 32 | "build:umd:min": "NODE_ENV=production webpack dist/redux-wordpress.min.js", 33 | "test": "jest", 34 | "prepublish": "npm run build" 35 | }, 36 | "devDependencies": { 37 | "babel-cli": "^6.24.1", 38 | "babel-core": "^6.24.1", 39 | "babel-eslint": "^7.2.3", 40 | "babel-jest": "^19.0.0", 41 | "babel-loader": "^6.4.1", 42 | "babel-preset-env": "^1.4.0", 43 | "babel-preset-es2015": "^6.24.1", 44 | "eslint": "^3.19.0", 45 | "eslint-config-airbnb": "^14.1.0", 46 | "eslint-loader": "^1.7.1", 47 | "eslint-plugin-import": "^2.2.0", 48 | "eslint-plugin-jsx-a11y": "^4.0.0", 49 | "faker": "^4.1.0", 50 | "jest": "^19.0.2", 51 | "jest-fetch-mock": "^1.1.1", 52 | "redux": "^3.6.0", 53 | "redux-mock-store": "^1.2.3", 54 | "redux-thunk": "^2.2.0", 55 | "webpack": "^2.4.1" 56 | }, 57 | "peerDependencies": { 58 | "redux": "^2.0.0 || ^3.0.0", 59 | "redux-thunk": "^2.0.0" 60 | }, 61 | "jest": { 62 | "automock": false, 63 | "setupFiles": [ 64 | "./jestrc.js" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { qs, upperFirst, trimEnd, fetchSingle, fetchAll } from './helpers'; 2 | 3 | const getSuccessAction = (json, response, type, params) => { 4 | const action = { 5 | type, 6 | ok: true, 7 | results: json, 8 | params, 9 | }; 10 | 11 | if (response) { 12 | const totalPages = parseInt(response.headers.get('X-WP-TotalPages'), 10); 13 | if (!isNaN(totalPages)) { 14 | action.totalPages = totalPages; 15 | } 16 | 17 | const total = parseInt(response.headers.get('X-WP-Total'), 10); 18 | if (!isNaN(total)) { 19 | action.total = total; 20 | } 21 | } 22 | 23 | return action; 24 | }; 25 | 26 | const getSuccessHandler = (dispatch, type, params) => (data) => { 27 | dispatch(getSuccessAction(data.json, data.response, type, params)); 28 | }; 29 | 30 | const getSuccessHandlerById = (dispatch, type, id, params) => (data) => { 31 | const action = getSuccessAction(data.json, data.response, type, params); 32 | action.id = id; 33 | dispatch(action); 34 | }; 35 | 36 | const getErrorHandler = (dispatch, type, params) => (error) => { 37 | dispatch({ 38 | type, 39 | ok: false, 40 | message: error, 41 | params, 42 | }); 43 | }; 44 | 45 | const getErrorHandlerById = (dispatch, type, id, params) => (error) => { 46 | dispatch({ 47 | type, 48 | ok: false, 49 | message: error, 50 | id, 51 | params, 52 | }); 53 | }; 54 | 55 | export default function createActions(name, host, endpoints, args = {}) { 56 | let options = args; 57 | 58 | // fallback for previous version of this function where the last param was 59 | // for namespace argument 60 | if (typeof args === 'string') { 61 | options = { namespace: args }; 62 | } 63 | 64 | const actions = {}; 65 | const normalizedHost = trimEnd(host, '/'); 66 | const namespace = options.namespace || 'wp/v2'; 67 | 68 | endpoints.forEach((endpoint) => { 69 | const endpointName = upperFirst(endpoint); 70 | 71 | if (options.fetch !== false) { 72 | actions[`fetch${endpointName}`] = (params = {}) => (dispatch) => { 73 | const type = `@@wp/${name}/fetched/${endpoint}`; 74 | 75 | dispatch({ 76 | type: `@@wp/${name}/fetching/${endpoint}`, 77 | params, 78 | }); 79 | 80 | return fetchSingle( 81 | `${normalizedHost}/${namespace}/${endpoint}?${qs(params)}`, 82 | getSuccessHandler(dispatch, type, params), 83 | getErrorHandler(dispatch, type, params), 84 | ); 85 | }; 86 | } 87 | 88 | if (options.fetchEndpoint !== false) { 89 | actions[`fetch${endpointName}Endpoint`] = (endpoint2, params = {}) => (dispatch) => { 90 | const type = `@@wp/${name}/fetched/${endpoint}/${endpoint2}`; 91 | 92 | dispatch({ 93 | type: `@@wp/${name}/fetching/${endpoint}/${endpoint2}`, 94 | params, 95 | }); 96 | 97 | return fetchSingle( 98 | `${normalizedHost}/${namespace}/${endpoint}/${endpoint2}?${qs(params)}`, 99 | getSuccessHandler(dispatch, type, params), 100 | getErrorHandler(dispatch, type, params), 101 | ); 102 | }; 103 | } 104 | 105 | if (options.fetchById !== false) { 106 | actions[`fetch${endpointName}ById`] = (id, params = {}) => (dispatch) => { 107 | const type = `@@wp/${name}/fetched-by-id/${endpoint}`; 108 | 109 | dispatch({ 110 | type: `@@wp/${name}/fetching-by-id/${endpoint}`, 111 | id, 112 | params, 113 | }); 114 | 115 | return fetchSingle( 116 | `${normalizedHost}/${namespace}/${endpoint}/${id}?${qs(params)}`, 117 | getSuccessHandlerById(dispatch, type, id, params), 118 | getErrorHandlerById(dispatch, type, id, params), 119 | ); 120 | }; 121 | } 122 | 123 | if (options.fetchEndpointById !== false) { 124 | actions[`fetch${endpointName}EndpointById`] = (id, endpoint2, params = {}) => (dispatch) => { 125 | const type = `@@wp/${name}/fetched-by-id/${endpoint}/${endpoint2}`; 126 | 127 | dispatch({ 128 | type: `@@wp/${name}/fetching-by-id/${endpoint}/${endpoint2}`, 129 | id, 130 | params, 131 | }); 132 | 133 | return fetchSingle( 134 | `${normalizedHost}/${namespace}/${endpoint}/${id}/${endpoint2}?${qs(params)}`, 135 | getSuccessHandlerById(dispatch, type, id, params), 136 | getErrorHandlerById(dispatch, type, id, params), 137 | ); 138 | }; 139 | } 140 | 141 | if (options.fetchAll !== false) { 142 | actions[`fetchAll${endpointName}`] = (params = {}) => (dispatch) => { 143 | const type = `@@wp/${name}/fetched-all/${endpoint}`; 144 | 145 | dispatch({ 146 | type: `@@wp/${name}/fetching-all/${endpoint}`, 147 | params, 148 | }); 149 | 150 | return fetchAll( 151 | `${normalizedHost}/${namespace}/${endpoint}`, 152 | params, 153 | getSuccessHandler(dispatch, type, params), 154 | getErrorHandler(dispatch, type, params), 155 | ); 156 | }; 157 | } 158 | 159 | if (options.fetchAllEndpoint !== false) { 160 | actions[`fetchAll${endpointName}Endpoint`] = (endpoint2, params = {}) => (dispatch) => { 161 | const type = `@@wp/${name}/fetched-all/${endpoint}/${endpoint2}`; 162 | 163 | dispatch({ 164 | type: `@@wp/${name}/fetching-all/${endpoint}/${endpoint2}`, 165 | params, 166 | }); 167 | 168 | return fetchAll( 169 | `${normalizedHost}/${namespace}/${endpoint}/${endpoint2}`, 170 | params, 171 | getSuccessHandler(dispatch, type, params), 172 | getErrorHandler(dispatch, type, params), 173 | ); 174 | }; 175 | } 176 | 177 | if (options.fetchAllEndpointById !== false) { 178 | actions[`fetchAll${endpointName}EndpointById`] = (id, endpoint2, params = {}) => (dispatch) => { 179 | const type = `@@wp/${name}/fetched-all-by-id/${endpoint}/${endpoint2}`; 180 | 181 | dispatch({ 182 | type: `@@wp/${name}/fetching-all-by-id/${endpoint}/${endpoint2}`, 183 | id, 184 | params, 185 | }); 186 | 187 | return fetchAll( 188 | `${normalizedHost}/${namespace}/${endpoint}/${id}/${endpoint2}`, 189 | params, 190 | getSuccessHandlerById(dispatch, type, id, params), 191 | getErrorHandlerById(dispatch, type, id, params), 192 | ); 193 | }; 194 | } 195 | }); 196 | 197 | return actions; 198 | } 199 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export function qs(params, parent = false) { 2 | return Object 3 | .keys(params) 4 | .sort() 5 | .map((key) => { 6 | const encode = encodeURIComponent; 7 | 8 | if (Array.isArray(params[key])) { 9 | return params[key] 10 | .map((value, i) => { 11 | if (!parent) { 12 | return `${encode(key)}[${encode(i)}]=${encode(value)}`; 13 | } 14 | 15 | return `${encode(parent)}[${encode(key)}][${encode(i)}]=${encode(value)}`; 16 | }) 17 | .join('&'); 18 | } else if (typeof params[key] === 'object') { 19 | return qs(params[key], key); 20 | } 21 | 22 | return !parent 23 | ? `${encode(key)}=${encode(params[key])}` 24 | : `${encode(parent)}[${encode(key)}]=${encode(params[key])}`; 25 | }) 26 | .join('&'); 27 | } 28 | 29 | export function upperFirst(name) { 30 | return name 31 | .split(/( |-|_)/) 32 | .filter(item => item !== ' ' && item !== '-' && item !== '_') 33 | .map(item => item.toLowerCase()) 34 | .map(item => item[0].toUpperCase() + item.slice(1)) 35 | .join(''); 36 | } 37 | export function trimEnd(message, char) { 38 | return message[message.length - 1] === char 39 | ? trimEnd(message.slice(0, -1), char) 40 | : message; 41 | } 42 | 43 | export function requestSingle(url) { 44 | const requestPromise = new Promise((resolve, reject) => { 45 | fetch(url) 46 | .then((response) => { 47 | if (response.ok) { 48 | response 49 | .json() 50 | .then(json => resolve({ json, response })) 51 | .catch(error => reject(error)); 52 | } else { 53 | reject(response.statusText); 54 | } 55 | }) 56 | .catch(reject); 57 | }); 58 | 59 | return requestPromise; 60 | } 61 | 62 | export function fetchSingle(url, success, error) { 63 | return requestSingle(url).then(success).catch(error); 64 | } 65 | 66 | export function requestAll(url, params = {}) { 67 | const fetchPage = (pagenum, data, resolve, reject) => { 68 | fetch(`${url}?${qs(Object.assign({ per_page: 100 }, params, { page: pagenum }))}`) 69 | .then((response) => { 70 | if (response.ok) { 71 | response 72 | .json() 73 | .then((items) => { 74 | items.forEach(item => data.push(item)); 75 | 76 | let totalpages = parseInt(response.headers.get('X-WP-TotalPages'), 10); 77 | if (isNaN(totalpages)) { 78 | totalpages = 0; 79 | } 80 | 81 | if (pagenum >= totalpages) { 82 | resolve({ json: data, response }); 83 | } else { 84 | fetchPage(pagenum + 1, data, resolve, reject); 85 | } 86 | }) 87 | .catch(error => reject(error)); 88 | } else { 89 | reject(response.statusText); 90 | } 91 | }) 92 | .catch(error => reject(error)); 93 | }; 94 | 95 | return new Promise((resolve, reject) => fetchPage(params.page || 1, [], resolve, reject)); 96 | } 97 | 98 | export function fetchAll(url, params, onSuccess, onError) { 99 | return requestAll(url, params).then(onSuccess).catch(onError); 100 | } 101 | 102 | export default { 103 | qs, 104 | fetchSingle, 105 | fetchAll, 106 | }; 107 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import reducer from './reducer'; 2 | import actions from './actions'; 3 | import requests from './requests'; 4 | 5 | const ReduxWordPress = { 6 | createReducer: reducer, 7 | createActions: actions, 8 | createRequests: requests, 9 | }; 10 | 11 | export default ReduxWordPress; 12 | export { reducer as createReducer }; 13 | export { actions as createActions }; 14 | export { requests as createRequests }; 15 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | class Reducer { 2 | constructor(name) { 3 | this.name = name; 4 | } 5 | 6 | match() { 7 | return false; 8 | } 9 | 10 | map(state) { 11 | return state; 12 | } 13 | 14 | prepareNewState(action) { 15 | const state = {}; 16 | 17 | if (action.results) { 18 | state.data = action.results; 19 | } else if (action.result) { 20 | state.data = action.result; 21 | } 22 | 23 | if (typeof action.total !== 'undefined') { 24 | state.total = action.total; 25 | } 26 | 27 | if (typeof action.totalPages !== 'undefined') { 28 | state.totalPages = action.totalPages; 29 | } 30 | 31 | return state; 32 | } 33 | } 34 | 35 | class FetchReducer extends Reducer { 36 | match(type) { 37 | return type.match(new RegExp(`^@@wp/${this.name}/fetched/(\\w+)$`)); 38 | } 39 | 40 | map(state, action) { 41 | const obj = {}; 42 | const self = this; 43 | const match = self.match(action.type); 44 | 45 | obj[match[1]] = Object.assign({}, state[match[1]] || {}, self.prepareNewState(action)); 46 | 47 | return Object.assign({}, state, obj); 48 | } 49 | } 50 | 51 | class FetchAllReducer extends FetchReducer { 52 | match(type) { 53 | return type.match(new RegExp(`^@@wp/${this.name}/fetched-all/(\\w+)$`)); 54 | } 55 | } 56 | 57 | class FetchByIdReducer extends Reducer { 58 | match(type) { 59 | return type.match(new RegExp(`^@@wp/${this.name}/fetched-by-id/(\\w+)$`)); 60 | } 61 | 62 | map(state, action) { 63 | const obj = {}; 64 | const match = this.match(action.type); 65 | 66 | obj[`${match[1]}/${action.id}`] = this.prepareNewState(action); 67 | 68 | return Object.assign({}, state, obj); 69 | } 70 | } 71 | 72 | class FetchEndpointReducer extends Reducer { 73 | match(type) { 74 | return type.match(new RegExp(`^@@wp/${this.name}/fetched/(\\w+)/(\\w+)$`)); 75 | } 76 | 77 | map(state, action) { 78 | const obj = {}; 79 | const self = this; 80 | const match = self.match(action.type); 81 | 82 | obj[`${match[1]}/${match[2]}`] = self.prepareNewState(action); 83 | 84 | return Object.assign({}, state, obj); 85 | } 86 | } 87 | 88 | class FetchEndpointByIdReducer extends Reducer { 89 | match(type) { 90 | return type.match(new RegExp(`^@@wp/${this.name}/fetched-by-id/(\\w+)/(\\w+)$`)); 91 | } 92 | 93 | map(state, action) { 94 | const obj = {}; 95 | const self = this; 96 | const match = self.match(action.type); 97 | 98 | obj[`${match[1]}/${action.id}/${match[2]}`] = self.prepareNewState(action); 99 | 100 | return Object.assign({}, state, obj); 101 | } 102 | } 103 | 104 | class FetchAllEndpointReducer extends FetchEndpointReducer { 105 | match(type) { 106 | return type.match(new RegExp(`^@@wp/${this.name}/fetched-all/(\\w+)/(\\w+)$`)); 107 | } 108 | } 109 | 110 | class FetchAllEndpointByIdReducer extends FetchEndpointByIdReducer { 111 | match(type) { 112 | return type.match(new RegExp(`^@@wp/${this.name}/fetched-all-by-id/(\\w+)/(\\w+)$`)); 113 | } 114 | } 115 | 116 | export default function createReducer(name) { 117 | const reducers = [ 118 | new FetchReducer(name), 119 | new FetchAllReducer(name), 120 | new FetchByIdReducer(name), 121 | new FetchEndpointReducer(name), 122 | new FetchEndpointByIdReducer(name), 123 | new FetchAllEndpointReducer(name), 124 | new FetchAllEndpointByIdReducer(name), 125 | ]; 126 | 127 | return (state = {}, action = {}) => { 128 | if (!action.ok) { 129 | return state; 130 | } 131 | 132 | let newState = Object.assign({}, state); 133 | 134 | reducers.forEach((reducer) => { 135 | if (reducer.match(action.type)) { 136 | newState = reducer.map(newState, action); 137 | } 138 | }); 139 | 140 | return newState; 141 | }; 142 | } 143 | -------------------------------------------------------------------------------- /src/requests.js: -------------------------------------------------------------------------------- 1 | import { qs, upperFirst, trimEnd, requestAll, requestSingle } from './helpers'; 2 | 3 | export default function createRequests(host, endpoints, args = {}) { 4 | let options = args; 5 | 6 | // fallback for previous version of this function where the last param was 7 | // for namespace argument 8 | if (typeof args === 'string') { 9 | options = { namespace: args }; 10 | } 11 | 12 | const requests = {}; 13 | const namespace = options.namespace || 'wp/v2'; 14 | 15 | endpoints.forEach((endpoint) => { 16 | const normalizedURL = trimEnd(host, '/'); 17 | const endpointName = upperFirst(endpoint); 18 | 19 | if (options.request !== false) { 20 | requests[`request${endpointName}`] = (params = {}) => requestSingle(`${normalizedURL}/${namespace}/${endpoint}?${qs(params)}`); 21 | } 22 | 23 | if (options.requestEndpoint !== false) { 24 | requests[`request${endpointName}Endpoint`] = (endpoint2, params = {}) => requestSingle(`${normalizedURL}/${namespace}/${endpoint}/${endpoint2}?${qs(params)}`); 25 | } 26 | 27 | if (options.requestById !== false) { 28 | requests[`request${endpointName}ById`] = (id, params = {}) => requestSingle(`${normalizedURL}/${namespace}/${endpoint}/${id}?${qs(params)}`); 29 | } 30 | 31 | if (options.requestEndpointById !== false) { 32 | requests[`request${endpointName}EndpointById`] = (id, endpoint2, params = {}) => requestSingle(`${normalizedURL}/${namespace}/${endpoint}/${id}/${endpoint2}?${qs(params)}`); 33 | } 34 | 35 | if (options.requestAll !== false) { 36 | requests[`requestAll${endpointName}`] = (params = {}) => requestAll(`${normalizedURL}/${namespace}/${endpoint}`, params); 37 | } 38 | 39 | if (options.requestAllEndpoint !== false) { 40 | requests[`requestAll${endpointName}Endpoint`] = (endpoint2, params = {}) => requestAll(`${normalizedURL}/${namespace}/${endpoint}/${endpoint2}`, params); 41 | } 42 | 43 | if (options.requestAllEndpointById !== false) { 44 | requests[`requestAll${endpointName}EndpointById`] = (id, endpoint2, params = {}) => requestAll(`${normalizedURL}/${namespace}/${endpoint}/${id}/${endpoint2}`, params); 45 | } 46 | }); 47 | 48 | return requests; 49 | } 50 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const webpack = require('webpack'); 4 | 5 | const config = {}; 6 | 7 | // entry and context 8 | config.context = path.resolve(__dirname, 'src'); 9 | config.entry = './index.js'; 10 | 11 | // output 12 | config.output = { 13 | library: "ReduxWordPress", 14 | libraryTarget: 'umd' 15 | }; 16 | 17 | // externals 18 | config.externals = { 19 | fetch: 'fetch' 20 | }; 21 | 22 | // define module and plugins 23 | config.module = {rules: []}; 24 | config.plugins = []; 25 | 26 | // eslint configuration 27 | config.module.rules.push({ 28 | test: /\.js$/, 29 | enforce: 'pre', 30 | exclude: /node_modules/, 31 | use: [ 32 | { 33 | loader: 'eslint-loader', 34 | options: { 35 | failOnWarning: false, 36 | failOnError: true 37 | } 38 | } 39 | ] 40 | }); 41 | 42 | // babel loader rule 43 | config.module.rules.push({ 44 | test: /\.js$/, 45 | exclude: /node_modules/, 46 | use: { 47 | loader: 'babel-loader', 48 | options: { 49 | cacheDirectory: true, 50 | presets: [ 51 | ['env', {targets: {browsers: ["last 2 versions", "safari >= 7"]}}] 52 | ] 53 | } 54 | } 55 | }); 56 | 57 | // no emit plugin 58 | config.plugins.push(new webpack.NoEmitOnErrorsPlugin()); 59 | 60 | // uglify plugin 61 | if ('production' === process.env.NODE_ENV) { 62 | config.plugins.push(new webpack.optimize.UglifyJsPlugin({sourceMap: true})); 63 | } 64 | 65 | module.exports = config; --------------------------------------------------------------------------------