diff --git a/.gitignore b/.gitignore index ba45838..ae3bb0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # build artefacts -/build/ -/build-es/ +# /build/ +# /build-es/ # npm /node_modules/ diff --git a/LICENSE.txt b/LICENSE.txt index 8facf8a..9df9522 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,7 @@ The MIT License (MIT) -Copyright (c) 2015 Michael Contento +Copyright (c) 2016- Gunjan Soni +Copyright (c) 2015-2016 Michael Contento Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 75a12b1..34ccdd7 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,23 @@ # [redux-storage][] -[![build](https://travis-ci.org/michaelcontento/redux-storage.svg?branch=master)](https://travis-ci.org/michaelcontento/redux-storage) -[![dependencies](https://david-dm.org/michaelcontento/redux-storage.svg)](https://david-dm.org/michaelcontento/redux-storage) -[![devDependencies](https://david-dm.org/michaelcontento/redux-storage/dev-status.svg)](https://david-dm.org/michaelcontento/redux-storage#info=devDependencies) +[![build](https://travis-ci.org/guns2410/redux-storage.svg?branch=master)](https://travis-ci.org/react-stack/redux-storage) +[![dependencies](https://david-dm.org/guns2410/redux-storage.svg)](https://david-dm.org/react-stack/redux-storage) +[![devDependencies](https://david-dm.org/guns2410/redux-storage/dev-status.svg)](https://david-dm.org/react-stack/redux-storage#info=devDependencies) [![license](https://img.shields.io/npm/l/redux-storage.svg?style=flat-square)](https://www.npmjs.com/package/redux-storage) [![npm version](https://img.shields.io/npm/v/redux-storage.svg?style=flat-square)](https://www.npmjs.com/package/redux-storage) [![npm downloads](https://img.shields.io/npm/dm/redux-storage.svg?style=flat-square)](https://www.npmjs.com/package/redux-storage) -[![Code Climate](https://codeclimate.com/github/michaelcontento/redux-storage/badges/gpa.svg)](https://codeclimate.com/github/michaelcontento/redux-storage) Save and load the [Redux][] state with ease. -# Deprecated - No longer maintained - -My focus has left the node / react ecosystem and this module is no -longer maintained. Maybe [redux-persist](https://github.com/rt2zz/redux-persist) -is a good replacement for you? Or if you want to step in and become -the new owner - just ping me :smile: - -Thank you for your patience and using this module in the first place! - ## Features * Flexible storage engines + * [indexedDb](https://github.com/prateekbh/redux-storage-engine-indexed-db): based on window.indexedDb * [localStorage][]: based on window.localStorage * Or for environments without `Promise` support [localStorageFakePromise][] * [reactNativeAsyncStorage][]: based on `react-native/AsyncStorage` + * [remoteEndpoint][]: save/load via XHR * Flexible state merger functions * [simple][merger-simple]: merge plain old JS structures (default) * [immutablejs][merger-immutablejs]: merge plain old JS **and** [Immutable][] @@ -164,11 +156,29 @@ import { SHOULD_SAVE } from './constants'; const middleware = createMiddleware(engine, [], [ SHOULD_SAVE ]); ``` +If you want to skip dispatching a redux action everytime something gets saved, +just specify it to the option object, which is the fourth argument. + +```js +import { createMiddleware } from 'redux-storage' + +import { SHOULD_SAVE } from './constants'; + +const middleware = createMiddleware(engine, [], [], { disableDispatchSaveAction: true }); +``` + +# A fork of [redux-storage](https://github.com/michaelcontento/redux-storage) + +The original author of the package [redux-storage](https://github.com/michaelcontento/redux-storage) has decided to deprecate the project and no longer maintained. The package will now be maintained here. + +Thank you [michaelcontento](https://github.com/michaelcontento) for a great library! + ## License The MIT License (MIT) - Copyright (c) 2015 Michael Contento + Copyright (c) 2016- Gunjan Soni + Copyright (c) 2015-2016 Michael Contento Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -187,22 +197,23 @@ const middleware = createMiddleware(engine, [], [ SHOULD_SAVE ]); IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - [merger-simple]: https://github.com/michaelcontento/redux-storage-merger-simple - [merger-immutablejs]: https://github.com/michaelcontento/redux-storage-merger-immutablejs + [merger-simple]: https://github.com/react-stack/redux-storage-merger-simple + [merger-immutablejs]: https://github.com/react-stack/redux-storage-merger-immutablejs [npm-engine]: https://www.npmjs.com/browse/keyword/redux-storage-engine [npm-decorator]: https://www.npmjs.com/browse/keyword/redux-storage-decorator [npm-merger]: https://www.npmjs.com/browse/keyword/redux-storage-merger [Redux]: https://github.com/gaearon/redux [Immutable]: https://github.com/facebook/immutable-js - [redux-storage]: https://github.com/michaelcontento/redux-storage + [redux-storage]: https://github.com/react-stack/redux-storage [react-native]: https://facebook.github.io/react-native/ - [localStorage]: https://github.com/michaelcontento/redux-storage-engine-localStorage - [localStorageFakePromise]: https://github.com/michaelcontento/redux-storage-engine-localStorageFakePromise - [reactNativeAsyncStorage]: https://github.com/michaelcontento/redux-storage-engine-reactNativeAsyncStorage - [LOAD]: https://github.com/michaelcontento/redux-storage/blob/master/src/constants.js#L1 - [SAVE]: https://github.com/michaelcontento/redux-storage/blob/master/src/constants.js#L2 - [debounce]: https://github.com/michaelcontento/redux-storage-decorator-debounce + [localStorage]: https://github.com/react-stack/redux-storage-engine-localStorage + [localStorageFakePromise]: https://github.com/react-stack/redux-storage-engine-localStorageFakePromise + [reactNativeAsyncStorage]: https://github.com/react-stack/redux-storage-engine-reactNativeAsyncStorage + [LOAD]: https://github.com/react-stack/redux-storage/blob/master/src/constants.js#L1 + [SAVE]: https://github.com/react-stack/redux-storage/blob/master/src/constants.js#L2 + [debounce]: https://github.com/react-stack/redux-storage-decorator-debounce [engines]: https://github.com/allegro/redux-storage-decorator-engines - [filter]: https://github.com/michaelcontento/redux-storage-decorator-filter + [filter]: https://github.com/react-stack/redux-storage-decorator-filter [migrate]: https://github.com/mathieudutour/redux-storage-decorator-migrate - [immutablejs]: https://github.com/michaelcontento/redux-storage-decorator-immutablejs + [immutablejs]: https://github.com/react-stack/redux-storage-decorator-immutablejs + [remoteEndpoint]: https://github.com/bionexo/redux-storage-engine-remoteendpoint diff --git a/build-es/__tests__/createMiddleware-test.js b/build-es/__tests__/createMiddleware-test.js new file mode 100644 index 0000000..dd0fb80 --- /dev/null +++ b/build-es/__tests__/createMiddleware-test.js @@ -0,0 +1,271 @@ +'use strict'; + +import createMiddleware from '../createMiddleware'; +import { LOAD, SAVE } from '../constants'; + +describe('createMiddleware', function () { + var oldEnv = void 0; + beforeEach(function () { + oldEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + }); + + afterEach(function () { + process.env.NODE_ENV = oldEnv; + }); + + function describeConsoleWarnInNonProduction(msg, cb, msgCheck) { + describe(msg, function () { + var warn = void 0; + + beforeEach(function () { + warn = sinon.stub(console, 'warn'); + }); + + afterEach(function () { + warn.restore(); + }); + + it('should warn if NODE_ENV != production', function () { + process.env.NODE_ENV = 'develop'; + cb(); + warn.should.have.been.called; + if (msgCheck) { + msgCheck(warn.firstCall.args[0]); + } + }); + + it('should NOT warn if NODE_ENV == production', function () { + process.env.NODE_ENV = 'production'; + cb(); + warn.should.not.have.been.called; + }); + }); + } + + it('should call next with the given action', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + createMiddleware(engine)(store)(next)(action); + + next.should.have.been.calledWith(action); + }); + + it('should return the result of next', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.stub().returns('nextResult'); + var action = { type: 'dummy' }; + + var result = createMiddleware(engine)(store)(next)(action); + + result.should.equal('nextResult'); + }); + + it('should ignore blacklisted actions', function () { + var engine = { save: sinon.spy() }; + var store = {}; + var next = sinon.spy(); + var action = { type: 'IGNORE_ME' }; + + createMiddleware(engine, ['IGNORE_ME'])(store)(next)(action); + + engine.save.should.not.have.been.called; + }); + + it('should ignore non-whitelisted actions', function () { + var engine = { save: sinon.spy() }; + var store = {}; + var next = sinon.spy(); + var action = { type: 'IGNORE_ME' }; + + createMiddleware(engine, [], ['ALLOWED'])(store)(next)(action); + + engine.save.should.not.have.been.called; + }); + + it('should process whitelisted actions', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'ALLOWED' }; + + createMiddleware(engine, [], ['ALLOWED'])(store)(next)(action); + + engine.save.should.have.been.called; + }); + + it('should allow whitelist function', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'ALLOWED' }; + var whitelistFn = function whitelistFn() { + return true; + }; + + createMiddleware(engine, [], whitelistFn)(store)(next)(action); + + engine.save.should.have.been.called; + }); + + it('should ignore actions if the whitelist function returns false', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'ALLOWED' }; + var whitelistFn = function whitelistFn() { + return false; + }; + + createMiddleware(engine, [], whitelistFn)(store)(next)(action); + + engine.save.should.not.have.been.called; + }); + + it('should pass the whole action to the whitelist function', function (done) { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'ALLOWED' }; + var whitelistFn = function whitelistFn(checkAction) { + checkAction.should.deep.equal(action); + done(); + }; + + createMiddleware(engine, [], whitelistFn)(store)(next)(action); + }); + + describeConsoleWarnInNonProduction('should not process functions', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = function action() {}; + + createMiddleware(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }, function (msg) { + msg.should.contain('ACTION IGNORED!'); + msg.should.contain('but received a function'); + }); + + describeConsoleWarnInNonProduction('should not process strings', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = 'haha'; + + createMiddleware(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }, function (msg) { + msg.should.contain('ACTION IGNORED!'); + msg.should.contain('but received: haha'); + }); + + describeConsoleWarnInNonProduction('should not process objects without a type', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { noType: 'damn it' }; + + createMiddleware(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }, function (msg) { + msg.should.contain('ACTION IGNORED!'); + msg.should.contain('objects should have a type property'); + }); + + describeConsoleWarnInNonProduction('should warn about action on both black- and whitelist', function () { + var engine = {}; + createMiddleware(engine, ['A'], ['A']); + }); + + it('should pass the current state to engine.save', function () { + var engine = { save: sinon.stub().resolves() }; + var state = { x: 42 }; + var store = { getState: sinon.stub().returns(state) }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + createMiddleware(engine)(store)(next)(action); + + engine.save.should.have.been.calledWith(state); + }); + + it('should trigger a SAVE action after engine.save', function (done) { + var engine = { save: sinon.stub().resolves() }; + var state = { x: 42 }; + var store = { + getState: sinon.stub().returns(state), + dispatch: sinon.spy() + }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + createMiddleware(engine)(store)(next)(action); + + setTimeout(function () { + var saveAction = { payload: state, type: SAVE }; + store.dispatch.should.have.been.calledWith(saveAction); + done(); + }, 5); + }); + + it('should add the parent action as meta.origin to the saveAction', function (done) { + process.env.NODE_ENV = 'develop'; + + var engine = { save: sinon.stub().resolves() }; + var state = { x: 42 }; + var store = { + getState: sinon.stub().returns(state), + dispatch: sinon.spy() + }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + createMiddleware(engine)(store)(next)(action); + + setTimeout(function () { + var saveAction = { payload: state, type: SAVE, meta: { origin: action } }; + store.dispatch.should.have.been.calledWith(saveAction); + done(); + }, 5); + }); + + it('should do nothing if engine.save fails', function () { + var engine = { save: sinon.stub().rejects() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + createMiddleware(engine)(store)(next)(action); + }); + + it('should always ignore SAVE action', function () { + var engine = { save: sinon.spy() }; + var store = {}; + var next = sinon.spy(); + var action = { type: SAVE }; + + createMiddleware(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }); + + it('should always ignore LOAD action', function () { + var engine = { save: sinon.spy() }; + var store = {}; + var next = sinon.spy(); + var action = { type: LOAD }; + + createMiddleware(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }); +}); \ No newline at end of file diff --git a/build-es/__tests__/index-test.js b/build-es/__tests__/index-test.js new file mode 100644 index 0000000..c11d6c4 --- /dev/null +++ b/build-es/__tests__/index-test.js @@ -0,0 +1,35 @@ +'use strict'; + +var _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; }; + +import defaultImport from '../'; +import * as fullImport from '../'; +import { LOAD, SAVE, createLoader, createMiddleware, reducer } from '../'; + +describe('index', function () { + it('should export everything by default', function () { + // NOTE: the new object is created to include the "default" key + // that exists in fullImport + fullImport.should.be.deep.equal(_extends({}, defaultImport, { 'default': defaultImport })); + }); + + it('should export LOAD', function () { + LOAD.should.be.a.string; + }); + + it('should export SAVE', function () { + SAVE.should.be.a.string; + }); + + it('should export createLoader', function () { + createLoader.should.be.a.func; + }); + + it('should export createMiddleware', function () { + createMiddleware.should.be.a.func; + }); + + it('should export reducer', function () { + reducer.should.be.a.func; + }); +}); \ No newline at end of file diff --git a/build-es/__tests__/reducer-test.js b/build-es/__tests__/reducer-test.js new file mode 100644 index 0000000..86d1959 --- /dev/null +++ b/build-es/__tests__/reducer-test.js @@ -0,0 +1,42 @@ +'use strict'; + +import reducer from '../reducer'; +import { LOAD } from '../constants'; + +describe('reducer', function () { + it('should do nothing for non LOAD actions', function () { + var spy = sinon.spy(); + var oldState = {}; + var action = { type: 'SOMETHING', payload: {} }; + + reducer(spy)(oldState, action); + + spy.should.have.been.calledWith(oldState, action); + }); + + it('should have a default merger in place', function () { + var spy = sinon.spy(); + var oldState = { x: 0, y: 0 }; + var action = { type: LOAD, payload: { y: 42 } }; + + reducer(spy)(oldState, action); + + spy.should.have.been.calledWith({ x: 0, y: 42 }, action); + }); + + it('should allow me to change the merger', function () { + var spy = sinon.spy(); + var oldState = { x: 0, y: 0 }; + var action = { type: LOAD, payload: { y: 42 } }; + + var merger = function merger(a, b) { + a.should.equal(oldState); + b.should.deep.equal({ y: 42 }); + return { c: 1 }; + }; + + reducer(spy, merger)(oldState, action); + + spy.should.have.been.calledWith({ c: 1 }, action); + }); +}); \ No newline at end of file diff --git a/build-es/actions.js b/build-es/actions.js new file mode 100644 index 0000000..81bbf7f --- /dev/null +++ b/build-es/actions.js @@ -0,0 +1,8 @@ +'use strict'; + +import { createAction } from 'redux-actions'; + +import * as constants from './constants'; + +export var load = createAction(constants.LOAD); +export var save = createAction(constants.SAVE); \ No newline at end of file diff --git a/build-es/constants.js b/build-es/constants.js new file mode 100644 index 0000000..efc4a2b --- /dev/null +++ b/build-es/constants.js @@ -0,0 +1,4 @@ +'use strict'; + +export var LOAD = 'REDUX_STORAGE_LOAD'; +export var SAVE = 'REDUX_STORAGE_SAVE'; \ No newline at end of file diff --git a/build-es/createLoader.js b/build-es/createLoader.js new file mode 100644 index 0000000..1a99eaf --- /dev/null +++ b/build-es/createLoader.js @@ -0,0 +1,15 @@ +'use strict'; + +import { load as actionLoad } from './actions'; + +export default (function (engine) { + return function (store) { + var dispatchLoad = function dispatchLoad(state) { + return store.dispatch(actionLoad(state)); + }; + return engine.load().then(function (newState) { + dispatchLoad(newState); + return newState; + }); + }; +}); \ No newline at end of file diff --git a/build-es/createMiddleware.js b/build-es/createMiddleware.js new file mode 100644 index 0000000..22f3f04 --- /dev/null +++ b/build-es/createMiddleware.js @@ -0,0 +1,112 @@ +'use strict'; + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +import isFunction from 'lodash.isfunction'; +import isObject from 'lodash.isobject'; + +import { save as actionSave } from './actions'; +import { LOAD, SAVE } from './constants'; + +function swallow() {} + +function warnAboutConfusingFiltering(blacklist, whitelist) { + blacklist.filter(function (item) { + return whitelist.indexOf(item) !== -1; + }).forEach(function (item) { + console.warn( // eslint-disable-line no-console + '[redux-storage] Action ' + item + ' is on BOTH black- and whitelist.' + ' This is most likely a mistake!'); + }); +} + +function isValidAction(action) { + var isFunc = isFunction(action); + var isObj = isObject(action); + var hasType = isObj && action.hasOwnProperty('type'); + + if (!isFunc && isObj && hasType) { + return true; + } + + if (process.env.NODE_ENV !== 'production') { + if (isFunc) { + console.warn( // eslint-disable-line no-console + '[redux-storage] ACTION IGNORED! Actions should be objects' + ' with a type property but received a function! Your' + ' function resolving middleware (e.g. redux-thunk) must be' + ' placed BEFORE redux-storage!'); + } else if (!isObj) { + console.warn( // eslint-disable-line no-console + '[redux-storage] ACTION IGNORED! Actions should be objects' + (' with a type property but received: ' + action)); + } else if (!hasType) { + console.warn( // eslint-disable-line no-console + '[redux-storage] ACTION IGNORED! Action objects should have' + ' a type property.'); + } + } + + return false; +} + +function handleWhitelist(action, actionWhitelist) { + if (Array.isArray(actionWhitelist)) { + return actionWhitelist.length === 0 ? true // Don't filter if the whitelist is empty + : actionWhitelist.indexOf(action.type) !== -1; + } + + // actionWhitelist is a function that returns true or false + return actionWhitelist(action); +} + +export default (function (engine) { + var actionBlacklist = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; + var actionWhitelist = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; + var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + var opts = Object.assign({ disableDispatchSaveAction: false }, options); + + // Also don't save if we process our own actions + var blacklistedActions = [].concat(_toConsumableArray(actionBlacklist), [LOAD, SAVE]); + + if (process.env.NODE_ENV !== 'production' && Array.isArray(actionWhitelist)) { + warnAboutConfusingFiltering(actionBlacklist, actionWhitelist); + } + + return function (_ref) { + var dispatch = _ref.dispatch, + getState = _ref.getState; + + return function (next) { + return function (action) { + var result = next(action); + + if (!isValidAction(action)) { + return result; + } + + var isOnBlacklist = blacklistedActions.indexOf(action.type) !== -1; + var isOnWhitelist = handleWhitelist(action, actionWhitelist); + + // Skip blacklisted actions + if (!isOnBlacklist && isOnWhitelist) { + var saveState = getState(); + var saveAction = actionSave(saveState); + + if (process.env.NODE_ENV !== 'production') { + if (!saveAction.meta) { + saveAction.meta = {}; + } + saveAction.meta.origin = action; + } + + var dispatchSave = function dispatchSave() { + return dispatch(saveAction); + }; + engine.save(saveState).then(function () { + if (opts.disableDispatchSaveAction === false) { + return dispatchSave(); + } + })['catch'](swallow); + } + + return result; + }; + }; + }; +}); \ No newline at end of file diff --git a/build-es/index.js b/build-es/index.js new file mode 100644 index 0000000..52d7ab7 --- /dev/null +++ b/build-es/index.js @@ -0,0 +1,15 @@ +'use strict'; + +var _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; }; + +export { default as createLoader } from './createLoader'; +export { default as createMiddleware } from './createMiddleware'; +export { default as reducer } from './reducer'; +export { LOAD, SAVE } from './constants'; + +// The full default export is required to be BC with redux-storage <= v1.3.2 +export default _extends({}, require('./constants'), { + createLoader: require('./createLoader')['default'], + createMiddleware: require('./createMiddleware')['default'], + reducer: require('./reducer')['default'] +}); \ No newline at end of file diff --git a/build-es/reducer.js b/build-es/reducer.js new file mode 100644 index 0000000..b2c07d5 --- /dev/null +++ b/build-es/reducer.js @@ -0,0 +1,13 @@ +'use strict'; + +import simpleMerger from 'redux-storage-merger-simple'; + +import { LOAD } from './constants'; + +export default (function (reducer) { + var merger = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : simpleMerger; + + return function (state, action) { + return reducer(action.type === LOAD ? merger(state, action.payload) : state, action); + }; +}); \ No newline at end of file diff --git a/build/__tests__/createMiddleware-test.js b/build/__tests__/createMiddleware-test.js new file mode 100644 index 0000000..86c3007 --- /dev/null +++ b/build/__tests__/createMiddleware-test.js @@ -0,0 +1,276 @@ +'use strict'; + +var _createMiddleware = require('../createMiddleware'); + +var _createMiddleware2 = _interopRequireDefault(_createMiddleware); + +var _constants = require('../constants'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +describe('createMiddleware', function () { + var oldEnv = void 0; + beforeEach(function () { + oldEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + }); + + afterEach(function () { + process.env.NODE_ENV = oldEnv; + }); + + function describeConsoleWarnInNonProduction(msg, cb, msgCheck) { + describe(msg, function () { + var warn = void 0; + + beforeEach(function () { + warn = sinon.stub(console, 'warn'); + }); + + afterEach(function () { + warn.restore(); + }); + + it('should warn if NODE_ENV != production', function () { + process.env.NODE_ENV = 'develop'; + cb(); + warn.should.have.been.called; + if (msgCheck) { + msgCheck(warn.firstCall.args[0]); + } + }); + + it('should NOT warn if NODE_ENV == production', function () { + process.env.NODE_ENV = 'production'; + cb(); + warn.should.not.have.been.called; + }); + }); + } + + it('should call next with the given action', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + next.should.have.been.calledWith(action); + }); + + it('should return the result of next', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.stub().returns('nextResult'); + var action = { type: 'dummy' }; + + var result = (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + result.should.equal('nextResult'); + }); + + it('should ignore blacklisted actions', function () { + var engine = { save: sinon.spy() }; + var store = {}; + var next = sinon.spy(); + var action = { type: 'IGNORE_ME' }; + + (0, _createMiddleware2['default'])(engine, ['IGNORE_ME'])(store)(next)(action); + + engine.save.should.not.have.been.called; + }); + + it('should ignore non-whitelisted actions', function () { + var engine = { save: sinon.spy() }; + var store = {}; + var next = sinon.spy(); + var action = { type: 'IGNORE_ME' }; + + (0, _createMiddleware2['default'])(engine, [], ['ALLOWED'])(store)(next)(action); + + engine.save.should.not.have.been.called; + }); + + it('should process whitelisted actions', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'ALLOWED' }; + + (0, _createMiddleware2['default'])(engine, [], ['ALLOWED'])(store)(next)(action); + + engine.save.should.have.been.called; + }); + + it('should allow whitelist function', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'ALLOWED' }; + var whitelistFn = function whitelistFn() { + return true; + }; + + (0, _createMiddleware2['default'])(engine, [], whitelistFn)(store)(next)(action); + + engine.save.should.have.been.called; + }); + + it('should ignore actions if the whitelist function returns false', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'ALLOWED' }; + var whitelistFn = function whitelistFn() { + return false; + }; + + (0, _createMiddleware2['default'])(engine, [], whitelistFn)(store)(next)(action); + + engine.save.should.not.have.been.called; + }); + + it('should pass the whole action to the whitelist function', function (done) { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'ALLOWED' }; + var whitelistFn = function whitelistFn(checkAction) { + checkAction.should.deep.equal(action); + done(); + }; + + (0, _createMiddleware2['default'])(engine, [], whitelistFn)(store)(next)(action); + }); + + describeConsoleWarnInNonProduction('should not process functions', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = function action() {}; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }, function (msg) { + msg.should.contain('ACTION IGNORED!'); + msg.should.contain('but received a function'); + }); + + describeConsoleWarnInNonProduction('should not process strings', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = 'haha'; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }, function (msg) { + msg.should.contain('ACTION IGNORED!'); + msg.should.contain('but received: haha'); + }); + + describeConsoleWarnInNonProduction('should not process objects without a type', function () { + var engine = { save: sinon.stub().resolves() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { noType: 'damn it' }; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }, function (msg) { + msg.should.contain('ACTION IGNORED!'); + msg.should.contain('objects should have a type property'); + }); + + describeConsoleWarnInNonProduction('should warn about action on both black- and whitelist', function () { + var engine = {}; + (0, _createMiddleware2['default'])(engine, ['A'], ['A']); + }); + + it('should pass the current state to engine.save', function () { + var engine = { save: sinon.stub().resolves() }; + var state = { x: 42 }; + var store = { getState: sinon.stub().returns(state) }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + engine.save.should.have.been.calledWith(state); + }); + + it('should trigger a SAVE action after engine.save', function (done) { + var engine = { save: sinon.stub().resolves() }; + var state = { x: 42 }; + var store = { + getState: sinon.stub().returns(state), + dispatch: sinon.spy() + }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + setTimeout(function () { + var saveAction = { payload: state, type: _constants.SAVE }; + store.dispatch.should.have.been.calledWith(saveAction); + done(); + }, 5); + }); + + it('should add the parent action as meta.origin to the saveAction', function (done) { + process.env.NODE_ENV = 'develop'; + + var engine = { save: sinon.stub().resolves() }; + var state = { x: 42 }; + var store = { + getState: sinon.stub().returns(state), + dispatch: sinon.spy() + }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + setTimeout(function () { + var saveAction = { payload: state, type: _constants.SAVE, meta: { origin: action } }; + store.dispatch.should.have.been.calledWith(saveAction); + done(); + }, 5); + }); + + it('should do nothing if engine.save fails', function () { + var engine = { save: sinon.stub().rejects() }; + var store = { getState: sinon.spy() }; + var next = sinon.spy(); + var action = { type: 'dummy' }; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + }); + + it('should always ignore SAVE action', function () { + var engine = { save: sinon.spy() }; + var store = {}; + var next = sinon.spy(); + var action = { type: _constants.SAVE }; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }); + + it('should always ignore LOAD action', function () { + var engine = { save: sinon.spy() }; + var store = {}; + var next = sinon.spy(); + var action = { type: _constants.LOAD }; + + (0, _createMiddleware2['default'])(engine)(store)(next)(action); + + engine.save.should.not.have.been.called; + }); +}); \ No newline at end of file diff --git a/build/__tests__/index-test.js b/build/__tests__/index-test.js new file mode 100644 index 0000000..4f69789 --- /dev/null +++ b/build/__tests__/index-test.js @@ -0,0 +1,37 @@ +'use strict'; + +var _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; }; + +var _ = require('../'); + +var fullImport = _interopRequireWildcard(_); + +function _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)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +describe('index', function () { + it('should export everything by default', function () { + // NOTE: the new object is created to include the "default" key + // that exists in fullImport + fullImport.should.be.deep.equal(_extends({}, fullImport['default'], { 'default': fullImport['default'] })); + }); + + it('should export LOAD', function () { + _.LOAD.should.be.a.string; + }); + + it('should export SAVE', function () { + _.SAVE.should.be.a.string; + }); + + it('should export createLoader', function () { + _.createLoader.should.be.a.func; + }); + + it('should export createMiddleware', function () { + _.createMiddleware.should.be.a.func; + }); + + it('should export reducer', function () { + _.reducer.should.be.a.func; + }); +}); \ No newline at end of file diff --git a/build/__tests__/reducer-test.js b/build/__tests__/reducer-test.js new file mode 100644 index 0000000..6e00a84 --- /dev/null +++ b/build/__tests__/reducer-test.js @@ -0,0 +1,47 @@ +'use strict'; + +var _reducer = require('../reducer'); + +var _reducer2 = _interopRequireDefault(_reducer); + +var _constants = require('../constants'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +describe('reducer', function () { + it('should do nothing for non LOAD actions', function () { + var spy = sinon.spy(); + var oldState = {}; + var action = { type: 'SOMETHING', payload: {} }; + + (0, _reducer2['default'])(spy)(oldState, action); + + spy.should.have.been.calledWith(oldState, action); + }); + + it('should have a default merger in place', function () { + var spy = sinon.spy(); + var oldState = { x: 0, y: 0 }; + var action = { type: _constants.LOAD, payload: { y: 42 } }; + + (0, _reducer2['default'])(spy)(oldState, action); + + spy.should.have.been.calledWith({ x: 0, y: 42 }, action); + }); + + it('should allow me to change the merger', function () { + var spy = sinon.spy(); + var oldState = { x: 0, y: 0 }; + var action = { type: _constants.LOAD, payload: { y: 42 } }; + + var merger = function merger(a, b) { + a.should.equal(oldState); + b.should.deep.equal({ y: 42 }); + return { c: 1 }; + }; + + (0, _reducer2['default'])(spy, merger)(oldState, action); + + spy.should.have.been.calledWith({ c: 1 }, action); + }); +}); \ No newline at end of file diff --git a/build/actions.js b/build/actions.js new file mode 100644 index 0000000..03e0ff8 --- /dev/null +++ b/build/actions.js @@ -0,0 +1,17 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.save = exports.load = undefined; + +var _reduxActions = require('redux-actions'); + +var _constants = require('./constants'); + +var constants = _interopRequireWildcard(_constants); + +function _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)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } + +var load = exports.load = (0, _reduxActions.createAction)(constants.LOAD); +var save = exports.save = (0, _reduxActions.createAction)(constants.SAVE); \ No newline at end of file diff --git a/build/constants.js b/build/constants.js new file mode 100644 index 0000000..05d1db5 --- /dev/null +++ b/build/constants.js @@ -0,0 +1,7 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var LOAD = exports.LOAD = 'REDUX_STORAGE_LOAD'; +var SAVE = exports.SAVE = 'REDUX_STORAGE_SAVE'; \ No newline at end of file diff --git a/build/createLoader.js b/build/createLoader.js new file mode 100644 index 0000000..cd759a1 --- /dev/null +++ b/build/createLoader.js @@ -0,0 +1,19 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _actions = require('./actions'); + +exports['default'] = function (engine) { + return function (store) { + var dispatchLoad = function dispatchLoad(state) { + return store.dispatch((0, _actions.load)(state)); + }; + return engine.load().then(function (newState) { + dispatchLoad(newState); + return newState; + }); + }; +}; \ No newline at end of file diff --git a/build/createMiddleware.js b/build/createMiddleware.js new file mode 100644 index 0000000..8787f62 --- /dev/null +++ b/build/createMiddleware.js @@ -0,0 +1,124 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _lodash = require('lodash.isfunction'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _lodash3 = require('lodash.isobject'); + +var _lodash4 = _interopRequireDefault(_lodash3); + +var _actions = require('./actions'); + +var _constants = require('./constants'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +function swallow() {} + +function warnAboutConfusingFiltering(blacklist, whitelist) { + blacklist.filter(function (item) { + return whitelist.indexOf(item) !== -1; + }).forEach(function (item) { + console.warn( // eslint-disable-line no-console + '[redux-storage] Action ' + item + ' is on BOTH black- and whitelist.' + ' This is most likely a mistake!'); + }); +} + +function isValidAction(action) { + var isFunc = (0, _lodash2['default'])(action); + var isObj = (0, _lodash4['default'])(action); + var hasType = isObj && action.hasOwnProperty('type'); + + if (!isFunc && isObj && hasType) { + return true; + } + + if (process.env.NODE_ENV !== 'production') { + if (isFunc) { + console.warn( // eslint-disable-line no-console + '[redux-storage] ACTION IGNORED! Actions should be objects' + ' with a type property but received a function! Your' + ' function resolving middleware (e.g. redux-thunk) must be' + ' placed BEFORE redux-storage!'); + } else if (!isObj) { + console.warn( // eslint-disable-line no-console + '[redux-storage] ACTION IGNORED! Actions should be objects' + (' with a type property but received: ' + action)); + } else if (!hasType) { + console.warn( // eslint-disable-line no-console + '[redux-storage] ACTION IGNORED! Action objects should have' + ' a type property.'); + } + } + + return false; +} + +function handleWhitelist(action, actionWhitelist) { + if (Array.isArray(actionWhitelist)) { + return actionWhitelist.length === 0 ? true // Don't filter if the whitelist is empty + : actionWhitelist.indexOf(action.type) !== -1; + } + + // actionWhitelist is a function that returns true or false + return actionWhitelist(action); +} + +exports['default'] = function (engine) { + var actionBlacklist = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; + var actionWhitelist = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; + var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + var opts = Object.assign({ disableDispatchSaveAction: false }, options); + + // Also don't save if we process our own actions + var blacklistedActions = [].concat(_toConsumableArray(actionBlacklist), [_constants.LOAD, _constants.SAVE]); + + if (process.env.NODE_ENV !== 'production' && Array.isArray(actionWhitelist)) { + warnAboutConfusingFiltering(actionBlacklist, actionWhitelist); + } + + return function (_ref) { + var dispatch = _ref.dispatch, + getState = _ref.getState; + + return function (next) { + return function (action) { + var result = next(action); + + if (!isValidAction(action)) { + return result; + } + + var isOnBlacklist = blacklistedActions.indexOf(action.type) !== -1; + var isOnWhitelist = handleWhitelist(action, actionWhitelist); + + // Skip blacklisted actions + if (!isOnBlacklist && isOnWhitelist) { + var saveState = getState(); + var saveAction = (0, _actions.save)(saveState); + + if (process.env.NODE_ENV !== 'production') { + if (!saveAction.meta) { + saveAction.meta = {}; + } + saveAction.meta.origin = action; + } + + var dispatchSave = function dispatchSave() { + return dispatch(saveAction); + }; + engine.save(saveState).then(function () { + if (opts.disableDispatchSaveAction === false) { + return dispatchSave(); + } + })['catch'](swallow); + } + + return result; + }; + }; + }; +}; \ No newline at end of file diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..c96116f --- /dev/null +++ b/build/index.js @@ -0,0 +1,58 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _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; }; + +var _createLoader = require('./createLoader'); + +Object.defineProperty(exports, 'createLoader', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_createLoader)['default']; + } +}); + +var _createMiddleware = require('./createMiddleware'); + +Object.defineProperty(exports, 'createMiddleware', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_createMiddleware)['default']; + } +}); + +var _reducer = require('./reducer'); + +Object.defineProperty(exports, 'reducer', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_reducer)['default']; + } +}); + +var _constants = require('./constants'); + +Object.defineProperty(exports, 'LOAD', { + enumerable: true, + get: function get() { + return _constants.LOAD; + } +}); +Object.defineProperty(exports, 'SAVE', { + enumerable: true, + get: function get() { + return _constants.SAVE; + } +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +// The full default export is required to be BC with redux-storage <= v1.3.2 +exports['default'] = _extends({}, require('./constants'), { + createLoader: require('./createLoader')['default'], + createMiddleware: require('./createMiddleware')['default'], + reducer: require('./reducer')['default'] +}); \ No newline at end of file diff --git a/build/reducer.js b/build/reducer.js new file mode 100644 index 0000000..647028d --- /dev/null +++ b/build/reducer.js @@ -0,0 +1,21 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _reduxStorageMergerSimple = require('redux-storage-merger-simple'); + +var _reduxStorageMergerSimple2 = _interopRequireDefault(_reduxStorageMergerSimple); + +var _constants = require('./constants'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +exports['default'] = function (reducer) { + var merger = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _reduxStorageMergerSimple2['default']; + + return function (state, action) { + return reducer(action.type === _constants.LOAD ? merger(state, action.payload) : state, action); + }; +}; \ No newline at end of file diff --git a/package.json b/package.json index d8a0ef2..70d8b33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-storage", - "version": "4.1.1", + "version": "4.1.4", "description": "Persistence layer for redux with flexible backends", "main": "build/index.js", "jsnext:main": "build-es/index.js", @@ -9,9 +9,10 @@ }, "repository": { "type": "git", - "url": "https://github.com/michaelcontento/redux-storage.git" + "url": "https://github.com/react-stack/redux-storage.git" }, - "homepage": "https://github.com/michaelcontento/redux-storage", + "bugs": "https://github.com/react-stack/redux-storage/issues", + "homepage": "https://github.com/react-stack/redux-storage", "keywords": [ "redux", "redux-middleware", @@ -23,7 +24,11 @@ "data", "localstorage" ], - "author": "Michael Contento ", + "author": [ + "Michael Contento ", + "Guns ", + "Renan Bandeira " + ], "files": [ "build/", "build-es/", @@ -54,7 +59,7 @@ "redux-storage-merger-simple": "^1.0.2" }, "peerDependencies": { - "redux": "^3.0.0 || ^2.0.0 || ^1.0.0 || 1.0.0-alpha || 1.0.0-rc" + "redux": "^4.0.0 || ^3.0.0 || ^2.0.0 || ^1.0.0 || 1.0.0-alpha || 1.0.0-rc" }, "browserify": { "transform": [ diff --git a/src/createMiddleware.js b/src/createMiddleware.js index 01ed44d..b527920 100644 --- a/src/createMiddleware.js +++ b/src/createMiddleware.js @@ -62,7 +62,9 @@ function handleWhitelist(action, actionWhitelist) { return actionWhitelist(action); } -export default (engine, actionBlacklist = [], actionWhitelist = []) => { +export default (engine, actionBlacklist = [], actionWhitelist = [], options = {}) => { + const opts = Object.assign({ disableDispatchSaveAction: false }, options); + // Also don't save if we process our own actions const blacklistedActions = [...actionBlacklist, LOAD, SAVE]; @@ -94,7 +96,13 @@ export default (engine, actionBlacklist = [], actionWhitelist = []) => { } const dispatchSave = () => dispatch(saveAction); - engine.save(saveState).then(dispatchSave).catch(swallow); + engine.save(saveState) + .then(() => { + if (opts.disableDispatchSaveAction === false) { + return dispatchSave(); + } + }) + .catch(swallow); } return result;