├── .babelrc ├── .gitignore ├── README.md ├── dist └── index.js ├── index.js ├── package-lock.json ├── package.json ├── src ├── detailEntity │ ├── detailedEntity.js │ ├── detailedEntityActions.js │ └── detailedEntityReducer.js ├── helpers.js ├── index.js └── queriedEntity │ ├── queriedEntity.js │ ├── queriedEntityActions.js │ └── queriedEntityReducer.js ├── test ├── sampleTestApp │ ├── App.js │ ├── mock-adapter.js │ ├── reducer.js │ └── store.js └── test.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 8 | ["@babel/plugin-proposal-class-properties", { "loose" : true }] 9 | ] 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # production 5 | /build 6 | 7 | # misc 8 | .DS_Store 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | #IDE 19 | /.idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rest React Redux 2 | 3 | [![N|Solid](https://www.twotalltotems.com/img/nav/nav-logo.svg)](https://www.twotalltotems.com) 4 | 5 | A higher order component for RestAPI abstraction. Reduces boilerplate and encourages clean code. 6 | 7 | By decorating your component with queriedEntity/detailedEntity and initializing the first network call, you can 8 | have access to a managed data entity named according to the "entity name" you passed as argument. 9 | 10 | ## Installation 11 | 12 | npm: 13 | ```sh 14 | npm install rest-react-redux --save 15 | ``` 16 | 17 | 18 | What you need to do: 19 | - Create the proper reducer structure 20 | - Annotate your component/container with the proper HOC 21 | - Enjoy the props introduced to your component and not ever worry about writing actions and reducers 22 | 23 | This library is supposed to be used only on a restful endpoint. here is an example: 24 | - *GET*: `http://www.url.com/contacts?page=1&page-size=10` returns a list of contacts and metadata about the query 25 | - *POST*: `http://www.url.com/contacts` creates a contact 26 | - *GET*, *PUT*, *PATCH* and *DELETE*: `http://www.url.com/contacts/contactId` gets, updates, patches or deletes the specific contact respectively 27 | 28 | ## Restrictions in the current version 29 | - JSON endpoints: network request and response body must be of `application/json` content type 30 | - Redux-thunk: your application redux store must contain redux-thunk as middleware 31 | ```js 32 | import {createStore, compose, applyMiddleware} from 'redux'; 33 | import thunk from 'redux-thunk'; 34 | export const store = createStore(rootReducer, compose(applyMiddleware(thunk))); 35 | ``` 36 | - Axios: your application must use axios as the network call library. BaseUrl and headers must be set in your application scope 37 | ```js 38 | import axios from 'axios'; 39 | axios.defaults.baseURL = 'http://www.url.com'; 40 | ``` 41 | - reducer: using `queriedEntityReducer` or `detailedEntityReducer` you must create a field in the root reducer matching the name of the entity you create. This is where the library manages the data 42 | ```js 43 | import { queriedEntityReducer, detailedEntityReducer } from 'rest-react-redux'; 44 | const reducers = { 45 | contacts: queriedEntityReducer('contact'), 46 | contact: detailedEntityReducer('contact'), 47 | } 48 | ``` 49 | 50 | ## Higher Order Components 51 | ### queriedEntity 52 | 53 | if you intend to work with an endpoint that returns a list use `queriedEntity` to decorate your components: 54 | ```js 55 | import { queriedEntity } from 'rest-react-redux'; 56 | 57 | @queriedEntity('contact') 58 | class ContactsPage extends React.Component { 59 | ... 60 | } 61 | ``` 62 | NOTE: For those of you who do not enjoy decorating as much as I do, use the standard way! 63 | 64 | ### queriedEntity(entityName, config[optional]) 65 | 66 | | Config field | Explanation | Default Value | 67 | | ------ | ------ | ------ | 68 | | resultField | the result field name in response body that matches the list of items | `content` | 69 | | retain_number | maximum number of queries to cache | `10` | 70 | | hideLoadIfDataFound | if set to `false`, will trigger loading UI even if data had been cached | `true` | 71 | | reducerName | The reducer name used for the entity. Default is the plural form of the entityName | `[entityName]s` | 72 | | preloadValidTime | The time (milliseconds) that a preloaded query is valid and should not be re-fetched | `10000` | 73 | | smartPreload | If set to `true` the library times the network calls specific to the defined entity. If the average is greater than 0.3 seconds, preloading will be cancelled. Overwrite this time with the next field | `false` | 74 | | smartThresholdTime | The acceptable average time (milliseconds) for network calls to continue preloading in `smartPreload` mode | `300` | 75 | 76 | #### Properties injected to the wrapped component 77 | | property (props) | Explanation | Example | Sample value | 78 | | ------ | ------ | ------ | ------ | 79 | | [entityName]s | The query result | `contacts` | `[{id: 1, name: 'John Doe'}]` | 80 | | [entityName]sQueryParams | The last successful parameters with which query was performed | `contactsQueryParams` | `{page: 1}` | 81 | | [entityName]sMetadata | The metadata that is received from the endpoint | `contactsMetadata` | `{totalPages: 10}` | 82 | | initialQuery[EntityName]s | A *must-be-called* function that initializes the query. Receives url and parameters (object) | `initialQueryContacts('/contacts/', {page: 1})` | 83 | | query[EntityName]s | Any query after initial query call. It will append the new partial params on top of the previous ones | `queryContacts({pageSize: 1})` | 84 | | create[EntityName] | Creates an object and performs the latest query again | `createContact({name: 'Foo Bar'})` | 85 | | update[EntityName] | Updates/replaces an entity. After success, will update the store and queries again | `updateContact({id: 1, name: 'Foo Bar'})` | 86 | | patch[EntityName] | Patches an entity. After success, will update the store and queries again | `patchContact({id: 1, name: 'Foo Bar'})` | 87 | | delete[EntityName] | Removes an entity. After success, will update the store and queries again | `deleteContact({id: 1, name: 'Foo Bar'})` | 88 | | set[EntityName]sPreloader | Sets a function for pre-loading data for a smooth UX | `setContactsPreloader(customPreloader)`* | 89 | | loading[EntityName]s | Network loading status | `loadingContacts` | `true` | 90 | 91 | *Note: the preloader function will receive (partialParams, params, queryMetadata) as arguments. The function should return an array of partialParams. See the preloading 92 | section for more information 93 | 94 | ### detailedEntity 95 | 96 | if you intend to work with an endpoint that returns a detailed entity use `detailedEntity` to decorate your components: 97 | ```js 98 | import { detailedEntity } from 'rest-react-redux'; 99 | 100 | @detailedEntity('contact') 101 | class ContactsPage extends React.Component { 102 | ... 103 | } 104 | ``` 105 | ### detailedEntity(entityName, config[optional]) 106 | 107 | | Config field | Explanation | Default Value | 108 | | ------ | ------ | ------ | 109 | | retain_number | maximum number of queries to cache | `10` | 110 | | hideLoadIfDataFound | if set to `false`, will trigger loading UI even if data had been cached | `true` | 111 | | reducerName | The reducer name used for the entity. Default is the entityName | `[entityName]` | 112 | 113 | #### Properties injected to the wrapped component 114 | | property (props) | Explanation | Example | Sample value | 115 | | ------ | ------ | ------ | ------ | 116 | | [entityName] | The result | `contact` | `{id: 1, name: 'John Doe'}` | 117 | | initialGet[EntityName] | A *must-be-called* function that initializes the entity. Receives url and object id | `initialGetContact('/contacts/12', 12)` | 118 | | get[EntityName] | Any get call after the initial get call. This is usually used for receiving updates if any | `getContact()` | 119 | | update[EntityName] | Updates/replaces the entity. After success, will update the store and gets again | `updateContact({id: 1, name: 'Foo Bar'})` | 120 | | patch[EntityName] | Patches the entity. After success, will update the store and gets again | `patchContact({id: 1, name: 'Foo Bar'})` | 121 | | delete[EntityName] | Removes the entity. After success, will update the store | `deleteContact()` | 122 | | loading[EntityName] | Network loading status | `loadingContact` | `true` | 123 | 124 | ### Preloading 125 | For a better user experience, you may pre-load the data that have a good chance of being loaded later. There are 126 | two main methods of preloading data: 127 | 128 | #### Entity specific preloading 129 | If you are dealing with a queried entity like calendar events, table data, chat messages etc. 130 | it might be a good idea to dynamically preload data based on what the user queries. For instance, loading the previous and 131 | the next pages of a table sounds like a good investment! To do that you simply need to set 132 | a function via property `set[EntityName]sPreloader`: 133 | 134 | ```js 135 | import { queriedEntity } from 'rest-react-redux'; 136 | 137 | @queriedEntity('contact') 138 | class ContactsPage extends React.Component { 139 | 140 | componentDidMount() { 141 | this.props.initialQueryContacts('/contacts/', {page: 1, size: 20}); 142 | this.props.setContactsPreloader((partialParams) => { 143 | 144 | const page = partialParams.page; 145 | 146 | // If the query does not contain a new page do not preload 147 | if (!partialParams.page) return []; 148 | 149 | // Preload previous and next pages 150 | return [{page: page - 1}, {page: page + 1}]; 151 | }) 152 | } 153 | } 154 | ``` 155 | 156 | #### Generic preloading 157 | You may need to preload entities that are not related to the component you are focusing at. 158 | For instance, if a user gets to the main dashboard, you want to preload all the contacts before 159 | the user goes to the contacts page. To achieve that you can use the exposed `queryEntities` or `getEntity` 160 | action generators: 161 | 162 | `queryEntities(entityName, url, params)` 163 | `getEntity(entityName, url)` 164 | 165 | Usage example: 166 | 167 | ```js 168 | import { queryEntities, getEntity } from 'rest-react-redux'; 169 | import store from '../store'; 170 | 171 | class Dashboard extends React.Component { 172 | 173 | componentDidMount() { 174 | // Preload a contact list query 175 | store.dispatch(queryEntities('contact', '/contacts/', {page: 1})); 176 | 177 | // Preload a detailed contact 178 | store.dispatch(getEntity('contact', '/contacts/1')); 179 | } 180 | } 181 | ``` 182 | 183 | 184 | ### Todos 185 | 186 | - Remove the redux-thunk dependency 187 | - Remove the JSON request/response requirement 188 | - Remove the need to update the reducer for each entity 189 | - Tests 190 | 191 | License 192 | ---- 193 | 194 | MIT -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("axios"), require("react"), require("react-redux")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["axios", "react", "react-redux"], factory); 6 | else if(typeof exports === 'object') 7 | exports["rest-react-redux"] = factory(require("axios"), require("react"), require("react-redux")); 8 | else 9 | root["rest-react-redux"] = factory(root["axios"], root["react"], root["react-redux"]); 10 | })(window, function(__WEBPACK_EXTERNAL_MODULE_axios__, __WEBPACK_EXTERNAL_MODULE_react__, __WEBPACK_EXTERNAL_MODULE_react_redux__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // define getter function for harmony exports 47 | /******/ __webpack_require__.d = function(exports, name, getter) { 48 | /******/ if(!__webpack_require__.o(exports, name)) { 49 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 50 | /******/ } 51 | /******/ }; 52 | /******/ 53 | /******/ // define __esModule on exports 54 | /******/ __webpack_require__.r = function(exports) { 55 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 56 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 57 | /******/ } 58 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 59 | /******/ }; 60 | /******/ 61 | /******/ // create a fake namespace object 62 | /******/ // mode & 1: value is a module id, require it 63 | /******/ // mode & 2: merge all properties of value into the ns 64 | /******/ // mode & 4: return value when already ns object 65 | /******/ // mode & 8|1: behave like require 66 | /******/ __webpack_require__.t = function(value, mode) { 67 | /******/ if(mode & 1) value = __webpack_require__(value); 68 | /******/ if(mode & 8) return value; 69 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 70 | /******/ var ns = Object.create(null); 71 | /******/ __webpack_require__.r(ns); 72 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 73 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 74 | /******/ return ns; 75 | /******/ }; 76 | /******/ 77 | /******/ // getDefaultExport function for compatibility with non-harmony modules 78 | /******/ __webpack_require__.n = function(module) { 79 | /******/ var getter = module && module.__esModule ? 80 | /******/ function getDefault() { return module['default']; } : 81 | /******/ function getModuleExports() { return module; }; 82 | /******/ __webpack_require__.d(getter, 'a', getter); 83 | /******/ return getter; 84 | /******/ }; 85 | /******/ 86 | /******/ // Object.prototype.hasOwnProperty.call 87 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 88 | /******/ 89 | /******/ // __webpack_public_path__ 90 | /******/ __webpack_require__.p = ""; 91 | /******/ 92 | /******/ 93 | /******/ // Load entry module and return exports 94 | /******/ return __webpack_require__(__webpack_require__.s = "./src/index.js"); 95 | /******/ }) 96 | /************************************************************************/ 97 | /******/ ({ 98 | 99 | /***/ "./src/detailEntity/detailedEntity.js": 100 | /*!********************************************!*\ 101 | !*** ./src/detailEntity/detailedEntity.js ***! 102 | \********************************************/ 103 | /*! no static exports found */ 104 | /***/ (function(module, exports, __webpack_require__) { 105 | 106 | "use strict"; 107 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"react\"));\n\nvar _reactRedux = __webpack_require__(/*! react-redux */ \"react-redux\");\n\nvar _detailedEntityActions = __webpack_require__(/*! ./detailedEntityActions */ \"./src/detailEntity/detailedEntityActions.js\");\n\nvar _helpers = __webpack_require__(/*! ../helpers */ \"./src/helpers.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _typeof(obj) { if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\n/**\n * Detailed entity abstraction (Higher Order Component)\n * Retrieves entityName, end point url. Dispatches queries and keeps track of the queries.\n * API contract: should accept page and pageSize as params, should return a json with the property item as list\n * This component will recycles cached queries after. It retains at most RETAIN_NUMBER of queries\n * Per entityName used, there must be a reducer with the same name located at reducers/index.js\n *\n * staticURL is a url provided at the annotation level and requires no logic to generate it i.e. no\n * parameter exists inside it. If your url contains custom parameters (usually ids that component knows about it)\n * use dynamicURL on the initialQuery. This will override the staticURL if any provided.\n */\n// These props should be filtered before inject\nvar filteredProps = {};\n['getEntity', 'pushToQueue', 'createItem', 'updateItem', 'patchItem', 'deleteItem'].forEach(function (prop) {\n return filteredProps[prop] = undefined;\n}); // Number of queries to cache in store\n\nvar RETAIN_NUMBER = 10;\n\nvar _default = function _default(entityName) {\n var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},\n reducerName = _ref.reducerName,\n _ref$retain_number = _ref.retain_number,\n retain_number = _ref$retain_number === void 0 ? RETAIN_NUMBER : _ref$retain_number;\n\n return function (WrappedComponent) {\n var _class, _temp;\n\n return (0, _reactRedux.connect)(function (state) {\n return _defineProperty({}, entityName, state[reducerName || entityName]);\n }, {\n getEntity: _detailedEntityActions.getEntity,\n pushToQueue: _detailedEntityActions.pushToQueue,\n createItem: _detailedEntityActions.createItem,\n updateItem: _detailedEntityActions.updateItem,\n patchItem: _detailedEntityActions.patchItem,\n deleteItem: _detailedEntityActions.deleteItem\n })((_temp = _class =\n /*#__PURE__*/\n function (_React$Component) {\n _inherits(_class, _React$Component);\n\n function _class() {\n var _getPrototypeOf2;\n\n var _this;\n\n _classCallCheck(this, _class);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(_class)).call.apply(_getPrototypeOf2, [this].concat(args)));\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"state\", {\n entityId: null,\n loadingData: false\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"initialGet\", function (url, entityId) {\n _this.setState({\n loadingData: true,\n url: url,\n entityId: entityId\n });\n\n return _this.get(url, entityId);\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"get\", function () {\n var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _this.state.url;\n var entityId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _this.state.entityId;\n var entity = _this.props[entityName][entityId];\n if (!entity) _this.props.freeze();\n\n _this.setState({\n loadingData: true\n });\n\n return _this.props.getEntity(entityName, url).then(function () {\n _this.setState({\n loadingData: false\n });\n\n _this.props.unfreeze();\n\n _this.collectGarbage();\n }).catch(function () {\n _this.setState({\n loadingData: false\n });\n\n _this.props.unfreeze();\n });\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"checkSetup\", function () {\n var _this$state = _this.state,\n url = _this$state.url,\n entityId = _this$state.entityId;\n if (!url) throw new Error(\"No url specified for \".concat(entityName));\n if (!entityId) throw new Error(\"No entityId specified for \".concat(entityName));\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"collectGarbage\", function () {\n return _this.props.pushToQueue(entityName, _this.state.entityId, retain_number);\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"update\", function (entity) {\n _this.checkSetup();\n\n _this.props.freeze();\n\n return _this.props.updateItem(entityName, entity, _this.state.entityId, _this.state.url).then(function () {\n _this.props.unfreeze();\n\n _this.get();\n });\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"patch\", function (fields) {\n _this.checkSetup();\n\n _this.props.freeze();\n\n return _this.props.patchItem(entityName, fields, _this.state.entityId, _this.state.url).then(function () {\n _this.props.unfreeze();\n\n _this.get();\n });\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"delete\", function () {\n _this.checkSetup();\n\n _this.props.freeze();\n\n return _this.props.deleteItem(entityName, _this.state.entityId, _this.state.url).then(function () {\n _this.props.unfreeze();\n\n _this.get();\n });\n });\n\n return _this;\n }\n\n _createClass(_class, [{\n key: \"render\",\n\n /** @ignore */\n value: function render() {\n var _injectedProps;\n\n var entityId = this.state.entityId;\n var entity = this.props[entityName][entityId];\n\n var item = _defineProperty({}, entityName, entity || {});\n\n var injectedProps = (_injectedProps = {}, _defineProperty(_injectedProps, entityName, entity || {}), _defineProperty(_injectedProps, 'initialGet' + (0, _helpers.CFL)(entityName), this.initialGet), _defineProperty(_injectedProps, 'get' + (0, _helpers.CFL)(entityName), this.get), _defineProperty(_injectedProps, 'update' + (0, _helpers.CFL)(entityName), this.update), _defineProperty(_injectedProps, 'patch' + (0, _helpers.CFL)(entityName), this.patch), _defineProperty(_injectedProps, 'delete' + (0, _helpers.CFL)(entityName), this.delete), _defineProperty(_injectedProps, 'loading' + (0, _helpers.CFL)(entityName), this.state.loadingData), _injectedProps);\n return _react.default.createElement(WrappedComponent, _extends({}, this.props, filteredProps, item, injectedProps));\n }\n }]);\n\n return _class;\n }(_react.default.Component), _defineProperty(_class, \"defaultProps\", {\n freeze: function freeze() {},\n unfreeze: function unfreeze() {}\n }), _temp));\n };\n};\n\nexports.default = _default;\n\n//# sourceURL=webpack://rest-react-redux/./src/detailEntity/detailedEntity.js?"); 108 | 109 | /***/ }), 110 | 111 | /***/ "./src/detailEntity/detailedEntityActions.js": 112 | /*!***************************************************!*\ 113 | !*** ./src/detailEntity/detailedEntityActions.js ***! 114 | \***************************************************/ 115 | /*! no static exports found */ 116 | /***/ (function(module, exports, __webpack_require__) { 117 | 118 | "use strict"; 119 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.deleteItem = exports.patchItem = exports.updateItem = exports.createItem = exports.pushToQueue = exports.getEntity = exports.patchItemDispatch = void 0;\n\nvar _axios = _interopRequireDefault(__webpack_require__(/*! axios */ \"axios\"));\n\nvar types = _interopRequireWildcard(__webpack_require__(/*! ../helpers */ \"./src/helpers.js\"));\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n/**\n * Actions\n */\nvar updateItemDispatch = function updateItemDispatch(payload, entityName) {\n return {\n type: types.UPDATE_ITEM(entityName),\n payload: payload\n };\n};\n\nvar patchItemDispatch = function patchItemDispatch(payload, entityName) {\n return {\n type: types.PATCH_ITEM(entityName),\n payload: payload\n };\n};\n\nexports.patchItemDispatch = patchItemDispatch;\n\nvar deleteItemDispatch = function deleteItemDispatch(payload, entityName) {\n var type = types.REMOVE_ITEM(entityName);\n return {\n type: type,\n payload: payload\n };\n};\n\nvar insertItem = function insertItem(payload, entityName) {\n return {\n type: types.INSERT_ITEM(entityName),\n payload: payload\n };\n};\n\nvar getEntity = function getEntity(entityName, url) {\n return function (dispatch) {\n return _axios.default.get(url).then(function (_ref) {\n var data = _ref.data;\n dispatch(insertItem(data, entityName));\n });\n };\n};\n\nexports.getEntity = getEntity;\n\nvar pushToQueue = function pushToQueue(entityName, id, retain_number) {\n return {\n type: types.PUSH_TO_TRACKING_QUEUE_DETAILED(entityName),\n payload: {\n id: id,\n retain_number: retain_number\n }\n };\n};\n\nexports.pushToQueue = pushToQueue;\n\nvar createItem = function createItem(entityName, entity, url) {\n // The returned data will not be of any special use since the position it is placed in db is unknown\n // therefore no dispatch is made to store\n return function () {\n return _axios.default.post(url, entity);\n };\n};\n\nexports.createItem = createItem;\n\nvar updateItem = function updateItem(entityName, entity, entityId, url) {\n return function (dispatch) {\n return _axios.default.put(url, entity).then(function () {\n dispatch(updateItemDispatch({\n entityId: entityId,\n entity: entity\n }, entityName));\n });\n };\n};\n\nexports.updateItem = updateItem;\n\nvar patchItem = function patchItem(entityName, entity, entityId, url) {\n return function (dispatch) {\n return _axios.default.patch(url, entity).then(function () {\n dispatch(patchItemDispatch({\n entity: entity,\n entityId: entityId\n }, entityName));\n });\n };\n};\n\nexports.patchItem = patchItem;\n\nvar deleteItem = function deleteItem(entityName, entityId, url) {\n return function (dispatch) {\n return _axios.default.delete(url).then(function () {\n dispatch(deleteItemDispatch(entityId, entityName));\n });\n };\n};\n\nexports.deleteItem = deleteItem;\n\n//# sourceURL=webpack://rest-react-redux/./src/detailEntity/detailedEntityActions.js?"); 120 | 121 | /***/ }), 122 | 123 | /***/ "./src/detailEntity/detailedEntityReducer.js": 124 | /*!***************************************************!*\ 125 | !*** ./src/detailEntity/detailedEntityReducer.js ***! 126 | \***************************************************/ 127 | /*! no static exports found */ 128 | /***/ (function(module, exports, __webpack_require__) { 129 | 130 | "use strict"; 131 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar types = _interopRequireWildcard(__webpack_require__(/*! ../helpers */ \"./src/helpers.js\"));\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }\n\nfunction _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }\n\nfunction _nonIterableSpread() { throw new TypeError(\"Invalid attempt to spread non-iterable instance\"); }\n\nfunction _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === \"[object Arguments]\") return Array.from(iter); }\n\nfunction _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nvar defaultState = {\n tracker: []\n};\n/**\n * Reducer generator for storing and to caching detailed entities\n */\n\nvar _default = function _default(entityName) {\n return function itemDetailRepo() {\n var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultState;\n var action = arguments.length > 1 ? arguments[1] : undefined;\n\n switch (action.type) {\n case types.INSERT_ITEM(entityName):\n {\n var entity = action.payload;\n return _objectSpread({}, state, _defineProperty({}, entity.id, entity));\n }\n\n case types.UPDATE_ITEM(entityName):\n {\n var _action$payload = action.payload,\n _entity = _action$payload.entity,\n entityId = _action$payload.entityId;\n return _objectSpread({}, state, _defineProperty({}, entityId, _objectSpread({\n id: entityId\n }, _entity)));\n }\n\n case types.PATCH_ITEM(entityName):\n {\n var _action$payload2 = action.payload,\n _entity2 = _action$payload2.entity,\n _entityId = _action$payload2.entityId;\n return _objectSpread({}, state, _defineProperty({}, _entityId, _objectSpread({}, state[_entityId], _entity2)));\n }\n\n case types.REMOVE_ITEM(entityName):\n {\n var _entityId2 = action.payload;\n\n var tracker = _toConsumableArray(state.tracker);\n\n var index = tracker.indexOf(_entityId2);\n\n if (index > -1) {\n delete state[tracker[index]];\n tracker.splice(index, 1);\n }\n\n return _objectSpread({}, state, {\n tracker: tracker\n });\n }\n\n case types.PUSH_TO_TRACKING_QUEUE_DETAILED(entityName):\n {\n var _tracker = _toConsumableArray(state.tracker);\n\n var _action$payload3 = action.payload,\n id = _action$payload3.id,\n retain_number = _action$payload3.retain_number;\n\n var _index = _tracker.indexOf(id);\n\n if (_index > -1) _tracker.splice(_index, 1);else if (_tracker.length >= retain_number) {\n delete state[_tracker[0]];\n\n _tracker.shift();\n }\n\n _tracker.push(id);\n\n return _objectSpread({}, state, {\n tracker: _tracker\n });\n }\n\n default:\n return state;\n }\n };\n};\n\nexports.default = _default;\n\n//# sourceURL=webpack://rest-react-redux/./src/detailEntity/detailedEntityReducer.js?"); 132 | 133 | /***/ }), 134 | 135 | /***/ "./src/helpers.js": 136 | /*!************************!*\ 137 | !*** ./src/helpers.js ***! 138 | \************************/ 139 | /*! no static exports found */ 140 | /***/ (function(module, exports, __webpack_require__) { 141 | 142 | "use strict"; 143 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.detailedUrl = exports.PL = exports.CFL = exports.LOADING = exports.encodeAPICall = exports.DELETE_ENTITY = exports.PATCH_ENTITY = exports.UPDATE_ENTITY = exports.UPDATE_NETWORK_TIMER = exports.PUSH_TO_TRACKING_QUEUE = exports.INSERT_QUERY = exports.PUSH_TO_TRACKING_QUEUE_DETAILED = exports.PATCH_ITEM = exports.REMOVE_ITEM = exports.UPDATE_ITEM = exports.INSERT_ITEM = void 0;\n\n/**\n * Detailed entity action type generators\n */\nvar INSERT_ITEM = function INSERT_ITEM(entityName) {\n return 'INSERT_' + entityName.toUpperCase();\n};\n\nexports.INSERT_ITEM = INSERT_ITEM;\n\nvar UPDATE_ITEM = function UPDATE_ITEM(entityName) {\n return 'UPDATE_' + entityName.toUpperCase();\n};\n\nexports.UPDATE_ITEM = UPDATE_ITEM;\n\nvar REMOVE_ITEM = function REMOVE_ITEM(entityName) {\n return 'REMOVE_' + entityName.toUpperCase();\n};\n\nexports.REMOVE_ITEM = REMOVE_ITEM;\n\nvar PATCH_ITEM = function PATCH_ITEM(entityName) {\n return 'PATCH_' + entityName.toUpperCase();\n};\n\nexports.PATCH_ITEM = PATCH_ITEM;\n\nvar PUSH_TO_TRACKING_QUEUE_DETAILED = function PUSH_TO_TRACKING_QUEUE_DETAILED(entityName) {\n return 'PUSH_TO_TRACKING_QUEUE_DETAILED_' + entityName.toUpperCase();\n};\n/**\n * Queried entity action type generators\n */\n\n\nexports.PUSH_TO_TRACKING_QUEUE_DETAILED = PUSH_TO_TRACKING_QUEUE_DETAILED;\n\nvar INSERT_QUERY = function INSERT_QUERY(entityName) {\n return 'INSERT_QUERY_' + entityName.toUpperCase();\n};\n\nexports.INSERT_QUERY = INSERT_QUERY;\n\nvar PUSH_TO_TRACKING_QUEUE = function PUSH_TO_TRACKING_QUEUE(entityName) {\n return 'PUSH_TO_TRACKING_QUEUE_QUERY_' + entityName.toUpperCase();\n};\n\nexports.PUSH_TO_TRACKING_QUEUE = PUSH_TO_TRACKING_QUEUE;\n\nvar UPDATE_NETWORK_TIMER = function UPDATE_NETWORK_TIMER(entityName) {\n return 'UPDATE_NETWORK_TIMER_' + entityName.toUpperCase();\n};\n\nexports.UPDATE_NETWORK_TIMER = UPDATE_NETWORK_TIMER;\n\nvar UPDATE_ENTITY = function UPDATE_ENTITY(entityName) {\n return 'UPDATE_ENTITY_' + entityName.toUpperCase();\n};\n\nexports.UPDATE_ENTITY = UPDATE_ENTITY;\n\nvar PATCH_ENTITY = function PATCH_ENTITY(entityName) {\n return 'PATCH_ENTITY_' + entityName.toUpperCase();\n};\n\nexports.PATCH_ENTITY = PATCH_ENTITY;\n\nvar DELETE_ENTITY = function DELETE_ENTITY(entityName) {\n return 'DELETE_ENTITY_' + entityName.toUpperCase();\n};\n/**\n * Generates a unique key from url and query params\n * @returns {number}\n */\n\n\nexports.DELETE_ENTITY = DELETE_ENTITY;\n\nvar encodeAPICall = function encodeAPICall(url, params) {\n var sortedParams = {};\n Object.keys(params).sort().forEach(function (key) {\n return sortedParams[key] = params[key];\n });\n var string = JSON.stringify(sortedParams);\n return hashCode(url + string);\n};\n/**\n * A placeholder constant for signalling the loading status in redux to prevent re-call\n */\n\n\nexports.encodeAPICall = encodeAPICall;\nvar LOADING = 'LOADING';\n/**\n * Generates a numerical hash from a string\n * @returns {number}\n */\n\nexports.LOADING = LOADING;\n\nvar hashCode = function hashCode(string) {\n var hash = 0;\n if (string.length === 0) return hash;\n\n for (var i = 0; i < string.length; i++) {\n var char = string.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash; // Convert to 32bit integer\n }\n\n return hash;\n};\n/**\n * Capitalizes first letter of a word\n * @return {string}\n */\n\n\nvar CFL = function CFL(word) {\n if (!word) return word;\n return word.slice(0, 1).toUpperCase() + word.slice(1, word.length);\n};\n/**\n * Pluralizes a word\n * @return {string}\n */\n\n\nexports.CFL = CFL;\n\nvar PL = function PL(word) {\n if (!word || word.length === 0) return word;\n if (/(x|s|ch|sh)$/i.test(word)) return word + 'es';\n if (/y$/i.test(word)) return word.slice(0, word.length - 1) + 'ies';\n return word + 's';\n};\n/**\n * Constructs a detailed entity url by appending id after the base url\n * @returns {string}\n */\n\n\nexports.PL = PL;\n\nvar detailedUrl = function detailedUrl(baseUrl, id) {\n var hasTrailingSlash = baseUrl.endsWith('/');\n return baseUrl + (!hasTrailingSlash ? '/' : '') + id + (hasTrailingSlash ? '/' : '');\n};\n\nexports.detailedUrl = detailedUrl;\n\n//# sourceURL=webpack://rest-react-redux/./src/helpers.js?"); 144 | 145 | /***/ }), 146 | 147 | /***/ "./src/index.js": 148 | /*!**********************!*\ 149 | !*** ./src/index.js ***! 150 | \**********************/ 151 | /*! no static exports found */ 152 | /***/ (function(module, exports, __webpack_require__) { 153 | 154 | "use strict"; 155 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"queriedEntityReducer\", {\n enumerable: true,\n get: function get() {\n return _queriedEntityReducer.default;\n }\n});\nObject.defineProperty(exports, \"queriedEntity\", {\n enumerable: true,\n get: function get() {\n return _queriedEntity.default;\n }\n});\nObject.defineProperty(exports, \"detailedEntityReducer\", {\n enumerable: true,\n get: function get() {\n return _detailedEntityReducer.default;\n }\n});\nObject.defineProperty(exports, \"detailedEntity\", {\n enumerable: true,\n get: function get() {\n return _detailedEntity.default;\n }\n});\nObject.defineProperty(exports, \"queryEntities\", {\n enumerable: true,\n get: function get() {\n return _queriedEntityActions.queryEntities;\n }\n});\nObject.defineProperty(exports, \"getEntity\", {\n enumerable: true,\n get: function get() {\n return _detailedEntityActions.getEntity;\n }\n});\n\nvar _queriedEntityReducer = _interopRequireDefault(__webpack_require__(/*! ./queriedEntity/queriedEntityReducer */ \"./src/queriedEntity/queriedEntityReducer.js\"));\n\nvar _queriedEntity = _interopRequireDefault(__webpack_require__(/*! ./queriedEntity/queriedEntity */ \"./src/queriedEntity/queriedEntity.js\"));\n\nvar _detailedEntityReducer = _interopRequireDefault(__webpack_require__(/*! ./detailEntity/detailedEntityReducer */ \"./src/detailEntity/detailedEntityReducer.js\"));\n\nvar _detailedEntity = _interopRequireDefault(__webpack_require__(/*! ./detailEntity/detailedEntity */ \"./src/detailEntity/detailedEntity.js\"));\n\nvar _queriedEntityActions = __webpack_require__(/*! ./queriedEntity/queriedEntityActions */ \"./src/queriedEntity/queriedEntityActions.js\");\n\nvar _detailedEntityActions = __webpack_require__(/*! ./detailEntity/detailedEntityActions */ \"./src/detailEntity/detailedEntityActions.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n//# sourceURL=webpack://rest-react-redux/./src/index.js?"); 156 | 157 | /***/ }), 158 | 159 | /***/ "./src/queriedEntity/queriedEntity.js": 160 | /*!********************************************!*\ 161 | !*** ./src/queriedEntity/queriedEntity.js ***! 162 | \********************************************/ 163 | /*! no static exports found */ 164 | /***/ (function(module, exports, __webpack_require__) { 165 | 166 | "use strict"; 167 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"react\"));\n\nvar _reactRedux = __webpack_require__(/*! react-redux */ \"react-redux\");\n\nvar _helpers = __webpack_require__(/*! ../helpers */ \"./src/helpers.js\");\n\nvar _queriedEntityActions = __webpack_require__(/*! ./queriedEntityActions */ \"./src/queriedEntity/queriedEntityActions.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _typeof(obj) { if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\n/**\n * Queried entity abstraction (Higher Order Component)\n * Retrieves entityName, end point url and params. Dispatches queries and keeps track of the queries.\n * This component will recycles cached queries after. It retains at most RETAIN_NUMBER of queries\n * Per entityName used, there must be a reducer with the same name located at reducers/index.js\n */\n// Default number of queries to cache in store\nvar RETAIN_NUMBER = 10; // Default time for a valid preload (milliseconds)\n\nvar PRELOAD_VALID_TIME = 10000; // Default time that is compared with average network time to decide whether to perform preload (milliseconds)\n\nvar SMART_THRESHOLD_TIME = 300; // Default field that maps the results in the response body, if set to null, the whole response will be returned;\n\nvar RESULT_FIELD = 'content'; // These props should be filtered before inject\n\nvar filteredProps = {};\n['queryEntities', 'pushToQueue', 'createEntity', 'updateEntity', 'patchEntity', 'deleteEntity'].forEach(function (prop) {\n return filteredProps[prop] = undefined;\n});\n\nvar _default = function _default(entityName) {\n var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},\n _ref$resultField = _ref.resultField,\n resultField = _ref$resultField === void 0 ? RESULT_FIELD : _ref$resultField,\n _ref$hideLoadIfDataFo = _ref.hideLoadIfDataFound,\n hideLoadIfDataFound = _ref$hideLoadIfDataFo === void 0 ? true : _ref$hideLoadIfDataFo,\n _ref$retain_number = _ref.retain_number,\n retain_number = _ref$retain_number === void 0 ? RETAIN_NUMBER : _ref$retain_number,\n reducer_name = _ref.reducer_name,\n _ref$preloadValidTime = _ref.preloadValidTime,\n preloadValidTime = _ref$preloadValidTime === void 0 ? PRELOAD_VALID_TIME : _ref$preloadValidTime,\n _ref$smartPreload = _ref.smartPreload,\n smartPreload = _ref$smartPreload === void 0 ? false : _ref$smartPreload,\n _ref$smartThresholdTi = _ref.smartThresholdTime,\n smartThresholdTime = _ref$smartThresholdTi === void 0 ? SMART_THRESHOLD_TIME : _ref$smartThresholdTi;\n\n return function (WrappedComponent) {\n var _class, _temp;\n\n return (0, _reactRedux.connect)(function (state) {\n return _defineProperty({}, (0, _helpers.PL)(entityName), state[reducer_name || (0, _helpers.PL)(entityName)]);\n }, {\n queryEntities: _queriedEntityActions.queryEntities,\n pushToQueue: _queriedEntityActions.pushToQueue,\n createEntity: _queriedEntityActions.createEntity,\n updateEntity: _queriedEntityActions.updateEntity,\n patchEntity: _queriedEntityActions.patchEntity,\n deleteEntity: _queriedEntityActions.deleteEntity\n })((_temp = _class =\n /*#__PURE__*/\n function (_React$Component) {\n _inherits(_class, _React$Component);\n\n function _class() {\n var _getPrototypeOf2;\n\n var _this;\n\n _classCallCheck(this, _class);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(_class)).call.apply(_getPrototypeOf2, [this].concat(args)));\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"state\", {\n params: {},\n loadingData: false\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"preLoaderFunc\", undefined);\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"initialQuery\", function (url) {\n var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n\n _this.setState({\n url: url\n });\n\n return _this.query(params, url, true);\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"query\", function () {\n var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _this.state.params;\n var url = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _this.state.url;\n var initial = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;\n var oldParams = initial ? _objectSpread({}, params) : _objectSpread({}, _this.state.params);\n\n var newParams = _objectSpread({}, oldParams, params);\n\n _this.setState({\n params: newParams,\n loadingData: true\n });\n\n var data = _this.props[(0, _helpers.PL)(entityName)][(0, _helpers.encodeAPICall)(url, newParams)];\n\n if (!data || !hideLoadIfDataFound) _this.props.freeze();\n\n _this.preload(params, url); // If it should not load the data\n\n\n if (!_this.shouldLoad(data)) return Promise.resolve();\n return _this.props.queryEntities(entityName, url, newParams, !data, false, smartPreload).then(function () {\n _this.setState({\n loadingData: false\n });\n\n _this.props.unfreeze();\n\n _this.collectGarbage(url, newParams);\n }).catch(function () {\n _this.setState({\n loadingData: false,\n params: oldParams\n });\n\n _this.props.unfreeze();\n });\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"checkSetup\", function () {\n var url = _this.state.url;\n if (!url) throw new Error(\"No url specified for \".concat(entityName));\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"shouldLoad\", function (data) {\n if (!data) return true;\n if (data === _helpers.LOADING) return false; // Check whether pre-loaded less than 10 seconds ago\n\n if (data.preloadedAt && new Date() - data.preloadedAt < preloadValidTime) return false;\n return true;\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"create\", function (entity) {\n _this.checkSetup();\n\n _this.props.freeze();\n\n return _this.props.createEntity(entityName, entity, _this.state.url).then(function () {\n _this.props.unfreeze();\n\n _this.query();\n });\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"update\", function (entity) {\n _this.checkSetup();\n\n _this.props.freeze();\n\n return _this.props.updateEntity(entityName, entity, _this.state.url, resultField).then(function () {\n _this.props.unfreeze();\n\n _this.query();\n });\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"patch\", function (fields) {\n _this.checkSetup();\n\n _this.props.freeze();\n\n return _this.props.patchEntity(entityName, fields, _this.state.url, resultField).then(function () {\n _this.props.unfreeze();\n\n _this.query();\n });\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"delete\", function (entity) {\n _this.checkSetup();\n\n if (typeof entity === 'string') entity = {\n id: entity\n };\n\n _this.props.freeze();\n\n return _this.props.deleteEntity(entityName, entity, _this.state.url, resultField).then(function () {\n _this.props.unfreeze();\n\n _this.query();\n });\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"setPreLoader\", function (preLoaderFunc) {\n _this.preLoaderFunc = preLoaderFunc;\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"preload\", function (params, url) {\n if (!_this.preLoaderFunc) return; // If in smartPreload mode and average of network calls are above 0.3 seconds do not preload\n\n if (smartPreload) {\n var _this$props$PL$networ = _this.props[(0, _helpers.PL)(entityName)].networkTimer,\n average = _this$props$PL$networ.average,\n numberOfCalls = _this$props$PL$networ.numberOfCalls;\n\n if (numberOfCalls > 3 && average > smartThresholdTime) return;\n } // The next 3 lines are repetitive and should be optimized\n\n\n var queryData = _this.props[(0, _helpers.PL)(entityName)][(0, _helpers.encodeAPICall)(url, params)] || {};\n var queryMetadata = resultField ? _objectSpread({}, queryData) : undefined;\n if (resultField) delete queryMetadata[resultField];\n\n var paramsList = _this.preLoaderFunc(params, _objectSpread({}, _this.state.params, params), _objectSpread({}, queryMetadata));\n\n paramsList.forEach(function (params) {\n var fullParams = _objectSpread({}, _this.state.params, params);\n\n var data = _this.props[(0, _helpers.PL)(entityName)][(0, _helpers.encodeAPICall)(url, fullParams)];\n\n if (data) return;\n\n _this.props.queryEntities(entityName, url, fullParams, !data, true, smartPreload).then(function () {\n _this.collectGarbage(url, fullParams);\n }).catch(function () {});\n });\n });\n\n _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), \"collectGarbage\", function (url, params) {\n return _this.props.pushToQueue(entityName, (0, _helpers.encodeAPICall)(url, params), retain_number);\n });\n\n return _this;\n }\n\n _createClass(_class, [{\n key: \"render\",\n value: function render() {\n var _injectedProps;\n\n var _this$state = this.state,\n url = _this$state.url,\n params = _this$state.params;\n var queryData = this.props[(0, _helpers.PL)(entityName)][(0, _helpers.encodeAPICall)(url, params)] || {};\n var queryMetadata = resultField ? _objectSpread({}, queryData) : undefined;\n if (resultField) delete queryMetadata[resultField];\n var injectedProps = (_injectedProps = {}, _defineProperty(_injectedProps, (0, _helpers.PL)(entityName) + 'QueryParams', this.state.params), _defineProperty(_injectedProps, (0, _helpers.PL)(entityName), (resultField ? queryData && queryData[resultField] : queryData) || []), _defineProperty(_injectedProps, (0, _helpers.PL)(entityName) + 'Metadata', queryMetadata), _defineProperty(_injectedProps, 'initialQuery' + (0, _helpers.CFL)((0, _helpers.PL)(entityName)), this.initialQuery), _defineProperty(_injectedProps, 'query' + (0, _helpers.CFL)((0, _helpers.PL)(entityName)), this.query), _defineProperty(_injectedProps, 'create' + (0, _helpers.CFL)(entityName), this.create), _defineProperty(_injectedProps, 'update' + (0, _helpers.CFL)(entityName), this.update), _defineProperty(_injectedProps, 'patch' + (0, _helpers.CFL)(entityName), this.patch), _defineProperty(_injectedProps, 'delete' + (0, _helpers.CFL)(entityName), this.delete), _defineProperty(_injectedProps, 'set' + (0, _helpers.CFL)((0, _helpers.PL)(entityName)) + 'Preloader', this.setPreLoader), _defineProperty(_injectedProps, 'loading' + (0, _helpers.CFL)((0, _helpers.PL)(entityName)), this.state.loadingData), _injectedProps);\n return _react.default.createElement(WrappedComponent, _extends({}, this.props, filteredProps, injectedProps));\n }\n }]);\n\n return _class;\n }(_react.default.Component), _defineProperty(_class, \"defaultProps\", {\n freeze: function freeze() {},\n unfreeze: function unfreeze() {}\n }), _temp));\n };\n};\n\nexports.default = _default;\n\n//# sourceURL=webpack://rest-react-redux/./src/queriedEntity/queriedEntity.js?"); 168 | 169 | /***/ }), 170 | 171 | /***/ "./src/queriedEntity/queriedEntityActions.js": 172 | /*!***************************************************!*\ 173 | !*** ./src/queriedEntity/queriedEntityActions.js ***! 174 | \***************************************************/ 175 | /*! no static exports found */ 176 | /***/ (function(module, exports, __webpack_require__) { 177 | 178 | "use strict"; 179 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.deleteEntity = exports.patchEntity = exports.updateEntity = exports.createEntity = exports.pushToQueue = exports.queryEntities = exports.patchEntityDispatch = void 0;\n\nvar types = _interopRequireWildcard(__webpack_require__(/*! ../helpers */ \"./src/helpers.js\"));\n\nvar _axios = _interopRequireDefault(__webpack_require__(/*! axios */ \"axios\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }\n\nfunction _objectDestructuringEmpty(obj) { if (obj == null) throw new TypeError(\"Cannot destructure undefined\"); }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\n/**\n * Actions\n */\nvar insertQuery = function insertQuery(payload, entityName) {\n return {\n type: types.INSERT_QUERY(entityName),\n payload: payload\n };\n};\n\nvar updateEntityDispatch = function updateEntityDispatch(payload, entityName) {\n return {\n type: types.UPDATE_ENTITY(entityName),\n payload: payload\n };\n};\n\nvar patchEntityDispatch = function patchEntityDispatch(payload, entityName) {\n return {\n type: types.PATCH_ENTITY(entityName),\n payload: payload\n };\n};\n\nexports.patchEntityDispatch = patchEntityDispatch;\n\nvar deleteEntityDispatch = function deleteEntityDispatch(payload, entityName) {\n return {\n type: types.DELETE_ENTITY(entityName),\n payload: payload\n };\n};\n\nvar updateNetworkTimer = function updateNetworkTimer(payload, entityName) {\n return {\n type: types.UPDATE_NETWORK_TIMER(entityName),\n payload: payload\n };\n};\n\nvar queryEntities = function queryEntities(entityName, url, params) {\n var hasData = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;\n var setPreloadFlag = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;\n var smartPreload = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;\n return function (dispatch) {\n var query = (0, types.encodeAPICall)(url, params);\n hasData && dispatch(insertQuery({\n LOADING: types.LOADING,\n query: query\n }, entityName));\n var time;\n if (smartPreload) time = new Date();\n return _axios.default.get(url, {\n params: params\n }).then(function (_ref) {\n var data = _ref.data;\n\n if (smartPreload) {\n var timeDiff = new Date() - time;\n dispatch(updateNetworkTimer(timeDiff, entityName));\n }\n\n var payload = setPreloadFlag ? _objectSpread({}, data, {\n query: query,\n preloadedAt: new Date()\n }) : _objectSpread({}, data, {\n query: query\n });\n dispatch(insertQuery(payload, entityName));\n });\n };\n};\n\nexports.queryEntities = queryEntities;\n\nvar pushToQueue = function pushToQueue(entityName, key, retain_number) {\n return {\n type: types.PUSH_TO_TRACKING_QUEUE(entityName),\n payload: {\n key: key,\n retain_number: retain_number\n }\n };\n};\n\nexports.pushToQueue = pushToQueue;\n\nvar createEntity = function createEntity(entityName, entity, url, onSuccess, onFailure) {\n // The returned data will not be of any special use since the position it is placed in db is unknown\n // therefore no dispatch is made to store\n return function () {\n return _axios.default.post(url, entity);\n };\n};\n\nexports.createEntity = createEntity;\n\nvar updateEntity = function updateEntity(entityName, entity, url, resultField) {\n if (!entity.id) throw new Error(\"Entity \".concat(entityName, \" does not have id to update\"));\n return function (dispatch) {\n return _axios.default.put((0, types.detailedUrl)(url, entity.id), entity).then(function (_ref2) {\n _objectDestructuringEmpty(_ref2);\n\n dispatch(updateEntityDispatch({\n entity: entity,\n resultField: resultField\n }, entityName));\n });\n };\n};\n\nexports.updateEntity = updateEntity;\n\nvar patchEntity = function patchEntity(entityName, entity, url, resultField) {\n if (!entity.id) throw new Error(\"Entity \".concat(entityName, \" does not have id to patch\"));\n return function (dispatch) {\n return _axios.default.patch((0, types.detailedUrl)(url, entity.id), entity).then(function () {\n dispatch(patchEntityDispatch({\n entity: entity,\n resultField: resultField\n }, entityName));\n });\n };\n};\n\nexports.patchEntity = patchEntity;\n\nvar deleteEntity = function deleteEntity(entityName, entity, url, resultField) {\n if (!entity.id) throw new Error(\"Entity \".concat(entityName, \" does not have id to delete\"));\n return function (dispatch) {\n return _axios.default.delete((0, types.detailedUrl)(url, entity.id)).then(function () {\n dispatch(deleteEntityDispatch({\n entity: entity,\n resultField: resultField\n }, entityName));\n });\n };\n};\n\nexports.deleteEntity = deleteEntity;\n\n//# sourceURL=webpack://rest-react-redux/./src/queriedEntity/queriedEntityActions.js?"); 180 | 181 | /***/ }), 182 | 183 | /***/ "./src/queriedEntity/queriedEntityReducer.js": 184 | /*!***************************************************!*\ 185 | !*** ./src/queriedEntity/queriedEntityReducer.js ***! 186 | \***************************************************/ 187 | /*! no static exports found */ 188 | /***/ (function(module, exports, __webpack_require__) { 189 | 190 | "use strict"; 191 | eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar types = _interopRequireWildcard(__webpack_require__(/*! ../helpers */ \"./src/helpers.js\"));\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }\n\nfunction _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }\n\nfunction _nonIterableSpread() { throw new TypeError(\"Invalid attempt to spread non-iterable instance\"); }\n\nfunction _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === \"[object Arguments]\") return Array.from(iter); }\n\nfunction _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nvar defaultState = {\n tracker: [],\n networkTimer: {\n average: 0,\n numberOfCalls: 0\n }\n};\n/**\n * Reducer generator for storing and to caching queried entities\n */\n\nvar _default = function _default(entityName) {\n return function () {\n var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultState;\n var action = arguments.length > 1 ? arguments[1] : undefined;\n\n switch (action.type) {\n case types.INSERT_QUERY(entityName):\n {\n return _objectSpread({}, state, _defineProperty({}, action.payload.query, action.payload));\n }\n\n case types.UPDATE_NETWORK_TIMER(entityName):\n {\n var _state$networkTimer = state.networkTimer,\n average = _state$networkTimer.average,\n numberOfCalls = _state$networkTimer.numberOfCalls; // Cumulative averaging\n\n var newAverage = (average * numberOfCalls + action.payload) / (numberOfCalls + 1);\n var newNumberOfCalls = numberOfCalls + 1;\n var networkTimer = {\n average: newAverage,\n numberOfCalls: newNumberOfCalls\n };\n return _objectSpread({}, state, {\n networkTimer: networkTimer\n });\n }\n\n case types.PUSH_TO_TRACKING_QUEUE(entityName):\n {\n var tracker = _toConsumableArray(state.tracker);\n\n var _action$payload = action.payload,\n key = _action$payload.key,\n retain_number = _action$payload.retain_number;\n var index = tracker.indexOf(key);\n if (index > -1) tracker.splice(index, 1);else if (tracker.length >= retain_number) {\n delete state[tracker[0]];\n tracker.shift();\n }\n tracker.push(key);\n return _objectSpread({}, state, {\n tracker: tracker\n });\n }\n\n case types.UPDATE_ENTITY(entityName):\n {\n var _tracker = state.tracker;\n if (_tracker.length === 0) return state;\n var lastQuery = _tracker[_tracker.length - 1];\n var lastQueryData = state[lastQuery];\n if (!lastQueryData) return state;\n var _action$payload2 = action.payload,\n entity = _action$payload2.entity,\n resultField = _action$payload2.resultField;\n var newQueryData = lastQueryData[resultField].map(function (data) {\n if (entity.id === data.id) return entity;\n return data;\n });\n return _objectSpread({}, state, _defineProperty({}, lastQuery, newQueryData));\n }\n\n case types.PATCH_ENTITY(entityName):\n {\n var _tracker2 = state.tracker;\n if (_tracker2.length === 0) return state;\n var _lastQuery = _tracker2[_tracker2.length - 1];\n var _lastQueryData = state[_lastQuery];\n if (!_lastQueryData) return state;\n var patchedFields = action.payload;\n\n var _newQueryData = _lastQueryData.map(function (data) {\n if (patchedFields.id === _newQueryData.id) return _objectSpread({}, data, patchedFields);\n return data;\n });\n\n return _objectSpread({}, state, _defineProperty({}, _lastQuery, _newQueryData));\n }\n\n case types.DELETE_ENTITY(entityName):\n {\n var _tracker3 = state.tracker;\n if (_tracker3.length === 0) return state;\n var _action$payload3 = action.payload,\n _resultField = _action$payload3.resultField,\n _entity = _action$payload3.entity;\n var _lastQuery2 = _tracker3[_tracker3.length - 1];\n var _lastQueryData2 = state[_lastQuery2];\n if (!_lastQueryData2) return state;\n\n var _newQueryData2 = _lastQueryData2[_resultField].filter(function (data) {\n return _entity.id !== data.id;\n });\n\n return _objectSpread({}, state, _defineProperty({}, _lastQuery2, _newQueryData2));\n }\n\n default:\n return state;\n }\n };\n};\n\nexports.default = _default;\n\n//# sourceURL=webpack://rest-react-redux/./src/queriedEntity/queriedEntityReducer.js?"); 192 | 193 | /***/ }), 194 | 195 | /***/ "axios": 196 | /*!************************!*\ 197 | !*** external "axios" ***! 198 | \************************/ 199 | /*! no static exports found */ 200 | /***/ (function(module, exports) { 201 | 202 | eval("module.exports = __WEBPACK_EXTERNAL_MODULE_axios__;\n\n//# sourceURL=webpack://rest-react-redux/external_%22axios%22?"); 203 | 204 | /***/ }), 205 | 206 | /***/ "react": 207 | /*!************************!*\ 208 | !*** external "react" ***! 209 | \************************/ 210 | /*! no static exports found */ 211 | /***/ (function(module, exports) { 212 | 213 | eval("module.exports = __WEBPACK_EXTERNAL_MODULE_react__;\n\n//# sourceURL=webpack://rest-react-redux/external_%22react%22?"); 214 | 215 | /***/ }), 216 | 217 | /***/ "react-redux": 218 | /*!******************************!*\ 219 | !*** external "react-redux" ***! 220 | \******************************/ 221 | /*! no static exports found */ 222 | /***/ (function(module, exports) { 223 | 224 | eval("module.exports = __WEBPACK_EXTERNAL_MODULE_react_redux__;\n\n//# sourceURL=webpack://rest-react-redux/external_%22react-redux%22?"); 225 | 226 | /***/ }) 227 | 228 | /******/ }); 229 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var lib = require('./dist/index'); 4 | 5 | // Reducer 6 | Object.defineProperty(exports , 'queriedEntityReducer', { 7 | value: lib.queriedEntityReducer 8 | }); 9 | 10 | // HOC 11 | Object.defineProperty(exports , 'queriedEntity', { 12 | value: lib.queriedEntity 13 | }); 14 | 15 | // Reducer 16 | Object.defineProperty(exports , 'detailedEntityReducer', { 17 | value: lib.detailedEntityReducer 18 | }); 19 | 20 | // HOC 21 | Object.defineProperty(exports , 'detailedEntity', { 22 | value: lib.detailedEntity 23 | }); 24 | 25 | // Action 26 | Object.defineProperty(exports , 'queryEntities', { 27 | value: lib.queryEntities 28 | }); 29 | 30 | // Action 31 | Object.defineProperty(exports , 'getEntity', { 32 | value: lib.getEntity 33 | }); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-react-redux", 3 | "version": "0.0.8", 4 | "description": "A library that provides higher order components for communicating with a backend server via standard rest API protocol", 5 | "homepage": "https://github.com/barimani/Rest-React-Redux", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "mocha --compilers js:@babel/register", 9 | "build": "webpack" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "redux", 14 | "API", 15 | "crud", 16 | "rest" 17 | ], 18 | "author": "Bari Barimani", 19 | "license": "MIT", 20 | "dependencies": { 21 | "axios": "^0.18.0", 22 | "react": "^16.0.0", 23 | "react-redux": "^5.0.0", 24 | "redux": "^4.0.0", 25 | "redux-thunk": "^2.2.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.0.0-beta.55", 29 | "@babel/plugin-proposal-class-properties": "^7.0.0-beta.55", 30 | "@babel/plugin-proposal-decorators": "^7.0.0-rc.1", 31 | "@babel/preset-env": "^7.0.0-beta.55", 32 | "@babel/preset-react": "^7.0.0-beta.55", 33 | "@babel/register": "^7.0.0-rc.1", 34 | "axios-mock-adapter": "^1.15.0", 35 | "babel-loader": "^8.0.0-beta.4", 36 | "babel-plugin-transform-class-properties": "^6.24.1", 37 | "babel-preset-es2015-no-commonjs": "0.0.2", 38 | "babel-preset-stage-2": "^6.24.1", 39 | "chai": "^4.1.2", 40 | "enzyme": "^3.4.1", 41 | "enzyme-adapter-react-16": "^1.2.0", 42 | "jsdom": "^11.12.0", 43 | "mocha": "^5.2.0", 44 | "react-dom": "^16.4.2", 45 | "webpack": "^4.16.4", 46 | "webpack-cli": "^3.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/detailEntity/detailedEntity.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import {getEntity, createItem, deleteItem, patchItem, pushToQueue, updateItem} from "./detailedEntityActions"; 4 | import {CFL} from "../helpers"; 5 | 6 | /** 7 | * Detailed entity abstraction (Higher Order Component) 8 | * Retrieves entityName, end point url. Dispatches queries and keeps track of the queries. 9 | * API contract: should accept page and pageSize as params, should return a json with the property item as list 10 | * This component will recycles cached queries after. It retains at most RETAIN_NUMBER of queries 11 | * Per entityName used, there must be a reducer with the same name located at reducers/index.js 12 | * 13 | * staticURL is a url provided at the annotation level and requires no logic to generate it i.e. no 14 | * parameter exists inside it. If your url contains custom parameters (usually ids that component knows about it) 15 | * use dynamicURL on the initialQuery. This will override the staticURL if any provided. 16 | */ 17 | 18 | // These props should be filtered before inject 19 | const filteredProps = {}; 20 | ['getEntity', 'pushToQueue', 'createItem', 'updateItem', 'patchItem', 'deleteItem'] 21 | .forEach(prop => filteredProps[prop] = undefined); 22 | 23 | // Number of queries to cache in store 24 | const RETAIN_NUMBER = 10; 25 | 26 | 27 | export default (entityName, {reducerName, retain_number = RETAIN_NUMBER} = {}) => WrappedComponent => 28 | connect(state => ({[entityName]: state[reducerName || entityName]}), 29 | {getEntity, pushToQueue, createItem, updateItem, patchItem, deleteItem})( 30 | class extends React.Component { 31 | 32 | static defaultProps = {freeze: () => {}, unfreeze: () => {}}; 33 | 34 | state = {entityId: null, loadingData: false}; 35 | 36 | initialGet = (url, entityId) => { 37 | this.setState({loadingData: true, url, entityId}); 38 | return this.get(url, entityId); 39 | }; 40 | 41 | get = (url = this.state.url, entityId = this.state.entityId) => { 42 | const entity = this.props[entityName][entityId]; 43 | if (!entity) this.props.freeze(); 44 | this.setState({loadingData: true}); 45 | return this.props.getEntity(entityName, url) 46 | .then(() => {this.setState({loadingData: false});this.props.unfreeze();this.collectGarbage();}) 47 | .catch(() => {this.setState({loadingData: false});this.props.unfreeze();}); 48 | }; 49 | 50 | // Checks whether initialGet is called and url is known 51 | checkSetup = () => { 52 | const { url, entityId } = this.state; 53 | if (!url) throw new Error(`No url specified for ${entityName}`); 54 | if (!entityId) throw new Error(`No entityId specified for ${entityName}`); 55 | }; 56 | 57 | // Garbage collector so the redux storage will not blow up! 58 | collectGarbage = () => this.props.pushToQueue(entityName, this.state.entityId, retain_number); 59 | 60 | // Entity must contain id and the whole properties of the model 61 | update = entity => { 62 | this.checkSetup(); 63 | this.props.freeze(); 64 | return this.props.updateItem(entityName, entity, this.state.entityId, this.state.url) 65 | .then(() => { 66 | this.props.unfreeze(); 67 | this.get(); 68 | }); 69 | }; 70 | 71 | // The fields to be patched, field should contain id 72 | patch = fields => { 73 | this.checkSetup(); 74 | this.props.freeze(); 75 | return this.props.patchItem(entityName, fields, this.state.entityId, this.state.url) 76 | .then(() => { 77 | this.props.unfreeze(); 78 | this.get(); 79 | }); 80 | }; 81 | 82 | // Accepts the entity object that contains id or the id itself as a string 83 | delete = () => { 84 | this.checkSetup(); 85 | this.props.freeze(); 86 | return this.props.deleteItem(entityName, this.state.entityId, this.state.url) 87 | .then(() => { 88 | this.props.unfreeze(); 89 | this.get(); 90 | }); 91 | }; 92 | 93 | /** @ignore */ 94 | render() { 95 | const {entityId} = this.state; 96 | const entity = this.props[entityName][entityId]; 97 | const item = {[entityName]: entity || {}}; 98 | const injectedProps = { 99 | [entityName]: entity || {}, 100 | ['initialGet' + CFL(entityName)]: this.initialGet, 101 | ['get' + CFL(entityName)]: this.get, 102 | ['update' + CFL(entityName)]: this.update, 103 | ['patch' + CFL(entityName)]: this.patch, 104 | ['delete' + CFL(entityName)]: this.delete, 105 | ['loading' + CFL(entityName)]: this.state.loadingData 106 | }; 107 | return ( 108 | 114 | ) 115 | } 116 | } 117 | ); 118 | -------------------------------------------------------------------------------- /src/detailEntity/detailedEntityActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import * as types from "../helpers"; 3 | 4 | /** 5 | * Actions 6 | */ 7 | const updateItemDispatch = (payload, entityName) => { 8 | return { 9 | type: types.UPDATE_ITEM(entityName), 10 | payload 11 | }; 12 | }; 13 | 14 | export const patchItemDispatch = (payload, entityName) => { 15 | return { 16 | type: types.PATCH_ITEM(entityName), 17 | payload 18 | }; 19 | }; 20 | 21 | const deleteItemDispatch = (payload, entityName) => { 22 | let type = types.REMOVE_ITEM(entityName); 23 | return { 24 | type: type, 25 | payload 26 | }; 27 | }; 28 | 29 | 30 | const insertItem = (payload, entityName) => { 31 | return { 32 | type: types.INSERT_ITEM(entityName), 33 | payload 34 | }; 35 | }; 36 | 37 | export const getEntity = (entityName, url) => { 38 | return dispatch => axios.get(url).then(({data}) => { 39 | dispatch(insertItem(data, entityName)); 40 | }); 41 | }; 42 | 43 | export const pushToQueue = (entityName, id, retain_number) => { 44 | return { 45 | type: types.PUSH_TO_TRACKING_QUEUE_DETAILED(entityName), 46 | payload: {id, retain_number} 47 | }; 48 | }; 49 | 50 | export const createItem = (entityName, entity, url) => { 51 | // The returned data will not be of any special use since the position it is placed in db is unknown 52 | // therefore no dispatch is made to store 53 | return () => axios.post(url, entity); 54 | }; 55 | 56 | export const updateItem = (entityName, entity, entityId, url) => { 57 | return dispatch => axios.put(url, entity).then(() => { 58 | dispatch(updateItemDispatch({entityId, entity}, entityName)); 59 | }); 60 | }; 61 | 62 | export const patchItem = (entityName, entity, entityId, url) => { 63 | return dispatch => axios.patch(url, entity).then(() => { 64 | dispatch(patchItemDispatch({entity, entityId}, entityName)); 65 | }); 66 | }; 67 | 68 | export const deleteItem = (entityName, entityId, url) => { 69 | return dispatch => axios.delete(url).then(() => { 70 | dispatch(deleteItemDispatch(entityId, entityName)); 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /src/detailEntity/detailedEntityReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../helpers"; 2 | 3 | const defaultState = {tracker: []}; 4 | 5 | /** 6 | * Reducer generator for storing and to caching detailed entities 7 | */ 8 | export default entityName => function itemDetailRepo(state = defaultState, action) { 9 | switch (action.type) { 10 | case types.INSERT_ITEM(entityName): { 11 | const entity = action.payload; 12 | return {...state, [entity.id]: entity}; 13 | } 14 | case types.UPDATE_ITEM(entityName): { 15 | const {entity, entityId} = action.payload; 16 | return {...state, [entityId]: {id: entityId, ...entity}}; 17 | } 18 | case types.PATCH_ITEM(entityName): { 19 | const {entity, entityId} = action.payload; 20 | return {...state, [entityId]: {...state[entityId], ...entity}}; 21 | } 22 | case types.REMOVE_ITEM(entityName): { 23 | const entityId = action.payload; 24 | const tracker = [...state.tracker]; 25 | const index = tracker.indexOf(entityId); 26 | if (index > -1) { 27 | delete state[tracker[index]]; 28 | tracker.splice(index, 1); 29 | } 30 | return {...state, tracker}; 31 | } 32 | case (types.PUSH_TO_TRACKING_QUEUE_DETAILED(entityName)): { 33 | const tracker = [...state.tracker]; 34 | const {id, retain_number} = action.payload; 35 | const index = tracker.indexOf(id); 36 | if (index > -1) tracker.splice(index, 1); 37 | else if (tracker.length >= retain_number) { 38 | delete state[tracker[0]]; 39 | tracker.shift(); 40 | } 41 | tracker.push(id); 42 | return {...state, tracker}; 43 | } 44 | default: 45 | return state; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Detailed entity action type generators 3 | */ 4 | export const INSERT_ITEM = entityName => 'INSERT_' + entityName.toUpperCase(); 5 | export const UPDATE_ITEM = entityName => 'UPDATE_' + entityName.toUpperCase(); 6 | export const REMOVE_ITEM = entityName => 'REMOVE_' + entityName.toUpperCase(); 7 | export const PATCH_ITEM = entityName => 'PATCH_' + entityName.toUpperCase(); 8 | export const PUSH_TO_TRACKING_QUEUE_DETAILED = entityName => 'PUSH_TO_TRACKING_QUEUE_DETAILED_' + entityName.toUpperCase(); 9 | 10 | 11 | /** 12 | * Queried entity action type generators 13 | */ 14 | export const INSERT_QUERY = entityName => 'INSERT_QUERY_' + entityName.toUpperCase(); 15 | export const PUSH_TO_TRACKING_QUEUE = entityName => 'PUSH_TO_TRACKING_QUEUE_QUERY_' + entityName.toUpperCase(); 16 | export const UPDATE_NETWORK_TIMER = entityName => 'UPDATE_NETWORK_TIMER_' + entityName.toUpperCase(); 17 | export const UPDATE_ENTITY = entityName => 'UPDATE_ENTITY_' + entityName.toUpperCase(); 18 | export const PATCH_ENTITY = entityName => 'PATCH_ENTITY_' + entityName.toUpperCase(); 19 | export const DELETE_ENTITY = entityName => 'DELETE_ENTITY_' + entityName.toUpperCase(); 20 | 21 | /** 22 | * Generates a unique key from url and query params 23 | * @returns {number} 24 | */ 25 | export const encodeAPICall = (url, params) => { 26 | const sortedParams = {}; 27 | Object.keys(params).sort().forEach(key => sortedParams[key] = params[key]); 28 | const string = JSON.stringify(sortedParams); 29 | return hashCode(url + string); 30 | }; 31 | 32 | /** 33 | * A placeholder constant for signalling the loading status in redux to prevent re-call 34 | */ 35 | export const LOADING = 'LOADING'; 36 | 37 | 38 | /** 39 | * Generates a numerical hash from a string 40 | * @returns {number} 41 | */ 42 | const hashCode = string => { 43 | let hash = 0; 44 | if (string.length === 0) return hash; 45 | for (let i = 0; i < string.length; i++) { 46 | const char = string.charCodeAt(i); 47 | hash = ((hash << 5) - hash) + char; 48 | hash = hash & hash; // Convert to 32bit integer 49 | } 50 | return hash; 51 | }; 52 | 53 | /** 54 | * Capitalizes first letter of a word 55 | * @return {string} 56 | */ 57 | export const CFL = word => { 58 | if (!word) return word; 59 | return word.slice(0, 1).toUpperCase() + word.slice(1, word.length); 60 | }; 61 | 62 | /** 63 | * Pluralizes a word 64 | * @return {string} 65 | */ 66 | export const PL = word => { 67 | if (!word || word.length === 0) return word; 68 | if (/(x|s|ch|sh)$/i.test(word)) return word + 'es'; 69 | if (/y$/i.test(word)) return word.slice(0, word.length - 1) + 'ies'; 70 | return word + 's'; 71 | }; 72 | 73 | 74 | /** 75 | * Constructs a detailed entity url by appending id after the base url 76 | * @returns {string} 77 | */ 78 | export const detailedUrl = (baseUrl, id) => { 79 | const hasTrailingSlash = baseUrl.endsWith('/'); 80 | return baseUrl + (!hasTrailingSlash ? '/' : '') + id + (hasTrailingSlash ? '/' : ''); 81 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import queriedEntityReducer from './queriedEntity/queriedEntityReducer'; 2 | import queriedEntity from './queriedEntity/queriedEntity'; 3 | import detailedEntityReducer from './detailEntity/detailedEntityReducer'; 4 | import detailedEntity from './detailEntity/detailedEntity'; 5 | import {queryEntities} from "./queriedEntity/queriedEntityActions"; 6 | import {getEntity} from "./detailEntity/detailedEntityActions"; 7 | 8 | 9 | export {queriedEntityReducer, queriedEntity, detailedEntityReducer, 10 | detailedEntity, queryEntities, getEntity } 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/queriedEntity/queriedEntity.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import {encodeAPICall, LOADING, PL} from "../helpers"; 4 | import {CFL} from "../helpers"; 5 | import {queryEntities, pushToQueue, createEntity, updateEntity, patchEntity, deleteEntity} from './queriedEntityActions'; 6 | 7 | /** 8 | * Queried entity abstraction (Higher Order Component) 9 | * Retrieves entityName, end point url and params. Dispatches queries and keeps track of the queries. 10 | * This component will recycles cached queries after. It retains at most RETAIN_NUMBER of queries 11 | * Per entityName used, there must be a reducer with the same name located at reducers/index.js 12 | */ 13 | 14 | 15 | // Default number of queries to cache in store 16 | const RETAIN_NUMBER = 10; 17 | 18 | // Default time for a valid preload (milliseconds) 19 | const PRELOAD_VALID_TIME = 10000; 20 | 21 | // Default time that is compared with average network time to decide whether to perform preload (milliseconds) 22 | const SMART_THRESHOLD_TIME = 300; 23 | 24 | // Default field that maps the results in the response body, if set to null, the whole response will be returned; 25 | const RESULT_FIELD = 'content'; 26 | 27 | // These props should be filtered before inject 28 | const filteredProps = {}; 29 | ['queryEntities', 'pushToQueue', 'createEntity', 30 | 'updateEntity', 'patchEntity', 'deleteEntity'].forEach(prop => filteredProps[prop] = undefined); 31 | 32 | export default (entityName, {resultField = RESULT_FIELD, hideLoadIfDataFound = true, 33 | retain_number = RETAIN_NUMBER, reducer_name, preloadValidTime = PRELOAD_VALID_TIME, 34 | smartPreload = false, smartThresholdTime = SMART_THRESHOLD_TIME} = {}) => 35 | WrappedComponent => 36 | connect(state => ({[PL(entityName)]: state[reducer_name || PL(entityName)]}), 37 | {queryEntities, pushToQueue, createEntity, updateEntity, patchEntity, deleteEntity})( 38 | class extends React.Component { 39 | 40 | static defaultProps = {freeze: () => {}, unfreeze: () => {}}; 41 | 42 | state = {params: {}, loadingData: false}; 43 | 44 | // An optional function where pre loads data, argument are params, metadata and returns a new params 45 | // or a list of params to preload 46 | preLoaderFunc = undefined; 47 | 48 | // Sets up the query and makes the initial query 49 | initialQuery = (url, params = {}) => { 50 | this.setState({url}); 51 | return this.query(params, url, true); 52 | }; 53 | 54 | // Queries with the params, will construct query params based on the old ones and new ones 55 | query = (params = this.state.params, url = this.state.url, initial = false) => { 56 | const oldParams = initial ? {...params} : {...this.state.params}; 57 | const newParams = {...oldParams, ...params}; 58 | this.setState({params: newParams, loadingData: true}); 59 | const data = this.props[PL(entityName)][encodeAPICall(url, newParams)]; 60 | if (!data || !hideLoadIfDataFound) this.props.freeze(); 61 | 62 | this.preload(params, url); 63 | 64 | // If it should not load the data 65 | if (!this.shouldLoad(data)) return Promise.resolve(); 66 | 67 | return this.props.queryEntities(entityName, url, newParams, !data, false, smartPreload) 68 | .then(() => {this.setState({loadingData: false});this.props.unfreeze();this.collectGarbage(url, newParams);}) 69 | .catch(() => {this.setState({loadingData: false, params: oldParams});this.props.unfreeze();}); 70 | }; 71 | 72 | // Checks whether initialQuery is called and url is known 73 | checkSetup = () => { 74 | const { url } = this.state; 75 | if (!url) throw new Error(`No url specified for ${entityName}`); 76 | }; 77 | 78 | // Determines whether a network call should be made to refresh 79 | shouldLoad = data => { 80 | if (!data) return true; 81 | if (data === LOADING) return false; 82 | // Check whether pre-loaded less than 10 seconds ago 83 | if (data.preloadedAt && ((new Date()) - data.preloadedAt) < preloadValidTime) return false; 84 | return true; 85 | }; 86 | 87 | // this entity does not contain id 88 | create = entity => { 89 | this.checkSetup(); 90 | this.props.freeze(); 91 | return this.props.createEntity(entityName, entity, this.state.url) 92 | .then(() => { 93 | this.props.unfreeze(); 94 | this.query(); 95 | }); 96 | }; 97 | 98 | // Entity must contain id and the whole properties of the model 99 | update = entity => { 100 | this.checkSetup(); 101 | this.props.freeze(); 102 | return this.props.updateEntity(entityName, entity, this.state.url, resultField) 103 | .then(() => { 104 | this.props.unfreeze(); 105 | this.query(); 106 | }); 107 | }; 108 | 109 | // The fields to be patched, field should contain id 110 | patch = fields => { 111 | this.checkSetup(); 112 | this.props.freeze(); 113 | return this.props.patchEntity(entityName, fields, this.state.url, resultField) 114 | .then(() => { 115 | this.props.unfreeze(); 116 | this.query() 117 | }); 118 | }; 119 | 120 | // Accepts the entity object that contains id or the id itself as a string 121 | delete = entity => { 122 | this.checkSetup(); 123 | if (typeof entity === 'string') entity = {id: entity}; 124 | this.props.freeze(); 125 | return this.props.deleteEntity(entityName, entity, this.state.url, resultField) 126 | .then(() => { 127 | this.props.unfreeze(); 128 | this.query(); 129 | }); 130 | }; 131 | 132 | setPreLoader = preLoaderFunc => {this.preLoaderFunc = preLoaderFunc}; 133 | 134 | preload = (params, url) => { 135 | if (!this.preLoaderFunc) return; 136 | 137 | // If in smartPreload mode and average of network calls are above 0.3 seconds do not preload 138 | if (smartPreload) { 139 | const {average, numberOfCalls} = this.props[PL(entityName)].networkTimer; 140 | if (numberOfCalls > 3 && average > smartThresholdTime) return; 141 | } 142 | 143 | // The next 3 lines are repetitive and should be optimized 144 | const queryData = this.props[PL(entityName)][encodeAPICall(url, params)] || {}; 145 | const queryMetadata = resultField ? {...queryData} : undefined; 146 | if (resultField) delete queryMetadata[resultField]; 147 | 148 | 149 | const paramsList = this.preLoaderFunc(params, {...this.state.params, ...params}, {...queryMetadata}); 150 | 151 | paramsList.forEach(params => { 152 | const fullParams = {...this.state.params, ...params}; 153 | const data = this.props[PL(entityName)][encodeAPICall(url, fullParams)]; 154 | if (data) return; 155 | this.props.queryEntities(entityName, url, fullParams, !data, true, smartPreload) 156 | .then(() => {this.collectGarbage(url, fullParams);}) 157 | .catch(() => {}); 158 | }) 159 | }; 160 | 161 | // Garbage collector so the redux storage will not blow up! 162 | collectGarbage = (url, params) => this.props.pushToQueue(entityName, encodeAPICall(url, params), retain_number); 163 | 164 | render() { 165 | const {url, params} = this.state; 166 | const queryData = this.props[PL(entityName)][encodeAPICall(url, params)] || {}; 167 | const queryMetadata = resultField ? {...queryData} : undefined; 168 | if (resultField) delete queryMetadata[resultField]; 169 | const injectedProps = { 170 | [PL(entityName) + 'QueryParams']: this.state.params, 171 | [PL(entityName)]: (resultField ? (queryData.data && queryData.data[resultField]) : queryData.data) || [], 172 | [PL(entityName) + 'Metadata']: queryMetadata, 173 | ['initialQuery' + CFL(PL(entityName))]: this.initialQuery, 174 | ['query' + CFL(PL(entityName))]: this.query, 175 | ['create' + CFL(entityName)]: this.create, 176 | ['update' + CFL(entityName)]: this.update, 177 | ['patch' + CFL(entityName)]: this.patch, 178 | ['delete' + CFL(entityName)]: this.delete, 179 | ['set' + CFL(PL(entityName)) + 'Preloader']: this.setPreLoader, 180 | ['loading' + CFL(PL(entityName))]: this.state.loadingData, 181 | }; 182 | return ( 183 | 188 | ) 189 | } 190 | } 191 | ); -------------------------------------------------------------------------------- /src/queriedEntity/queriedEntityActions.js: -------------------------------------------------------------------------------- 1 | import {detailedUrl, encodeAPICall, LOADING} from "../helpers"; 2 | import axios from "axios"; 3 | import * as types from "../helpers"; 4 | 5 | /** 6 | * Actions 7 | */ 8 | const insertQuery = (payload, entityName) => { 9 | return { 10 | type: types.INSERT_QUERY(entityName), 11 | payload 12 | }; 13 | }; 14 | 15 | const updateEntityDispatch = (payload, entityName) => { 16 | return { 17 | type: types.UPDATE_ENTITY(entityName), 18 | payload 19 | }; 20 | }; 21 | 22 | export const patchEntityDispatch = (payload, entityName) => { 23 | return { 24 | type: types.PATCH_ENTITY(entityName), 25 | payload 26 | }; 27 | }; 28 | 29 | const deleteEntityDispatch = (payload, entityName) => { 30 | return { 31 | type: types.DELETE_ENTITY(entityName), 32 | payload 33 | }; 34 | }; 35 | 36 | const updateNetworkTimer = (payload, entityName) => { 37 | return { 38 | type: types.UPDATE_NETWORK_TIMER(entityName), 39 | payload 40 | }; 41 | }; 42 | 43 | export const queryEntities = (entityName, url, params, hasData = false, setPreloadFlag = false, smartPreload = false) => { 44 | return dispatch => { 45 | const query = encodeAPICall(url, params); 46 | 47 | hasData && dispatch(insertQuery({LOADING, query}, entityName)); 48 | 49 | let time; 50 | if (smartPreload) time = new Date(); 51 | 52 | return axios.get(url, {params}).then(({data}) => { 53 | 54 | if (smartPreload) { 55 | const timeDiff = (new Date() - time); 56 | dispatch(updateNetworkTimer(timeDiff, entityName)) 57 | } 58 | 59 | const payload = setPreloadFlag ? {data, query, preloadedAt: new Date()} : {data, query}; 60 | dispatch(insertQuery(payload, entityName)); 61 | }); 62 | } 63 | }; 64 | 65 | export const pushToQueue = (entityName, key, retain_number) => { 66 | return { 67 | type: types.PUSH_TO_TRACKING_QUEUE(entityName), 68 | payload: {key, retain_number} 69 | }; 70 | }; 71 | 72 | export const createEntity = (entityName, entity, url) => { 73 | // The returned data will not be of any special use since the position it is placed in db is unknown 74 | // therefore no dispatch is made to store 75 | return () => axios.post(url, entity); 76 | }; 77 | 78 | export const updateEntity = (entityName, entity, url, resultField) => { 79 | if (!entity.id) throw new Error(`Entity ${entityName} does not have id to update`); 80 | return dispatch => axios.put(detailedUrl(url, entity.id), entity) 81 | .then(({}) => { 82 | dispatch(updateEntityDispatch({entity, resultField}, entityName)); 83 | }); 84 | }; 85 | 86 | export const patchEntity = (entityName, entity, url, resultField) => { 87 | if (!entity.id) throw new Error(`Entity ${entityName} does not have id to patch`); 88 | return dispatch => axios.patch(detailedUrl(url, entity.id), entity) 89 | .then(() => { 90 | dispatch(patchEntityDispatch({entity, resultField}, entityName)); 91 | }); 92 | }; 93 | 94 | export const deleteEntity = (entityName, entity, url, resultField) => { 95 | if (!entity.id) throw new Error(`Entity ${entityName} does not have id to delete`); 96 | return dispatch => axios.delete(detailedUrl(url, entity.id)) 97 | .then(() => { 98 | dispatch(deleteEntityDispatch({entity, resultField}, entityName)); 99 | }); 100 | }; 101 | -------------------------------------------------------------------------------- /src/queriedEntity/queriedEntityReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../helpers'; 2 | 3 | const defaultState = {tracker: [], networkTimer: {average: 0, numberOfCalls: 0}}; 4 | 5 | /** 6 | * Reducer generator for storing and to caching queried entities 7 | */ 8 | export default entityName => (state = defaultState, action) => { 9 | switch (action.type) { 10 | case (types.INSERT_QUERY(entityName)): { 11 | return {...state, [action.payload.query]: action.payload}; 12 | } 13 | case (types.UPDATE_NETWORK_TIMER(entityName)): { 14 | const {average, numberOfCalls} = state.networkTimer; 15 | // Cumulative averaging 16 | const newAverage = (average * numberOfCalls + action.payload) / (numberOfCalls + 1); 17 | const newNumberOfCalls = numberOfCalls + 1; 18 | const networkTimer = {average: newAverage, numberOfCalls: newNumberOfCalls}; 19 | return {...state, networkTimer}; 20 | } 21 | case (types.PUSH_TO_TRACKING_QUEUE(entityName)): { 22 | let tracker = [...state.tracker]; 23 | const {key, retain_number} = action.payload; 24 | const index = tracker.indexOf(key); 25 | if (index > -1) tracker.splice(index, 1); 26 | else if (tracker.length >= retain_number) { 27 | delete state[tracker[0]]; 28 | tracker.shift(); 29 | } 30 | tracker.push(key); 31 | return {...state, tracker}; 32 | } 33 | 34 | case (types.UPDATE_ENTITY(entityName)): { 35 | const { tracker } = state; 36 | if (tracker.length === 0) return state; 37 | 38 | const lastQuery = tracker[tracker.length - 1]; 39 | const lastQueryData = state[lastQuery]; 40 | if (!lastQueryData) return state; 41 | const {entity, resultField} = action.payload; 42 | const content = resultField ? lastQueryData.data[resultField] : lastQueryData.data; 43 | const newQueryData = content.map(data => { 44 | if (entity.id === data.id) return entity; 45 | return data; 46 | }); 47 | if (resultField) 48 | return {...state, [lastQuery]: {...lastQueryData, data: {...lastQueryData.data, [resultField]: newQueryData}}}; 49 | else 50 | return {...state, [lastQuery]: {...lastQueryData, data: newQueryData}}; 51 | } 52 | 53 | case (types.PATCH_ENTITY(entityName)): { 54 | const { tracker } = state; 55 | if (tracker.length === 0) return state; 56 | 57 | const lastQuery = tracker[tracker.length - 1]; 58 | const lastQueryData = state[lastQuery]; 59 | if (!lastQueryData) return state; 60 | 61 | const {entity, resultField} = action.payload; 62 | const content = resultField ? lastQueryData[resultField].data : lastQueryData.data; 63 | const newQueryData = content.map(data => { 64 | if (entity.id === newQueryData.id) return {...data, ...entity}; 65 | return data; 66 | }); 67 | if (resultField) 68 | return {...state, [lastQuery]: {...lastQueryData, data: {...lastQueryData.data, [resultField]: newQueryData}}}; 69 | else 70 | return {...state, [lastQuery]: {...lastQueryData, data: newQueryData}}; } 71 | 72 | case (types.DELETE_ENTITY(entityName)): { 73 | const { tracker } = state; 74 | if (tracker.length === 0) return state; 75 | 76 | const {resultField, entity} = action.payload; 77 | const lastQuery = tracker[tracker.length - 1]; 78 | const lastQueryData = state[lastQuery]; 79 | if (!lastQueryData) return state; 80 | const content = resultField ? lastQueryData[resultField].data : lastQueryData.data; 81 | const newQueryData = content.filter(data => entity.id !== data.id); 82 | if (resultField) 83 | return {...state, [lastQuery]: {...lastQueryData, data: {...lastQueryData.data, [resultField]: newQueryData}}}; 84 | else 85 | return {...state, [lastQuery]: {...lastQueryData, data: newQueryData}}; 86 | } 87 | 88 | default: 89 | return state; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/sampleTestApp/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import store from './store'; 4 | import {detailedEntity, queriedEntity} from "../../src/index"; 5 | import mockServer from './mock-adapter'; 6 | mockServer(); 7 | 8 | @detailedEntity('contact') 9 | class ContactDetail extends React.Component { 10 | render() { 11 | return
test component
12 | } 13 | } 14 | 15 | @queriedEntity('contact') 16 | class Contacts extends React.Component { 17 | render() { 18 | return
test component
19 | } 20 | } 21 | 22 | class App extends React.Component { 23 | 24 | render() { 25 | return ( 26 | 27 |
28 | 29 | 30 |
31 |
32 | ) 33 | } 34 | } 35 | 36 | export default App; 37 | export {store}; 38 | -------------------------------------------------------------------------------- /test/sampleTestApp/mock-adapter.js: -------------------------------------------------------------------------------- 1 | import MockAdapter from 'axios-mock-adapter'; 2 | import axios from 'axios'; 3 | 4 | // Unimportant server name 5 | axios.defaults.baseURL = 'http://mockserver.com'; 6 | 7 | 8 | /** 9 | * Sets up the mock server with corresponding endpoints 10 | */ 11 | const configureAxiosMockAdapter = () => { 12 | 13 | const mock = new MockAdapter(axios); 14 | 15 | let contacts = Array.from(Array(50).keys()).map(n => ({id: n.toString(), name: 'Name' + n})); 16 | 17 | mock.onGet(/\/contacts\/\d+/).reply(({url}) => { 18 | const id = url.match(/\d+/)[0]; 19 | const contact = contacts.find(contact => contact.id == id); 20 | if (contact) return [200, contact]; 21 | else return [404]; 22 | }); 23 | 24 | mock.onPut(/\/contacts\/\d+/).reply(({url, data}) => { 25 | const id = url.match(/\d+/)[0]; 26 | let updateSuccessful = false; 27 | contacts = contacts.map(contact => { 28 | if (contact.id == id) { 29 | updateSuccessful = true; 30 | const newContact = JSON.parse(data); 31 | return {id, ...newContact}; 32 | } else return contact; 33 | }); 34 | if (updateSuccessful) return [200, data]; 35 | else return [404]; 36 | }); 37 | 38 | mock.onDelete(/\/contacts\/\d+/).reply(({url}) => { 39 | const id = url.match(/\d+/)[0]; 40 | let removeSuccessful = false; 41 | contacts = contacts.filter(contact => { 42 | if (contact.id == id) { 43 | removeSuccessful = true; 44 | return false; 45 | } else return true; 46 | }); 47 | if (removeSuccessful) return [200]; 48 | else return [404]; 49 | }); 50 | 51 | mock.onGet('contacts').reply(({params}) => { 52 | const {page = 1, pageSize = 10} = params; 53 | 54 | // Check out of index 55 | if (contacts.length < (page - 1) * pageSize) { 56 | return [404] 57 | } else { 58 | const filteredContacts = contacts.slice((page - 1) * pageSize, Math.min(page * pageSize, contacts.length)); 59 | return [200, { 60 | totalItems: contacts.length, 61 | totalPages: Math.floor(contacts.length / pageSize), 62 | content: filteredContacts 63 | }]; 64 | } 65 | }) 66 | }; 67 | 68 | export default configureAxiosMockAdapter; 69 | -------------------------------------------------------------------------------- /test/sampleTestApp/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { detailedEntityReducer, queriedEntityReducer } from '../../src/index'; 3 | 4 | const reducers = { 5 | contact: detailedEntityReducer('contact'), 6 | contacts: queriedEntityReducer('contact'), 7 | }; 8 | 9 | export default combineReducers(reducers); 10 | -------------------------------------------------------------------------------- /test/sampleTestApp/store.js: -------------------------------------------------------------------------------- 1 | import {createStore, compose, applyMiddleware} from 'redux'; 2 | import rootReducer from './reducer'; 3 | import thunk from 'redux-thunk'; 4 | 5 | const store = createStore( 6 | rootReducer, 7 | compose( 8 | applyMiddleware(thunk), 9 | ), 10 | ); 11 | 12 | export default store; 13 | 14 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import {encodeAPICall} from "../src/helpers"; 2 | 3 | const { JSDOM } = require('jsdom'); 4 | 5 | const jsdom = new JSDOM(''); 6 | const { window } = jsdom; 7 | 8 | function copyProps(src, target) { 9 | const props = Object.getOwnPropertyNames(src) 10 | .filter(prop => typeof target[prop] === 'undefined') 11 | .reduce((result, prop) => ({ 12 | ...result, 13 | [prop]: Object.getOwnPropertyDescriptor(src, prop), 14 | }), {}); 15 | Object.defineProperties(target, props); 16 | } 17 | 18 | global.window = window; 19 | global.document = window.document; 20 | global.navigator = { 21 | userAgent: 'node.js', 22 | }; 23 | copyProps(window, global); 24 | 25 | 26 | 27 | 28 | import React from 'react'; 29 | import {expect} from 'chai'; 30 | import Enzyme, { shallow, mount } from 'enzyme'; 31 | import Adapter from 'enzyme-adapter-react-16'; 32 | import App, {store} from './sampleTestApp/App'; 33 | 34 | Enzyme.configure({ adapter: new Adapter() }); 35 | 36 | 37 | 38 | const dummyHOC = WrappedComponent => class extends React.Component{ 39 | render() { 40 | return 41 | } 42 | }; 43 | const Component = () =>
hi
; 44 | 45 | /** 46 | * The following tests are by no means considered as 'unit tests'. The tests describe a story from top to bottom. 47 | * Skipping/modifying a story could cause any test after that to fail 48 | */ 49 | 50 | 51 | describe('Sanity', () => { 52 | it('checks 1 and 1 are equal', () => { 53 | expect(1).to.equal(1); 54 | }); 55 | 56 | it('checks whether Component can be rendered', () => { 57 | const component = shallow(); 58 | expect(component.find('div')).to.have.length(1); 59 | }); 60 | 61 | it('can render a wrapped component and access the wrapped component', () => { 62 | const WrappedComponent = dummyHOC(Component); 63 | const wrapped = shallow().first().shallow(); 64 | expect(wrapped.find('div')).to.have.length(1); 65 | }); 66 | 67 | it('can render a doubly wrapped component and access the wrapped component', () => { 68 | const WrappedComponent = dummyHOC(dummyHOC(Component)); 69 | const wrapped = shallow().first().shallow().first().shallow(); 70 | expect(wrapped.find('div')).to.have.length(1); 71 | }); 72 | 73 | it('can shallow a full App simulation', () => { 74 | const wrapped = shallow(); 75 | expect(wrapped.find('div')).to.have.length(1); 76 | }); 77 | }); 78 | 79 | describe('The example ContactDetails in App with detailedEntity decoration', () => { 80 | let contactDetail; 81 | let getProps; 82 | 83 | before(() => { 84 | // Create a sample wrapped component 85 | contactDetail = mount().find('ContactDetail'); 86 | getProps = () => contactDetail.instance().props; 87 | }); 88 | 89 | it('should have a div and rest-react-redux methods injected as properties', () => { 90 | expect(contactDetail.find('div')).to.have.length(1); 91 | expect(getProps()).to.have.property('initialGetContact'); 92 | expect(getProps()).to.have.property('updateContact'); 93 | }); 94 | 95 | it('is exposed to a store with an empty contact field', () => { 96 | expect(store.getState()).to.have.property('contact').that.is.an('object'); 97 | }); 98 | 99 | it('can call initialGetContact method from the test scope, update store and receive data', done => { 100 | getProps().initialGetContact('/contacts/1', '1').then(() => { 101 | expect(store.getState().contact).to.have.property('1').and.not.to.have.property('2'); 102 | expect(getProps().contact).to.have.property('id').that.is.equal('1'); 103 | done(); 104 | }) 105 | }); 106 | 107 | it('can update the contact info and update store without re-getting', done => { 108 | getProps().updateContact({name: 'changed name'}).then(() => { 109 | expect(getProps().contact).to.have.property('id').that.is.equal('1'); 110 | expect(getProps().contact).to.have.property('name').that.is.equal('changed name'); 111 | done(); 112 | }) 113 | }); 114 | 115 | it('can fetch via getContact and see that the data has not changed', done => { 116 | getProps().getContact().then(() => { 117 | expect(getProps().contact).to.have.property('name').that.is.equal('changed name'); 118 | done(); 119 | }) 120 | }); 121 | 122 | it('can remove the contact entity and reflect in the store', done => { 123 | getProps().deleteContact().then(() => { 124 | expect(store.getState().contact).to.not.have.property('1'); 125 | expect(store.getState().contact.tracker).to.have.lengthOf(0); 126 | expect(getProps().contact).to.be.deep.equal({}); 127 | done(); 128 | }) 129 | }); 130 | 131 | it('can get the second contact entity', done => { 132 | getProps().initialGetContact('/contacts/2', '2').then(() => { 133 | expect(store.getState().contact).to.have.property('2'); 134 | expect(store.getState().contact).to.not.have.property('1'); 135 | expect(getProps().contact).to.have.property('id').that.is.equal('2'); 136 | done(); 137 | }) 138 | }); 139 | 140 | it('can get the third contact and store should remember the second contact as well', done => { 141 | getProps().initialGetContact('/contacts/3', '3').then(() => { 142 | expect(store.getState().contact).to.have.property('2'); 143 | expect(store.getState().contact).to.have.property('3'); 144 | expect(store.getState().contact.tracker).to.have.lengthOf(2); 145 | expect(getProps().contact).to.have.property('id').that.is.equal('3'); 146 | done(); 147 | }) 148 | }); 149 | 150 | }); 151 | 152 | describe('The example Contacts in App with queriedEntity decoration', () => { 153 | let contacts; 154 | let getProps; 155 | 156 | before(() => { 157 | // Create a sample wrapped component 158 | contacts = mount().find('Contacts'); 159 | getProps = () => contacts.instance().props; 160 | }); 161 | 162 | it('should have a div and rest-react-redux methods injected as properties', () => { 163 | expect(contacts.find('div')).to.have.length(1); 164 | expect(getProps()).to.have.property('initialQueryContacts'); 165 | expect(getProps()).to.have.property('updateContact'); 166 | }); 167 | 168 | it('is exposed to a store with an empty contacts field', () => { 169 | expect(store.getState()).to.have.property('contacts').that.is.an('object'); 170 | }); 171 | 172 | it('can call initialQueryContacts method from the test scope, update store and receive data', done => { 173 | getProps().initialQueryContacts('/contacts', {page: 1, pageSize: 10}).then(() => { 174 | const hashedKey = encodeAPICall('/contacts', {page: 1, pageSize: 10}); 175 | expect(store.getState().contacts).to.have.property(hashedKey); 176 | expect(getProps().contacts).to.have.lengthOf(10); 177 | expect(getProps().contactsQueryParams).to.be.deep.equal({page: 1, pageSize: 10}); 178 | expect(getProps().contactsMetadata).to.have.property('totalItems').that.is.not.equal(0); 179 | expect(getProps().contactsMetadata).to.have.property('totalPages').that.is.not.equal(0); 180 | done(); 181 | }) 182 | }); 183 | 184 | it('can perform a second query without losing the previous data', done => { 185 | getProps().queryContacts({page: 2}).then(() => { 186 | // Params updated correctly 187 | expect(getProps().contactsQueryParams).to.be.deep.equal({page: 2, pageSize: 10}); 188 | expect(getProps().contacts).to.have.lengthOf(10); 189 | done(); 190 | }) 191 | }); 192 | 193 | it('query with a different pageSize', done => { 194 | getProps().queryContacts({pageSize: 5}).then(() => { 195 | // Params updated correctly 196 | expect(getProps().contactsQueryParams).to.be.deep.equal({page: 2, pageSize: 5}); 197 | expect(getProps().contacts).to.have.lengthOf(5); 198 | done(); 199 | }) 200 | }); 201 | 202 | it('can update a single entity', done => { 203 | const targetContact = getProps().contacts[0]; 204 | targetContact.name = 'Changed name'; 205 | getProps().updateContact(targetContact).then(() => { 206 | expect(getProps().contacts[0].name).to.equal('Changed name'); 207 | done(); 208 | }) 209 | }); 210 | 211 | it('can delete a single entity', done => { 212 | const targetContact = getProps().contacts[0]; 213 | getProps().deleteContact(targetContact).then(() => { 214 | expect(getProps().contacts[0].name).to.not.equal(targetContact.name); 215 | done(); 216 | }) 217 | }); 218 | }); 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './src/index.js', 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'index.js', 9 | library: 'rest-react-redux', 10 | libraryTarget: 'umd', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['@babel/preset-env', '@babel/preset-react'], 21 | plugins: ['@babel/plugin-proposal-class-properties'] 22 | }, 23 | } 24 | } 25 | ] 26 | }, 27 | externals: { 28 | axios: 'axios', 29 | react: 'react', 30 | redux: 'redux', 31 | ['react-redux']: 'react-redux', 32 | ['redux-thunk']: 'redux-thunk' 33 | } 34 | }; --------------------------------------------------------------------------------