├── .babelrc ├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── README.md ├── index.js ├── lib └── index.js ├── package.json ├── src └── index.js └── test ├── _action_types.js ├── reducers ├── index.js ├── todos.js └── todosobject.js ├── standalone.js ├── with-redux-object.js └── with-redux.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ] 6 | } 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | excludes: ['test', 'node_modules'] 3 | check: 4 | global: 5 | lines: 100 6 | branches: 100 7 | statements: 100 8 | functions: 100 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 5 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pouch-redux-middleware 2 | 3 | [![By](https://img.shields.io/badge/made%20by-yld!-32bbee.svg?style=flat)](http://yld.io/contact?source=github-pouch-redux-middleware) 4 | [![Build Status](https://secure.travis-ci.org/pgte/pouch-redux-middleware.svg?branch=master)](http://travis-ci.org/pgte/pouch-redux-middleware?branch=master) 5 | 6 | Redux Middleware for syncing state and a PouchDB database. 7 | 8 | Propagates changes made to a state into PouchDB. 9 | Propagates changes made to PouchDB into the state. 10 | 11 | ## Install 12 | 13 | ``` 14 | $ npm install pouch-redux-middleware --save 15 | ``` 16 | 17 | ## Overview 18 | 19 | pouch-redux-middleware will automatically populate a part of the store, specified by `path`, with the documents using the specified actions. This "sub-state" will be a list of the documents straight out of the database. When the database is modified by a 3rd party (e.g. by replication) a Redux action will be dispatched to update the "sub-state". Conversely, if you alter a document within the "sub-state", then the document will be updated in the database. 20 | 21 | * If a new document is created in the database (e.g. by replication or directly using `db.post`), then the corresponding `insert` action will be dispatched. If a document is updated in the database (e.g. by replication or directly), then the corresponding `update` action will be dispatched. If a document is deleted in the database (e.g. by replication or directly), then the corresponding `remove` action will be dispatched. 22 | * If you add a document to the "sub-state", then the document will be added to the database automatically (you should specify keys such as `_id`). If you alter a document in the "sub-state", the document will be updated in the database automatically. If you remove a document from the "sub-state", the document will be removed from the database automatically. 23 | * You may specify that that only a subset of the database's documents should populate the store by using `changeFilter` which effectively filters the documents under consideration. 24 | 25 | ## Example 26 | 27 | Example of configuring a store: 28 | 29 | ```js 30 | import * as types from '../constants/ActionTypes' 31 | import PouchMiddleware from 'pouch-redux-middleware' 32 | import { createStore, applyMiddleware } from 'redux' 33 | import rootReducer from '../reducers' 34 | import PouchDB from 'pouchdb' 35 | 36 | export default function configureStore() { 37 | const db = new PouchDB('todos'); 38 | 39 | const pouchMiddleware = PouchMiddleware({ 40 | path: '/todos', 41 | db, 42 | actions: { 43 | remove: doc => { return { type: types.DELETE_TODO, id: doc._id } }, 44 | insert: doc => { return { type: types.INSERT_TODO, todo: doc } }, 45 | batchInsert: docs => { return { type: types.BATCH_INSERT_TODOS, todos: docs } } 46 | update: doc => { return { type: types.UPDATE_TODO, todo: doc } }, 47 | } 48 | }) 49 | 50 | const store = createStore( 51 | rootReducer, 52 | undefined, 53 | applyMiddleware(pouchMiddleware) 54 | ) 55 | 56 | return store 57 | } 58 | ``` 59 | 60 | ## API 61 | 62 | ### PouchMiddleware(paths) 63 | 64 | * `paths`: path or array containing path specs 65 | 66 | A path spec is an object describing the behaviour of a sub-tree of the state it has the following attributes: 67 | 68 | * `path`: a JsonPath path where the documents will stored in the state as an array 69 | * `db`: a PouchDB database 70 | * `actions`: an object describing the actions to perform when initially inserting items and when a change occurs in the db. 71 | It's an object with keys containing a function that returns an action for each 72 | of the events (`remove`, `insert`, `batchInsert` and `update`) 73 | * `changeFilter`: a filtering function that receives a changed document, and if it returns 74 | false, the document will be ignored for the path. This is useful when you have 75 | multiple paths in a single database that are differentiated through an attribute 76 | (like `type`). 77 | * `handleResponse` a function that is invoked with the direct response of the database, 78 | which is useful when metadata is needed or errors need custom handling. 79 | Arguments are `error, data, callback`. `callback` must be invoked with a potential error 80 | after custom handling is done. 81 | * `initialBatchDispatched` a function that is invoked once the initial set of 82 | data has been read from pouchdb and dispatched to the redux store. 83 | This comes handy if you want skip the initial updates to a store 84 | subscriber by delaying the subscription to the redux store 85 | until the initial state is present. For example, when your application is first 86 | loaded you may wish to delay rendering until the store is updated. 87 | 88 | Example of a path spec: 89 | 90 | ```js 91 | { 92 | path: '/todos', 93 | db, 94 | actions: { 95 | remove: doc => { return { type: types.DELETE_TODO, id: doc._id } }, 96 | insert: doc => { return { type: types.INSERT_TODO, todo: doc } }, 97 | batchInsert: docs => { return { type: types.BATCH_INSERT_TODOS, todos: docs } } 98 | update: doc => { return { type: types.UPDATE_TODO, todo: doc } }, 99 | } 100 | } 101 | ``` 102 | 103 | ## License 104 | 105 | ISC 106 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jPath = require('json-path'); 4 | var Queue = require('async-function-queue'); 5 | var extend = require('xtend'); 6 | var equal = require('deep-equal'); 7 | 8 | module.exports = createPouchMiddleware; 9 | 10 | function createPouchMiddleware(_paths) { 11 | var paths = _paths || []; 12 | if (!Array.isArray(paths)) { 13 | paths = [paths]; 14 | } 15 | 16 | if (!paths.length) { 17 | throw new Error('PouchMiddleware: no paths'); 18 | } 19 | 20 | var defaultSpec = { 21 | path: '.', 22 | remove: scheduleRemove, 23 | insert: scheduleInsert, 24 | propagateDelete: propagateDelete, 25 | propagateUpdate: propagateUpdate, 26 | propagateInsert: propagateInsert, 27 | propagateBatchInsert: propagateBatchInsert, 28 | handleResponse: function handleResponse(err, data, cb) { 29 | cb(err); 30 | }, 31 | queue: Queue(1), 32 | docs: {}, 33 | actions: { 34 | remove: defaultAction('remove'), 35 | update: defaultAction('update'), 36 | insert: defaultAction('insert'), 37 | batchInsert: defaultAction('batchInsert') 38 | } 39 | }; 40 | 41 | paths = paths.map(function (path) { 42 | var spec = extend({}, defaultSpec, path); 43 | spec.actions = extend({}, defaultSpec.actions, spec.actions); 44 | spec.docs = {}; 45 | 46 | if (!spec.db) { 47 | throw new Error('path ' + path.path + ' needs a db'); 48 | } 49 | return spec; 50 | }); 51 | 52 | function listen(path, dispatch, initialBatchDispatched) { 53 | path.db.allDocs({ include_docs: true }).then(function (rawAllDocs) { 54 | var allDocs = rawAllDocs.rows.map(function (doc) { 55 | return doc.doc; 56 | }); 57 | var filteredAllDocs = allDocs; 58 | if (path.changeFilter) { 59 | filteredAllDocs = allDocs.filter(path.changeFilter); 60 | } 61 | allDocs.forEach(function (doc) { 62 | path.docs[doc._id] = doc; 63 | }); 64 | path.propagateBatchInsert(filteredAllDocs, dispatch); 65 | initialBatchDispatched(); 66 | var changes = path.db.changes({ 67 | live: true, 68 | include_docs: true, 69 | since: 'now' 70 | }); 71 | changes.on('change', function (change) { 72 | onDbChange(path, change, dispatch); 73 | }); 74 | }); 75 | } 76 | 77 | function processNewStateForPath(path, state) { 78 | var docs = jPath.resolve(state, path.path); 79 | 80 | /* istanbul ignore else */ 81 | if (docs && docs.length) { 82 | docs.forEach(function (docs) { 83 | var diffs = differences(path.docs, docs); 84 | diffs.new.concat(diffs.updated).forEach(function (doc) { 85 | return path.insert(doc); 86 | }); 87 | diffs.deleted.forEach(function (doc) { 88 | return path.remove(doc); 89 | }); 90 | }); 91 | } 92 | } 93 | 94 | function write(data, responseHandler) { 95 | return function (done) { 96 | data.db[data.type](data.doc, function (err, resp) { 97 | responseHandler(err, { 98 | response: resp, 99 | doc: data.doc, 100 | type: data.type 101 | }, function (err2) { 102 | done(err2, resp); 103 | }); 104 | }); 105 | }; 106 | } 107 | 108 | function scheduleInsert(doc) { 109 | this.docs[doc._id] = doc; 110 | this.queue.push(write({ 111 | type: 'put', 112 | doc: doc, 113 | db: this.db 114 | }, this.handleResponse)); 115 | } 116 | 117 | function scheduleRemove(doc) { 118 | delete this.docs[doc._id]; 119 | this.queue.push(write({ 120 | type: 'remove', 121 | doc: doc, 122 | db: this.db 123 | }, this.handleResponse)); 124 | } 125 | 126 | function propagateDelete(doc, dispatch) { 127 | dispatch(this.actions.remove(doc)); 128 | } 129 | 130 | function propagateInsert(doc, dispatch) { 131 | dispatch(this.actions.insert(doc)); 132 | } 133 | 134 | function propagateUpdate(doc, dispatch) { 135 | dispatch(this.actions.update(doc)); 136 | } 137 | 138 | function propagateBatchInsert(docs, dispatch) { 139 | dispatch(this.actions.batchInsert(docs)); 140 | } 141 | 142 | return function (options) { 143 | paths.forEach(function (path) { 144 | listen(path, options.dispatch, function (err) { 145 | if (path.initialBatchDispatched) { 146 | path.initialBatchDispatched(err); 147 | } 148 | }); 149 | }); 150 | 151 | return function (next) { 152 | return function (action) { 153 | var returnValue = next(action); 154 | var newState = options.getState(); 155 | 156 | paths.forEach(function (path) { 157 | return processNewStateForPath(path, newState); 158 | }); 159 | 160 | return returnValue; 161 | }; 162 | }; 163 | }; 164 | } 165 | 166 | function differences(oldDocs, newDocs) { 167 | var result = { 168 | new: [], 169 | updated: [], 170 | deleted: Object.keys(oldDocs).map(function (oldDocId) { 171 | return oldDocs[oldDocId]; 172 | }) 173 | }; 174 | 175 | var checkDoc = function checkDoc(newDoc) { 176 | var id = newDoc._id; 177 | 178 | /* istanbul ignore next */ 179 | if (!id) { 180 | warn('doc with no id'); 181 | } 182 | result.deleted = result.deleted.filter(function (doc) { 183 | return doc._id !== id; 184 | }); 185 | var oldDoc = oldDocs[id]; 186 | if (!oldDoc) { 187 | result.new.push(newDoc); 188 | } else if (!equal(oldDoc, newDoc)) { 189 | result.updated.push(newDoc); 190 | } 191 | }; 192 | 193 | if (Array.isArray(newDocs)) { 194 | newDocs.forEach(function (doc) { 195 | checkDoc(doc); 196 | }); 197 | } else { 198 | var keys = Object.keys(newDocs); 199 | for (var key in newDocs) { 200 | checkDoc(newDocs[key]); 201 | } 202 | } 203 | 204 | return result; 205 | } 206 | 207 | function onDbChange(path, change, dispatch) { 208 | var changeDoc = change.doc; 209 | 210 | if (path.changeFilter && !path.changeFilter(changeDoc)) { 211 | return; 212 | } 213 | 214 | if (changeDoc._deleted) { 215 | if (path.docs[changeDoc._id]) { 216 | delete path.docs[changeDoc._id]; 217 | path.propagateDelete(changeDoc, dispatch); 218 | } 219 | } else { 220 | var oldDoc = path.docs[changeDoc._id]; 221 | path.docs[changeDoc._id] = changeDoc; 222 | if (oldDoc) { 223 | path.propagateUpdate(changeDoc, dispatch); 224 | } else { 225 | path.propagateInsert(changeDoc, dispatch); 226 | } 227 | } 228 | } 229 | 230 | /* istanbul ignore next */ 231 | function warn(what) { 232 | var fn = console.warn || console.log; 233 | if (fn) { 234 | fn.call(console, what); 235 | } 236 | } 237 | 238 | /* istanbul ignore next */ 239 | function defaultAction(action) { 240 | return function () { 241 | throw new Error('no action provided for ' + action); 242 | }; 243 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pouch-redux-middleware", 3 | "version": "1.1.0", 4 | "description": "PouchDB Redux Middleware", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "node --harmony node_modules/istanbul/lib/cli.js cover -- lab -vl && istanbul check-coverage", 8 | "prepublish": "npm run build", 9 | "build": "babel ./src -d lib" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/pgte/pouch-redux-middleware.git" 14 | }, 15 | "keywords": [ 16 | "pouchdb", 17 | "redux", 18 | "react", 19 | "middleware" 20 | ], 21 | "author": "pgte", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/pgte/pouch-redux-middleware/issues" 25 | }, 26 | "homepage": "https://github.com/pgte/pouch-redux-middleware#readme", 27 | "dependencies": { 28 | "async-function-queue": "^1.0.0", 29 | "deep-equal": "^1.0.1", 30 | "json-path": "^0.1.3", 31 | "xtend": "^4.0.1" 32 | }, 33 | "devDependencies": { 34 | "async": "^2.1.4", 35 | "code": "^4.0.0", 36 | "istanbul": "^0.4.5", 37 | "lab": "^12.1.0", 38 | "memdown": "^1.2.4", 39 | "pouchdb": "^6.1.2", 40 | "pre-commit": "^1.2.2", 41 | "redux": "^3.6.0", 42 | "babel-cli": "^6.22.2", 43 | "babel-core": "^6.22.1", 44 | "babel-preset-es2015": "^6.22.0", 45 | "babel-preset-stage-0": "^6.22.0" 46 | }, 47 | "pre-commit": [ 48 | "test" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var jPath = require('json-path'); 2 | var Queue = require('async-function-queue'); 3 | var extend = require('xtend'); 4 | var equal = require('deep-equal'); 5 | 6 | module.exports = createPouchMiddleware; 7 | 8 | function createPouchMiddleware(_paths) { 9 | var paths = _paths || []; 10 | if (!Array.isArray(paths)) { 11 | paths = [paths]; 12 | } 13 | 14 | if (!paths.length) { 15 | throw new Error('PouchMiddleware: no paths'); 16 | } 17 | 18 | var defaultSpec = { 19 | path: '.', 20 | remove: scheduleRemove, 21 | insert: scheduleInsert, 22 | propagateDelete, 23 | propagateUpdate, 24 | propagateInsert, 25 | propagateBatchInsert, 26 | handleResponse: function(err, data, cb) { cb(err); }, 27 | queue: Queue(1), 28 | docs: {}, 29 | actions: { 30 | remove: defaultAction('remove'), 31 | update: defaultAction('update'), 32 | insert: defaultAction('insert'), 33 | batchInsert: defaultAction('batchInsert'), 34 | } 35 | } 36 | 37 | paths = paths.map(function(path) { 38 | var spec = extend({}, defaultSpec, path); 39 | spec.actions = extend({}, defaultSpec.actions, spec.actions); 40 | spec.docs = {}; 41 | 42 | if (! spec.db) { 43 | throw new Error('path ' + path.path + ' needs a db'); 44 | } 45 | return spec; 46 | }); 47 | 48 | function listen(path, dispatch, initialBatchDispatched) { 49 | path.db.allDocs({ include_docs: true }).then((rawAllDocs) => { 50 | var allDocs = rawAllDocs.rows.map((doc) => doc.doc); 51 | var filteredAllDocs = allDocs; 52 | if (path.changeFilter) { 53 | filteredAllDocs = allDocs.filter(path.changeFilter); 54 | } 55 | allDocs.forEach((doc) => { 56 | path.docs[doc._id] = doc; 57 | }); 58 | path.propagateBatchInsert(filteredAllDocs, dispatch); 59 | initialBatchDispatched(); 60 | var changes = path.db.changes({ 61 | live: true, 62 | include_docs: true, 63 | since: 'now', 64 | }); 65 | changes.on('change', change => { 66 | onDbChange(path, change, dispatch); 67 | }); 68 | }); 69 | } 70 | 71 | function processNewStateForPath(path, state) { 72 | var docs = jPath.resolve(state, path.path); 73 | 74 | /* istanbul ignore else */ 75 | if (docs && docs.length) { 76 | docs.forEach(function(docs) { 77 | var diffs = differences(path.docs, docs); 78 | diffs.new.concat(diffs.updated).forEach(doc => path.insert(doc)) 79 | diffs.deleted.forEach(doc => path.remove(doc)); 80 | }); 81 | } 82 | } 83 | 84 | function write(data, responseHandler) { 85 | return function(done) { 86 | data.db[data.type](data.doc, function(err, resp) { 87 | responseHandler( 88 | err, 89 | { 90 | response: resp, 91 | doc: data.doc, 92 | type: data.type 93 | }, 94 | function(err2) { 95 | done(err2, resp); 96 | } 97 | ); 98 | }); 99 | }; 100 | } 101 | 102 | function scheduleInsert(doc) { 103 | this.docs[doc._id] = doc; 104 | this.queue.push(write( 105 | { 106 | type: 'put', 107 | doc: doc, 108 | db: this.db 109 | }, 110 | this.handleResponse 111 | )); 112 | } 113 | 114 | function scheduleRemove(doc) { 115 | delete this.docs[doc._id]; 116 | this.queue.push(write( 117 | { 118 | type: 'remove', 119 | doc: doc, 120 | db: this.db 121 | }, 122 | this.handleResponse 123 | )); 124 | } 125 | 126 | function propagateDelete(doc, dispatch) { 127 | dispatch(this.actions.remove(doc)); 128 | } 129 | 130 | function propagateInsert(doc, dispatch) { 131 | dispatch(this.actions.insert(doc)); 132 | } 133 | 134 | function propagateUpdate(doc, dispatch) { 135 | dispatch(this.actions.update(doc)); 136 | } 137 | 138 | function propagateBatchInsert(docs, dispatch) { 139 | dispatch(this.actions.batchInsert(docs)); 140 | } 141 | 142 | return function(options) { 143 | paths.forEach((path) => { 144 | listen(path, options.dispatch, (err) => { 145 | if (path.initialBatchDispatched) { 146 | path.initialBatchDispatched(err); 147 | } 148 | }); 149 | }); 150 | 151 | return function(next) { 152 | return function(action) { 153 | var returnValue = next(action); 154 | var newState = options.getState(); 155 | 156 | paths.forEach(path => processNewStateForPath(path, newState)); 157 | 158 | return returnValue; 159 | } 160 | } 161 | } 162 | } 163 | 164 | function differences(oldDocs, newDocs) { 165 | var result = { 166 | new: [], 167 | updated: [], 168 | deleted: Object.keys(oldDocs).map(oldDocId => oldDocs[oldDocId]), 169 | }; 170 | 171 | var checkDoc = function(newDoc) { 172 | var id = newDoc._id; 173 | 174 | /* istanbul ignore next */ 175 | if (! id) { 176 | warn('doc with no id'); 177 | } 178 | result.deleted = result.deleted.filter(doc => doc._id !== id); 179 | var oldDoc = oldDocs[id]; 180 | if (! oldDoc) { 181 | result.new.push(newDoc); 182 | } else if (!equal(oldDoc, newDoc)) { 183 | result.updated.push(newDoc); 184 | } 185 | }; 186 | 187 | if (Array.isArray(newDocs)){ 188 | newDocs.forEach(function (doc) { 189 | checkDoc(doc) 190 | }); 191 | } else{ 192 | var keys = Object.keys(newDocs); 193 | for (var key in newDocs){ 194 | checkDoc(newDocs[key]) 195 | } 196 | } 197 | 198 | 199 | return result; 200 | } 201 | 202 | function onDbChange(path, change, dispatch) { 203 | var changeDoc = change.doc; 204 | 205 | if(path.changeFilter && (! path.changeFilter(changeDoc))) { 206 | return; 207 | } 208 | 209 | if (changeDoc._deleted) { 210 | if (path.docs[changeDoc._id]) { 211 | delete path.docs[changeDoc._id]; 212 | path.propagateDelete(changeDoc, dispatch); 213 | } 214 | } else { 215 | var oldDoc = path.docs[changeDoc._id]; 216 | path.docs[changeDoc._id] = changeDoc; 217 | if (oldDoc) { 218 | path.propagateUpdate(changeDoc, dispatch); 219 | } else { 220 | path.propagateInsert(changeDoc, dispatch); 221 | } 222 | } 223 | } 224 | 225 | /* istanbul ignore next */ 226 | function warn(what) { 227 | var fn = console.warn || console.log; 228 | if (fn) { 229 | fn.call(console, what); 230 | } 231 | } 232 | 233 | /* istanbul ignore next */ 234 | function defaultAction(action) { 235 | return function() { 236 | throw new Error('no action provided for ' + action); 237 | }; 238 | } 239 | -------------------------------------------------------------------------------- /test/_action_types.js: -------------------------------------------------------------------------------- 1 | [ 2 | 'ERROR', 3 | 'ADD_TODO', 4 | 'INSERT_TODO', 5 | 'BATCH_INSERT_TODOS', 6 | 'DELETE_TODO', 7 | 'EDIT_TODO', 8 | 'UPDATE_TODO', 9 | 'COMPLETE_TODO', 10 | 'COMPLETE_ALL', 11 | 'CLEAR_COMPLETED' 12 | ].forEach(function(type) { 13 | exports[type] = type; 14 | }); 15 | -------------------------------------------------------------------------------- /test/reducers/index.js: -------------------------------------------------------------------------------- 1 | var redux = require('redux'); 2 | var todos = require('./todos'); 3 | var todosobject = require('./todosobject'); 4 | 5 | module.exports = redux.combineReducers({ 6 | todos: todos, 7 | todosobject: todosobject 8 | }); 9 | -------------------------------------------------------------------------------- /test/reducers/todos.js: -------------------------------------------------------------------------------- 1 | var actionTypes = require('../_action_types'); 2 | 3 | const initialState = [] 4 | 5 | module.exports = function todos(state, action) { 6 | if (! state) { 7 | state = []; 8 | } 9 | 10 | switch (action.type) { 11 | case actionTypes.ADD_TODO: 12 | return [ 13 | { 14 | _id: action.id || id(), 15 | completed: false, 16 | text: action.text 17 | }, 18 | ...state 19 | ] 20 | 21 | case actionTypes.INSERT_TODO: 22 | return [ 23 | action.todo, 24 | ...state 25 | ] 26 | case actionTypes.BATCH_INSERT_TODOS: 27 | return [...state, ...action.todos] 28 | case actionTypes.DELETE_TODO: 29 | return state.filter(todo => 30 | todo._id !== action.id 31 | ) 32 | 33 | case actionTypes.EDIT_TODO: 34 | return state.map(todo => 35 | todo._id === action.id ? 36 | Object.assign({}, todo, { text: action.text }) : 37 | todo 38 | ) 39 | 40 | case actionTypes.UPDATE_TODO: 41 | return state.map(todo => 42 | todo._id === action.todo._id ? 43 | action.todo : 44 | todo 45 | ) 46 | 47 | case actionTypes.COMPLETE_TODO: 48 | return state.map(todo => 49 | todo._id === action.id ? 50 | Object.assign({}, todo, { completed: !todo.completed }) : 51 | todo 52 | ) 53 | 54 | case actionTypes.COMPLETE_ALL: 55 | const areAllMarked = state.every(todo => todo.completed) 56 | return state.map(todo => Object.assign({}, todo, { 57 | completed: !areAllMarked 58 | })) 59 | 60 | case actionTypes.CLEAR_COMPLETED: 61 | return state.filter(todo => todo.completed === false) 62 | 63 | default: 64 | return state 65 | } 66 | } 67 | 68 | function id() { 69 | return Math.random().toString(36).substring(7); 70 | } 71 | -------------------------------------------------------------------------------- /test/reducers/todosobject.js: -------------------------------------------------------------------------------- 1 | var actionTypes = require('../_action_types'); 2 | 3 | const initialState = [] 4 | 5 | module.exports = function todosobject(state, action) { 6 | if (! state) { 7 | state = {}; 8 | } 9 | 10 | switch (action.type) { 11 | case actionTypes.ADD_TODO: 12 | { 13 | var todo = { 14 | _id: action.id || id(), 15 | completed: false, 16 | text: action.text 17 | }; 18 | return Object.assign(state, {[todo._id]: todo}); 19 | } 20 | case actionTypes.INSERT_TODO: 21 | return Object.assign(state, { [action.todo._id]: action.todo }); 22 | case actionTypes.DELETE_TODO: 23 | var newState = Object.assign({}, state); 24 | delete newState[action.id]; 25 | return newState; 26 | 27 | case actionTypes.UPDATE_TODO: 28 | return Object.assign(state, { [action.todo._id]: action.todo }); 29 | 30 | default: 31 | return state 32 | } 33 | } 34 | 35 | function id() { 36 | return Math.random().toString(36).substring(7); 37 | } 38 | -------------------------------------------------------------------------------- /test/standalone.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var lab = exports.lab = Lab.script(); 3 | var describe = lab.experiment; 4 | var before = lab.before; 5 | var after = lab.after; 6 | var it = lab.it; 7 | var Code = require('code'); 8 | var expect = Code.expect; 9 | 10 | var PouchMiddleware = require('../src/'); 11 | 12 | var PouchDB = require('pouchdb'); 13 | var db = new PouchDB('todos', { 14 | db: require('memdown'), 15 | }); 16 | 17 | describe('Pouch Redux Middleware', function() { 18 | var pouchMiddleware; 19 | var store; 20 | 21 | it('cannot be created with no paths', function(done) { 22 | expect(function() { 23 | PouchMiddleware(); 24 | }).to.throw('PouchMiddleware: no paths'); 25 | done(); 26 | }); 27 | 28 | it('requires db in path', function(done) { 29 | expect(function() { 30 | PouchMiddleware([{}]); 31 | }).to.throw('path undefined needs a db'); 32 | done(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/with-redux-object.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var lab = exports.lab = Lab.script(); 3 | var describe = lab.experiment; 4 | var before = lab.before; 5 | var after = lab.after; 6 | var it = lab.it; 7 | var Code = require('code'); 8 | var expect = Code.expect; 9 | 10 | var actionTypes = require('./_action_types'); 11 | var rootReducer = require('./reducers'); 12 | 13 | var timers = require('timers'); 14 | var async = require('async'); 15 | var PouchDB = require('pouchdb'); 16 | var db = new PouchDB('todosobject', { 17 | db: require('memdown'), 18 | }); 19 | 20 | var redux = require('redux'); 21 | 22 | var PouchMiddleware = require('../src/'); 23 | 24 | describe('Pouch Redux Middleware with Objects', function() { 25 | var pouchMiddleware; 26 | var store; 27 | 28 | it('todosmaps can be created', function(done) { 29 | 30 | pouchMiddleware = PouchMiddleware({ 31 | path: '/todosobject', 32 | db: db, 33 | actions: { 34 | remove: (doc) => { return {type: actionTypes.DELETE_TODO, id: doc._id} }, 35 | insert: (doc) => { return {type: actionTypes.INSERT_TODO, todo: doc} }, 36 | batchInsert: (docs) => { return {type: actionTypes.BATCH_INSERT_TODOS, todos: docs} }, 37 | update: (doc) => { return {type: actionTypes.UPDATE_TODO, todo: doc} } 38 | }, 39 | changeFilter: doc => !doc.filter 40 | }); 41 | done(); 42 | }); 43 | 44 | it('can be used to create a store', function(done) { 45 | var createStoreWithMiddleware = redux.applyMiddleware(pouchMiddleware)(redux.createStore); 46 | store = createStoreWithMiddleware(rootReducer); 47 | done(); 48 | }); 49 | 50 | it('accepts a few inserts', function(done) { 51 | store.dispatch({type: actionTypes.ADD_TODO, text: 'do laundry', id: 'a'}); 52 | store.dispatch({type: actionTypes.ADD_TODO, text: 'wash dishes', id: 'b'}); 53 | timers.setTimeout(done, 100); 54 | }); 55 | 56 | it('saves changes in pouchdb', function(done) { 57 | async.map(['a', 'b'], db.get.bind(db), function(err, results) { 58 | if (err) return done(err); 59 | expect(results.length).to.equal(2); 60 | expect(results[0].text).to.equal('do laundry'); 61 | expect(results[1].text).to.equal('wash dishes'); 62 | done(); 63 | }); 64 | }); 65 | 66 | it('accepts an removal', function(done) { 67 | store.dispatch({type: actionTypes.DELETE_TODO, id: 'a'}); 68 | timers.setTimeout(done, 100); 69 | }); 70 | 71 | it('saves changes in pouchdb', function(done) { 72 | db.get('a', function(err) { 73 | expect(err).to.be.an.object(); 74 | expect(err.message).to.equal('missing'); 75 | done(); 76 | }); 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /test/with-redux.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var lab = exports.lab = Lab.script(); 3 | var describe = lab.experiment; 4 | var before = lab.before; 5 | var after = lab.after; 6 | var it = lab.it; 7 | var Code = require('code'); 8 | var expect = Code.expect; 9 | 10 | var actionTypes = require('./_action_types'); 11 | var rootReducer = require('./reducers'); 12 | 13 | var timers = require('timers'); 14 | var async = require('async'); 15 | var PouchDB = require('pouchdb'); 16 | var db = new PouchDB('todos', { 17 | db: require('memdown'), 18 | }); 19 | 20 | var redux = require('redux'); 21 | 22 | var PouchMiddleware = require('../src/'); 23 | 24 | describe('Pouch Redux Middleware', function() { 25 | var pouchMiddleware; 26 | var store; 27 | 28 | it('can be created', function(done) { 29 | 30 | pouchMiddleware = PouchMiddleware({ 31 | path: '/todos', 32 | db: db, 33 | actions: { 34 | remove: (doc) => { return {type: actionTypes.DELETE_TODO, id: doc._id} }, 35 | insert: (doc) => { return {type: actionTypes.INSERT_TODO, todo: doc} }, 36 | batchInsert: (docs) => { return {type: actionTypes.BATCH_INSERT_TODOS, todos: docs} }, 37 | update: (doc) => { return {type: actionTypes.UPDATE_TODO, todo: doc} } 38 | }, 39 | changeFilter: doc => !doc.filter 40 | }); 41 | done(); 42 | }); 43 | 44 | it('can be used to create a store', function(done) { 45 | var createStoreWithMiddleware = redux.applyMiddleware(pouchMiddleware)(redux.createStore); 46 | store = createStoreWithMiddleware(rootReducer); 47 | done(); 48 | }); 49 | 50 | it('accepts a few inserts', function(done) { 51 | store.dispatch({type: actionTypes.ADD_TODO, text: 'do laundry', id: 'a'}); 52 | store.dispatch({type: actionTypes.ADD_TODO, text: 'wash dishes', id: 'b'}); 53 | timers.setTimeout(done, 100); 54 | }); 55 | 56 | it('saves changes in pouchdb', function(done) { 57 | async.map(['a', 'b'], db.get.bind(db), function(err, results) { 58 | if (err) return done(err); 59 | expect(results.length).to.equal(2); 60 | expect(results[0].text).to.equal('do laundry'); 61 | expect(results[1].text).to.equal('wash dishes'); 62 | done(); 63 | }); 64 | }); 65 | 66 | it('accepts an edit', function(done) { 67 | store.dispatch({type: actionTypes.EDIT_TODO, text: 'wash all the dishes', id: 'b'}); 68 | timers.setTimeout(done, 100); 69 | }); 70 | 71 | it('saves changes in pouchdb', function(done) { 72 | async.map(['a', 'b'], db.get.bind(db), function(err, results) { 73 | if (err) return done(err); 74 | expect(results.length).to.equal(2); 75 | expect(results[0].text).to.equal('do laundry'); 76 | expect(results[1].text).to.equal('wash all the dishes'); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('accepts an removal', function(done) { 82 | store.dispatch({type: actionTypes.DELETE_TODO, id: 'a'}); 83 | timers.setTimeout(done, 100); 84 | }); 85 | 86 | it('saves changes in pouchdb', function(done) { 87 | db.get('a', function(err) { 88 | expect(err).to.be.an.object(); 89 | expect(err.message).to.equal('missing'); 90 | done(); 91 | }); 92 | }); 93 | 94 | it('making changes in pouchdb...', function(done) { 95 | db.get('b', function(err, doc) { 96 | expect(err).to.equal(null); 97 | doc.text = 'wash some of the dishes'; 98 | db.put(doc, done); 99 | }); 100 | }); 101 | 102 | it('waiting a bit', function(done) { 103 | timers.setTimeout(done, 100); 104 | }); 105 | 106 | it('...propagates update from pouchdb', function(done) { 107 | expect(store.getState().todos.filter(function(doc) { 108 | return doc._id == 'b'; 109 | })[0].text).to.equal('wash some of the dishes'); 110 | done(); 111 | }); 112 | 113 | it('making removal in pouchdb...', function(done) { 114 | db.get('b', function(err, doc) { 115 | expect(err).to.equal(null); 116 | db.remove(doc, done); 117 | }); 118 | }); 119 | 120 | it('waiting a bit', function(done) { 121 | timers.setTimeout(done, 100); 122 | }); 123 | 124 | it('...propagates update from pouchdb', function(done) { 125 | expect(store.getState().todos.filter(function(doc) { 126 | return doc._id == 'b'; 127 | }).length).to.equal(0); 128 | done(); 129 | }); 130 | 131 | it('making insert in pouchdb...', function(done) { 132 | db.post({ 133 | _id: 'c', 134 | text: 'pay bills', 135 | }, done); 136 | }); 137 | 138 | it('waiting a bit', function(done) { 139 | timers.setTimeout(done, 100); 140 | }); 141 | 142 | it('...propagates update from pouchdb', function(done) { 143 | expect(store.getState().todos.filter(function(doc) { 144 | return doc._id == 'c'; 145 | })[0].text).to.equal('pay bills'); 146 | done(); 147 | }); 148 | 149 | it('...inserts filtered document', function(done) { 150 | db.post({ 151 | _id: 'd', 152 | filter: true, 153 | }).then(() => done()).catch(done); 154 | }); 155 | 156 | it('waiting a bit', function(done) { 157 | timers.setTimeout(done, 100); 158 | }); 159 | 160 | it('...filters documents', function(done) { 161 | expect(store.getState().todos.filter(function(doc) { 162 | return doc._id == 'd'; 163 | }).length).to.equal(0); 164 | done(); 165 | }); 166 | 167 | it('calles initialBatchDispatched', (done) => { 168 | const anotherMiddleware = PouchMiddleware({ 169 | path: '/todos', 170 | db: db, 171 | actions: { 172 | remove: (doc) => { return {type: actionTypes.DELETE_TODO, id: doc._id} }, 173 | insert: (doc) => { return {type: actionTypes.INSERT_TODO, todo: doc} }, 174 | batchInsert: (docs) => { return {type: actionTypes.BATCH_INSERT_TODOS, todos: docs} }, 175 | update: (doc) => { return {type: actionTypes.UPDATE_TODO, todo: doc} } 176 | }, 177 | initialBatchDispatched(err) { 178 | if (err) { 179 | return done(err); 180 | } 181 | 182 | var called = false; 183 | store.subscribe(() => { 184 | if (called) { 185 | done(new Error('expect subscribe to only be called once')); 186 | } 187 | called = true; 188 | expect(store.getState().todos.length).to.equal(1); 189 | timers.setTimeout(done, 100); 190 | }); 191 | 192 | expect(store.getState().todos.length).to.equal(2); 193 | store.dispatch({type: actionTypes.DELETE_TODO, id: 'c'}); 194 | } 195 | }); 196 | const store = redux.applyMiddleware(anotherMiddleware)(redux.createStore)(rootReducer); 197 | expect(store.getState().todos.length).to.equal(0); 198 | }); 199 | }); 200 | --------------------------------------------------------------------------------