├── config ├── shelfdb.config.json └── store.config.json ├── lib ├── core │ ├── event-listener.js │ ├── validations.js │ ├── exception-handler.js │ └── item.js ├── adapter │ ├── pouch-sync.js │ ├── pouch-server.js │ ├── pouch-events.js │ ├── pouch-store.js │ └── pouch.js ├── store-loader.js └── store.js ├── test ├── setup.js ├── utils │ ├── setup.js │ └── create-suite.js ├── fixtures │ └── sample-data.json ├── sync.test.js ├── server.test.js ├── event.test.js ├── find.test.js ├── validation.test.js ├── schema.test.js ├── remove.test.js ├── store.test.js └── relations.test.js ├── testem.json ├── index.js ├── package.json └── README.md /config/shelfdb.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "root": "/shelfdb" 4 | } 5 | } -------------------------------------------------------------------------------- /config/store.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbLocation": ".db/", 3 | 4 | "preferredAdapters": [ 5 | "redisdown", 6 | "riakdown", 7 | "mongodown", 8 | "sqldown", 9 | "mysqldown", 10 | "memdown" 11 | ] 12 | } -------------------------------------------------------------------------------- /lib/core/event-listener.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | function EventListener (pointer, event, callback) { 5 | this.pointer = pointer; 6 | this.event = event; 7 | this.callback = callback; 8 | } 9 | 10 | EventListener.prototype.off = function () { 11 | this.pointer.cancel(); 12 | }; 13 | 14 | module.exports = EventListener; -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | // Export modules to global scope as necessary (only for testing) 2 | if (typeof process !== 'undefined' && process.title === 'node') { 3 | // We are in node. Require modules. 4 | expect = require('chai').expect; 5 | isBrowser = false; 6 | } else { 7 | // We are in the browser. Set up variables like above using served js files. 8 | expect = chai.expect; 9 | // num and sinon already exported globally in the browser. 10 | isBrowser = true; 11 | } -------------------------------------------------------------------------------- /test/utils/setup.js: -------------------------------------------------------------------------------- 1 | // Export modules to global scope as necessary (only for testing) 2 | if (typeof process !== 'undefined' && process.title === 'node') { 3 | // We are in node. Require modules. 4 | expect = require('chai').expect; 5 | isBrowser = false; 6 | } else { 7 | // We are in the browser. Set up variables like above using served js files. 8 | expect = chai.expect; 9 | // num and sinon already exported globally in the browser. 10 | isBrowser = true; 11 | } -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "mocha", 3 | "src_files": [ 4 | "node_modules/chai/chai.js", 5 | "lib/**/*.js", 6 | "test/utils/setup.js", 7 | "test/*.test.js" 8 | ], 9 | "launchers": { 10 | "node": { 11 | "command": "npm run mocha", 12 | "protocol": "tap" 13 | } 14 | }, 15 | "launch_in_dev": [ 16 | "node", "chrome" 17 | ], 18 | "serve_files": [ "tmp/tests.js" ], 19 | "before_tests": "npm run create-tmp && npm run create-suite && npm run browserify", 20 | "on_exit": "npm run clean" 21 | } 22 | -------------------------------------------------------------------------------- /lib/adapter/pouch-sync.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | var _ = require('lodash'); 5 | var PouchDb = require('pouchdb'); 6 | 7 | function PouchSync () {} 8 | 9 | PouchSync.sync = function (store, syncStore, options) { 10 | 11 | if (!syncStore) { 12 | return; 13 | } 14 | 15 | _.merge({ 16 | live: true, 17 | retry: true 18 | }, options); 19 | 20 | if (!_.isString(syncStore) && syncStore.toString() === '[object Store]') { 21 | syncStore = syncStore._adapter.pouch; 22 | } 23 | 24 | return PouchDb.sync(store._adapter.pouch, syncStore, options); 25 | }; 26 | 27 | 28 | module.exports = PouchSync; 29 | -------------------------------------------------------------------------------- /test/utils/create-suite.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | var _ = require('lodash'); 6 | 7 | function collectTests (path) { 8 | var tests; 9 | 10 | var filenames = fs.readdirSync(path); 11 | 12 | return _.filter(filenames, function (filename) { 13 | return /\.test\.js$/.test(filename); 14 | }); 15 | } 16 | 17 | function createSuite (tests) { 18 | return _.map(tests, function (filename) { 19 | return 'require("../test/' + filename + '");'; 20 | }).join('\n'); 21 | } 22 | 23 | function writeSuite (target, source) { 24 | return fs.writeFileSync(target, source); 25 | } 26 | 27 | var filenames = collectTests(__dirname + '/../'); 28 | var source = createSuite(filenames); 29 | 30 | writeSuite(__dirname + '/../../tmp/suite.js', source); 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | var PouchDbStore = require('./lib/store'); 5 | var StoreLoader = require('./lib/store-loader'); 6 | 7 | module.exports = { 8 | // 9 | // Plugin loader for PouchDbStore. 10 | // 11 | // Usage: 12 | // var PouchDb = require('pouchdb'); 13 | // PouchDb.plugin(require('pouchdb-store')); 14 | // var MyStore = new PouchDb('myStore').store(); 15 | // ... 16 | // 17 | store: function (options) { 18 | return PouchDbStore.load(this, options); 19 | }, 20 | 21 | // 22 | // Standalone loader for PouchDbStore. 23 | // 24 | // Usage: 25 | // var PouchDbStore = require('pouchdb-store'); 26 | // var MyStore = PouchDbStore.open('myStore'); 27 | // ... 28 | // 29 | open: function (dbName, options) { 30 | return StoreLoader.load(dbName, options); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /lib/store-loader.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var isNode = require('detect-node'); 7 | 8 | var PouchDb = require('pouchdb'); 9 | var PouchDbStore = require('./store'); 10 | 11 | PouchDb.plugin(PouchDbStore); 12 | 13 | function PouchDbStoreLoader () {} 14 | 15 | PouchDbStoreLoader.prototype.load = function (dbName, options) { 16 | 17 | options = _.extend({}, options); 18 | 19 | var stores = {}; 20 | 21 | if (!stores[dbName]) { 22 | var pouch = new PouchDb(dbName, options); 23 | var store = PouchDbStore.load(pouch, options); 24 | 25 | stores[dbName] = store; 26 | } 27 | 28 | return stores[dbName]; 29 | }; 30 | 31 | PouchDbStoreLoader.prototype._extract = function () { 32 | var options = _.first(arguments); 33 | 34 | var result = _.pick.apply(options, arguments); 35 | options = _.omit(options, arguments); 36 | 37 | return result; 38 | }; 39 | 40 | module.exports = new PouchDbStoreLoader(); 41 | -------------------------------------------------------------------------------- /lib/adapter/pouch-server.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | var _ = require('lodash'); 5 | var isNode = require('detect-node'); 6 | 7 | function PouchServer () {} 8 | 9 | PouchServer.listen = function (store, app, serverOptions) { 10 | 11 | if (!app) { 12 | return; 13 | } 14 | 15 | serverOptions = _.merge({ 16 | root: '/', 17 | configPath: './.db/config.json' 18 | }, serverOptions); 19 | 20 | app.use(serverOptions.root, PouchServer._express(store, serverOptions)); 21 | }; 22 | 23 | 24 | PouchServer._express = function (store, serverOptions) { 25 | if (isNode) { 26 | 27 | // Make sure temporary files created by pouch express are 28 | // stored into the right folder 29 | var configPath = serverOptions.configPath.replace(/\/[^\/]*$/, '/'); 30 | 31 | var mkdirp = require('mkdirp'); 32 | mkdirp.sync(configPath.replace()); 33 | 34 | var PouchDb = require('pouchdb').defaults(store._adapter.pouch.__opts); 35 | 36 | PouchServer.__express = PouchServer.__express || require('express-pouchdb')(PouchDb, serverOptions); 37 | return PouchServer.__express; 38 | } 39 | return null; 40 | }; 41 | 42 | module.exports = PouchServer; 43 | -------------------------------------------------------------------------------- /test/fixtures/sample-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [{ 3 | "count": 1, 4 | "value": "test-1", 5 | "shared": "shared", 6 | "nested": { 7 | "shared": "nested-shared", 8 | "value": "test-nested-1", 9 | "deep": { 10 | "shared": "nested-deep-shared", 11 | "value": "test-nested-deep-1" 12 | } 13 | } 14 | }, { 15 | "count": 2, 16 | "value": "test-2", 17 | "shared": "shared", 18 | "nested": { 19 | "shared": "nested-shared", 20 | "value": "test-nested-2", 21 | "deep": { 22 | "shared": "nested-deep-shared", 23 | "value": "test-nested-deep-2" 24 | } 25 | } 26 | }, { 27 | "count": 3, 28 | "value": "test-3", 29 | "shared": "shared", 30 | "nested": { 31 | "shared": "nested-shared", 32 | "value": "test-nested-3" 33 | } 34 | }, { 35 | "count": 4, 36 | "value": "test-4", 37 | "shared": "shared" 38 | }, { 39 | "count": 5, 40 | "value": "test-5", 41 | "shared": "unique", 42 | "nested": { 43 | "shared": "shared-nested-unique", 44 | "unique-value": "test-nested-unique", 45 | "deep": { 46 | "shared": "shared-nested-deep-unique", 47 | "unique-value": "test-nested-deep-unique" 48 | } 49 | } 50 | }] 51 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "store.pouchdb", 3 | "version": "0.1.12", 4 | "description": "ORM-style storage plugin for the PouchDb database", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/chunksnbits/store.pouchdb.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/chunksnbits/store.pouch/issues" 12 | }, 13 | "scripts": { 14 | "browserify": "./node_modules/.bin/browserify tmp/suite.js -o tmp/tests.js --ignore-missing", 15 | "clean": "rm -rf tmp", 16 | "create-suite": "node ./test/utils/create-suite.js", 17 | "create-tmp": "mkdir -p tmp", 18 | "mocha": "./node_modules/.bin/mocha -r test/utils/setup.js -R tap test/*.test.js", 19 | "test": "./node_modules/.bin/testem" 20 | }, 21 | "keywords": [ 22 | "pouchdb", 23 | "pouch", 24 | "store", 25 | "save", 26 | "update", 27 | "delete", 28 | "model", 29 | "crud", 30 | "local-storage", 31 | "offline-first", 32 | "node", 33 | "orm", 34 | "db", 35 | "mongodb" 36 | ], 37 | "author": "daniel.eissing@gmx.net", 38 | "browser": { 39 | "mkdirp": false, 40 | "express-pouchdb": false 41 | }, 42 | "licenses": [ 43 | { 44 | "type": "Apache License 2.0", 45 | "url": "http://opensource.org/licenses/MIT" 46 | } 47 | ], 48 | "dependencies": { 49 | "bluebird": "^2.9.16", 50 | "deep-equal": "^1.0.0", 51 | "detect-node": "^2.0.3", 52 | "express-pouchdb": "^0.14.0", 53 | "lodash": "^3.0.0", 54 | "mkdirp": "^0.5.0", 55 | "pouchdb": "^3.3.1" 56 | }, 57 | "devDependencies": { 58 | "browserify": "^8.1.3", 59 | "chai": "^1.10.0", 60 | "fs.extra": "^1.3.2", 61 | "leveldown": "^0.10.4", 62 | "memdown": "^1.0.0", 63 | "mocha": "^2.1.0", 64 | "mocha-qa": "^0.9.3", 65 | "testem": "^0.6.39" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/adapter/pouch-events.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | var _ = require('lodash'); 5 | 6 | var ExceptionHandler = require('../core/exception-handler'); 7 | var EventListener = require('../core/event-listener'); 8 | 9 | function EventHandler (store) { 10 | 11 | this.store = store; 12 | 13 | this.listeners = { 14 | create: {}, 15 | update: {}, 16 | delete: {}, 17 | change: {}, 18 | complete: {}, 19 | error: {} 20 | }; 21 | 22 | this.supportedEvents = _.keys(this.listeners); 23 | } 24 | 25 | EventHandler.prototype.register = function (event, callback) { 26 | var registration = this.store.pouch.changes({ 27 | since: 'now', 28 | live: true 29 | }).on(event, callback); 30 | 31 | var listener = new EventListener(registration, event, callback); 32 | 33 | if (!_.contains(this.supportedEvents, event)) { 34 | return ExceptionHandler.create( 35 | 'ShelfIllegalEventException', 36 | 'The event you want to subscribe to is not supported. Supported events are: ' + this.supportedEvents.join(',') 37 | ); 38 | } 39 | 40 | this.listeners[event][callback] = listener; 41 | 42 | return listener; 43 | }; 44 | 45 | EventHandler.prototype.unregister = function (event, callback) { 46 | var self = this; 47 | 48 | if (event && !_.contains(this.supportedEvents, event)) { 49 | return ExceptionHandler.create( 50 | 'ShelfIllegalEventException', 51 | 'The event you want to unsubscribe from is not supported. Supported events are: ' + this.supportedEvents.join(',') 52 | ); 53 | } 54 | 55 | if (callback) { 56 | this.listeners[event][callback].off(); 57 | delete this.listeners[event][callback]; 58 | } 59 | else if (event) { 60 | _.each(this.listeners[event], function (listener) { 61 | self.unregister(event, listener.callback); 62 | }); 63 | } 64 | else { 65 | _.each(this.listeners, function (listeners, event) { 66 | self.unregister(event); 67 | }); 68 | } 69 | }; 70 | 71 | module.exports = EventHandler; 72 | -------------------------------------------------------------------------------- /lib/core/validations.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | var _ = require('lodash'); 5 | 6 | var ExceptionHandler = require ('./exception-handler'); 7 | 8 | function ValidationService () {} 9 | 10 | ValidationService.strategies = { 11 | string: function (property) { 12 | return _.isString(property); 13 | }, 14 | number: function (property) { 15 | return _.isNumber(property) && !_.isNaN(property); 16 | }, 17 | boolean: function (property) { 18 | return _.isBoolean(property); 19 | }, 20 | date: function (property) { 21 | return _.isDate(property); 22 | }, 23 | object: function (property) { 24 | return _.isObject(property) && !_.isArray(property); 25 | }, 26 | array: function (property) { 27 | return _.isArray(property); 28 | } 29 | }; 30 | 31 | ValidationService.prototype.validate = function (item, schema) { 32 | _.each(schema, function (validations, key) { 33 | var value = item[key]; 34 | 35 | this._validateRequired(value, validations.required, key); 36 | this._validateType(value, validations.type, key); 37 | this._validateCustomValidation(item, value, validations.validate, key); 38 | }, this); 39 | 40 | return true; 41 | }; 42 | 43 | 44 | 45 | ValidationService.prototype._validateRequired = function (value, required, key) { 46 | if (!required) { 47 | return; 48 | } 49 | 50 | if (!value && value !== false) { 51 | throw ExceptionHandler.create('ShelfInvalidValidationException', 52 | 'There was an error validating field: ' + key + '. ' + 53 | 'Expected required field to be not empty.' 54 | ); 55 | } 56 | }; 57 | 58 | 59 | ValidationService.prototype._validateType = function (value, type, key) { 60 | if (!type) { 61 | return; 62 | } 63 | 64 | if (!ValidationService.strategies[type](value)) { 65 | throw ExceptionHandler.create('ShelfInvalidValidationException', 66 | 'There was an error validating field: ' + key + '. ' + 67 | 'Expected type: ' + type + ', was: ' + (typeof value) 68 | ); 69 | } 70 | }; 71 | 72 | 73 | ValidationService.prototype._validateCustomValidation = function (item, value, validation, key) { 74 | if (!validation) { 75 | return; 76 | } 77 | 78 | if (_.isRegExp(validation) && !validation.test(value)) { 79 | throw ExceptionHandler.create('ShelfInvalidValidationException', 80 | 'There was an error validating field: ' + key + '. ' + 81 | 'Validating ' + validation + ' failed.' 82 | ); 83 | } 84 | else if (!validation.call(item, value)) { 85 | throw ExceptionHandler.create('ShelfInvalidValidationException', 86 | 'There was an error validating field: ' + key + '. ' + 87 | 'A custom validation returned failed.' 88 | ); 89 | } 90 | }; 91 | 92 | module.exports = new ValidationService(); 93 | -------------------------------------------------------------------------------- /test/sync.test.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* global describe, afterEach, before, beforeEach, it */ 3 | 4 | // Ignores "Expected an assignment or function call and instead saw an expression" 5 | /* jshint -W030 */ 6 | 7 | 'use strict'; 8 | 9 | var PouchDb = require('pouchdb'); 10 | var PouchDbStore = require('../index'); 11 | 12 | var expect = require('chai').expect; 13 | var http = require('http'); 14 | var isNode = require('detect-node'); 15 | 16 | var pouch, syncStore; 17 | 18 | PouchDb.plugin(PouchDbStore); 19 | 20 | describe('Testing shelfdb sync', function () { 21 | 22 | before(function () { 23 | syncStore = new PouchDb('sync', isNode ? { db: require('memdown') } : {}).store(); 24 | }); 25 | 26 | beforeEach(function () { 27 | pouch = new PouchDb('test', isNode ? { db: require('memdown') } : {}); 28 | }); 29 | 30 | describe('using sync', function () { 31 | 32 | it('will create a sync connection when using sync() with a string argument', 33 | function () { 34 | var store = pouch.store(); 35 | 36 | var sync = store.sync('http://localhost:1234/remote/sync'); 37 | 38 | expect(sync).to.exist; 39 | expect(store._sync).to.exist; 40 | expect(sync).to.equal(store._sync); 41 | expect(sync.canceled).to.equal(false); 42 | }); 43 | 44 | it('will create a sync connection when using sync() with a store argument', 45 | function () { 46 | var store = pouch.store(); 47 | 48 | var syncStore = new PouchDb('sync', isNode ? { db: require('memdown') } : {}).store(); 49 | 50 | var sync = store.sync(syncStore); 51 | 52 | expect(sync).to.exist; 53 | expect(store._sync).to.exist; 54 | expect(sync).to.equal(store._sync); 55 | expect(sync.canceled).to.equal(false); 56 | }); 57 | 58 | it('will create a sync connection when using sync() with a pouchdb argument', 59 | function () { 60 | var store = pouch.store(); 61 | 62 | var syncStore = new PouchDb('sync', isNode ? { db: require('memdown') } : {}); 63 | 64 | var sync = store.sync(syncStore); 65 | 66 | expect(sync).to.exist; 67 | expect(store._sync).to.exist; 68 | expect(sync).to.equal(store._sync); 69 | expect(sync.canceled).to.equal(false); 70 | }); 71 | 72 | it('will create a sync connection when providing sync option on initialization', 73 | function () { 74 | var store = pouch.store({ 75 | sync: 'http://localhost:1234/remote/sync' 76 | }); 77 | 78 | var sync = store.sync(); 79 | 80 | expect(sync).to.exist; 81 | expect(store._sync).to.exist; 82 | expect(sync).to.equal(store._sync); 83 | expect(sync.canceled).to.equal(false); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /lib/core/exception-handler.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | function ShelfInitializationException(message, info) { 5 | this.message = message + JSON.stringify(info, null, 2); 6 | this.name = 'ShelfInitializationException'; 7 | this.info = info; 8 | } 9 | 10 | ShelfInitializationException.prototype = Error.prototype; 11 | 12 | function ShelfProtectedPropertyException (message, info) { 13 | this.message = message + JSON.stringify(info, null, 2); 14 | this.name = 'ShelfProtectedPropertyException'; 15 | this.info = info; 16 | } 17 | 18 | ShelfProtectedPropertyException.prototype = Error.prototype; 19 | 20 | function ShelfDocumentUpdateConflict (message, info) { 21 | this.message = message + JSON.stringify(info, null, 2); 22 | this.name = 'ShelfDocumentUpdateConflict'; 23 | this.info = info; 24 | } 25 | 26 | ShelfDocumentUpdateConflict.prototype = Error.prototype; 27 | 28 | function ShelfDocumentNotFoundConflict (message, info) { 29 | this.message = message + JSON.stringify(info, null, 2); 30 | this.name = 'ShelfDocumentNotFoundConflict'; 31 | this.info = info; 32 | } 33 | 34 | ShelfDocumentNotFoundConflict.prototype = Error.prototype; 35 | 36 | function ShelfGenericErrorException (message, info) { 37 | this.message = message + JSON.stringify(info, null, 2); 38 | this.name = 'ShelfGenericErrorException'; 39 | this.info = info; 40 | } 41 | 42 | ShelfGenericErrorException.prototype = Error.prototype; 43 | 44 | function ShelfIllegalEventException (message, info) { 45 | this.message = message + JSON.stringify(info, null, 2); 46 | this.name = 'ShelfIllegalEventException'; 47 | this.info = info; 48 | } 49 | 50 | ShelfIllegalEventException.prototype = Error.prototype; 51 | 52 | function ShelfInvalidValidationException(message, info) { 53 | this.message = message + JSON.stringify(info, null, 2); 54 | this.name = 'ShelfInvalidValidationException'; 55 | this.info = info; 56 | } 57 | 58 | ShelfInitializationException.prototype = Error.prototype; 59 | 60 | module.exports = { 61 | create: function (type, message, error) { 62 | // 63 | // Allows definition by argument or in a single 64 | // definition object in the form of 65 | // 66 | // { type: '', message: '', error: {} } 67 | // 68 | var info = { 69 | type: type || arguments[0].type, 70 | message: message || arguments[0].message, 71 | _original: error || arguments[0].error 72 | }; 73 | 74 | switch (type) { 75 | case 'ShelfDocumentUpdateConflict': 76 | return new ShelfDocumentUpdateConflict(message, info); 77 | case 'ShelfInitializationException': 78 | return new ShelfInitializationException(message, info); 79 | case 'ShelfProtectedPropertyException': 80 | return new ShelfProtectedPropertyException(message, info); 81 | case 'ShelfDocumentNotFoundConflict': 82 | return new ShelfDocumentNotFoundConflict(message, info); 83 | case 'ShelfInitializationException': 84 | return new ShelfInitializationException(message, info); 85 | case 'ShelfIllegalEventException': 86 | return new ShelfIllegalEventException(message, info); 87 | case 'ShelfInvalidValidationException': 88 | return new ShelfInvalidValidationException(message, info); 89 | } 90 | 91 | return new ShelfGenericErrorException(message, info); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /test/server.test.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* global describe, afterEach, beforeEach, it, catchIt */ 3 | 4 | // Ignores "Expected an assignment or function call and instead saw an expression" 5 | /* jshint -W030 */ 6 | 7 | 'use strict'; 8 | 9 | var PouchDb = require('pouchdb'); 10 | var PouchDbStore = require('../index'); 11 | 12 | var expect = require('chai').expect; 13 | var http = require('http'); 14 | 15 | var isNode = require('detect-node'); 16 | 17 | // 18 | // This is a backend-only part of the implementation. 19 | // No use letting it run on the browser, as it would fail 20 | // on the requirements. 21 | // 22 | if (isNode) { 23 | 24 | var app, server, pouch; 25 | 26 | PouchDb.plugin(PouchDbStore); 27 | 28 | describe('Testing shelfdb server', function () { 29 | 30 | beforeEach(function () { 31 | var express = require('express'); 32 | app = express(); 33 | 34 | pouch = new PouchDb('test', { db: require('memdown') }); 35 | }); 36 | 37 | afterEach(function () { 38 | server.close(); 39 | }); 40 | 41 | describe('using store.listen()', function () { 42 | 43 | it('will attach the store to the server app', 44 | function (done) { 45 | 46 | var store = pouch.store(); 47 | 48 | store.listen(app); 49 | 50 | server = app.listen(1234); 51 | 52 | http.get('http://localhost:1234/test', function (response) { 53 | expect(response.statusCode).to.be.equal(200); 54 | done(); 55 | }) 56 | .on('error', function(error) { 57 | done(error); 58 | }); 59 | }); 60 | 61 | it('will correctly use option "root"', function (done) { 62 | 63 | var store = pouch.store(); 64 | 65 | store.listen(app, { root: '/store' }); 66 | 67 | server = app.listen(1234); 68 | 69 | http.get('http://localhost:1234/store/test', function (response) { 70 | expect(response.statusCode).to.be.equal(200); 71 | done(); 72 | }) 73 | .on('error', function(error) { 74 | done(error); 75 | }); 76 | }); 77 | }); 78 | 79 | describe('using initialization options', function () { 80 | 81 | it('will attach the store to the server app', 82 | function (done) { 83 | 84 | var store = pouch.store({ listen: app }); 85 | 86 | server = app.listen(1234); 87 | 88 | http.get('http://localhost:1234/test', function (response) { 89 | expect(response.statusCode).to.be.equal(200); 90 | done(); 91 | }) 92 | .on('error', function(error) { 93 | done(error); 94 | }); 95 | }); 96 | 97 | it('will correctly use option "root"', function (done) { 98 | 99 | var store = pouch.store({ listen: app, root: '/store' }); 100 | 101 | server = app.listen(1234); 102 | 103 | http.get('http://localhost:1234/store/test', function (response) { 104 | expect(response.statusCode).to.be.equal(200); 105 | done(); 106 | }) 107 | .on('error', function(error) { 108 | done(error); 109 | }); 110 | }); 111 | }); 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /lib/core/item.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | // ----------------------------------------------------------------- Initialization 5 | // ------------------------------------------------------------ Dependencies 6 | // ------------------------------------------------------- Libraries 7 | var _ = require('lodash'); 8 | 9 | // ------------------------------------------------------- Internal 10 | var ExceptionHandler = require('./exception-handler'); 11 | var ValidationService = require('./validations'); 12 | 13 | var protectedProperties = [ 14 | 'save', 15 | 'rev', 16 | '_store', 17 | '__store' 18 | ]; 19 | 20 | // ------------------------------------------------------------ Object creation 21 | // ------------------------------------------------------- Constructor 22 | /** 23 | * @description 24 | * Constructor method for the item class. 25 | * Creates a new item. 26 | * 27 | * @params: 28 | * [data]: A plain object holding the initial data for the object instance 29 | * to create. 30 | * 31 | * @returns: 32 | * [Item]: The newly created item. 33 | */ 34 | function Item(store, data) { 35 | 36 | if (data instanceof Item) { 37 | return data; 38 | } 39 | 40 | if (!data.rev) { 41 | _.each(protectedProperties, function (key) { 42 | if (data[key] !== undefined) { 43 | throw ExceptionHandler.create('ShelfProtectedPropertyException', 44 | [ 45 | 'There was an exception creating a new item. The attributes', 46 | '"' + protectedProperties.join('", "') + '" are protected and cannot', 47 | 'be assigned' 48 | ].join(' ')); 49 | } 50 | }); 51 | } 52 | 53 | _.extend(this, data, Item._defaults(store)); 54 | } 55 | 56 | 57 | // ------------------------------------------------------------ Default options 58 | Item._defaults = function (store) { 59 | 60 | return { 61 | // The store to persist this item to. 62 | __store: store, 63 | 64 | // Stores store for reference in case this is a related item. 65 | _store: store._name, 66 | 67 | // Overwrite default toString to print the proper filename. 68 | toString: function () { 69 | return '[object Item]'; 70 | } 71 | }; 72 | }; 73 | 74 | 75 | // ----------------------------------------------------------------- Public interface 76 | // ------------------------------------------------------------ Persistance 77 | /** 78 | * @description 79 | * Persistance method for this item. 80 | * Persists this instance in the associated store. 81 | * 82 | * @returns: 83 | * [Promise]: A promise that will be resolved once 84 | * the item has been persisted. 85 | */ 86 | Item.prototype.store = function () { 87 | return this.__store.store(this); 88 | }; 89 | 90 | // ------------------------------------------------------------ Deletion 91 | /** 92 | * @description 93 | * Deletion method for this item. 94 | * Deletes this instance from the associated store. 95 | * 96 | * @returns: 97 | * [Promise]: A promise that will be resolved once 98 | * the item has been deleted. 99 | */ 100 | Item.prototype.remove = function () { 101 | return this.__store.remove(this); 102 | }; 103 | 104 | // ------------------------------------------------------------ Validation 105 | /** 106 | * @description 107 | * Validates this item. 108 | * A item is considered valid if it follows all rules assigned 109 | * as specified in it's store property schema. 110 | * 111 | * 112 | * @returns: 113 | * [boolean]: True if this item is valid, false if not. 114 | */ 115 | Item.prototype.validate = function () { 116 | try { 117 | return ValidationService.validate(this, this.__store._schema.validates); 118 | } 119 | catch (error) { 120 | return false; 121 | } 122 | }; 123 | 124 | module.exports = Item; 125 | -------------------------------------------------------------------------------- /lib/adapter/pouch-store.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | var _ = require('lodash'); 5 | 6 | var PouchDb = require('pouchdb'); 7 | 8 | var isNode = require('detect-node'); 9 | var config = require('../../config/store.config.json'); 10 | 11 | // ----------------------------------------------------------------- Initialization 12 | // ------------------------------------------------------------ Instance 13 | // ------------------------------------------------------- Constructor 14 | function PouchStore () {} 15 | 16 | PouchStore.createDefaultStore = function (options) { 17 | 18 | function tryAndLoadAdapters (preferredAdapters) { 19 | for (var index=0; index= 3) { 190 | expect(callbackCalled).to.equal(1); 191 | return done(); 192 | } 193 | 194 | controlGroupCalled++; 195 | Store.store({ 196 | value: Math.random() 197 | }); 198 | }; 199 | Store.on('change', controlGroup); 200 | 201 | // Start the chain 202 | _.delay(function () { 203 | Store.store({ 204 | value: 'test' 205 | }); 206 | }, 100); 207 | }); 208 | }); 209 | 210 | 211 | }); 212 | -------------------------------------------------------------------------------- /test/find.test.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* global describe, after, before, it, catchIt */ 3 | 4 | // Ignores "Expected an assignment or function call and instead saw an expression" 5 | /* jshint -W030 */ 6 | 7 | 'use strict'; 8 | 9 | var PouchDb = require('pouchdb'); 10 | var PouchDbStore = require('../index'); 11 | 12 | var expect = require('chai').expect; 13 | var _ = require('lodash'); 14 | var q = require('bluebird'); 15 | var isNode = require('detect-node'); 16 | var sampleDataJson = require('./fixtures/sample-data.json'); 17 | 18 | require('mocha-qa').global(); 19 | 20 | var Store, pouch, sampleData = {}; 21 | 22 | PouchDb.plugin(PouchDbStore); 23 | 24 | function clearAllDocs (pouch) { 25 | return pouch.allDocs() 26 | .then(function (response) { 27 | return q.all(_.map(response.rows, function (row) { 28 | return pouch.remove(row.id, row.value.rev); 29 | })); 30 | }); 31 | } 32 | 33 | describe('Testing shelfdb lookups', function(){ 34 | 35 | before(function initialize () { 36 | pouch = new PouchDb('tests', isNode ? { db: require('memdown') } : {}); 37 | Store = pouch.store(); 38 | }); 39 | 40 | before(function emptyDb () { 41 | return clearAllDocs(pouch); 42 | }); 43 | 44 | before(function populateDb () { 45 | var items = _.cloneDeep(sampleDataJson.values); 46 | 47 | return pouch.bulkDocs(items) 48 | .then(function (identity) { 49 | return _.map(items, function (item, index) { 50 | sampleData[item.value] = _.extend(item, identity[index]); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('using find(:id)', function () { 56 | 57 | it('returns the right item when querying by id', 58 | function () { 59 | 60 | var testItem = sampleData['test-1']; 61 | 62 | return Store.find(testItem.id) 63 | .then(function (item) { 64 | expect(item.id).to.equal(testItem.id); 65 | expect(item.rev).to.exist; 66 | expect(item.value).to.equal('test-1'); 67 | expect(item.toString()).to.equal('[object Item]'); 68 | expect(item.nested.deep.value).to.equal('test-nested-deep-1'); 69 | }); 70 | }); 71 | 72 | catchIt('throws an \'not-found\' exception when no item is found for the given id', 73 | function () { 74 | return Store.find('invalid'); 75 | }); 76 | }); 77 | 78 | describe('using find(:query)', function () { 79 | it('returns the right item when querying by query', 80 | function () { 81 | 82 | var testItem = sampleData['test-1']; 83 | 84 | return Store.find({ 85 | value: testItem.value 86 | }) 87 | .then(function (items) { 88 | var item = _.first(items); 89 | 90 | expect(item.id).to.exist; 91 | expect(item.id).to.equal(testItem.id); 92 | expect(item.rev).to.exist; 93 | expect(item.value).to.equal('test-1'); 94 | expect(item.nested.deep.value).to.equal('test-nested-deep-1'); 95 | }); 96 | }); 97 | 98 | it('returns the right item when querying by nested query', 99 | function () { 100 | 101 | var testItem = sampleData['test-1']; 102 | 103 | return Store.find({ 104 | nested: { 105 | deep: { 106 | value: testItem.nested.deep.value 107 | } 108 | } 109 | }) 110 | .then(function (items) { 111 | var item = _.first(items); 112 | 113 | expect(item.id).to.exist; 114 | expect(item.id).to.equal(testItem.id); 115 | expect(item.rev).to.exist; 116 | expect(item.value).to.equal('test-1'); 117 | expect(item.nested.deep.value).to.equal('test-nested-deep-1'); 118 | }); 119 | }); 120 | 121 | it('returns all items that match the given query with nested queries', 122 | function () { 123 | 124 | return Store.find({ 125 | shared: 'shared' 126 | }) 127 | .then(function (items) { 128 | expect(items.length).to.equal(4); 129 | }); 130 | }); 131 | 132 | it('returns all items that match the given query', 133 | function () { 134 | 135 | return Store.find({ 136 | nested: { 137 | shared: 'nested-shared' 138 | } 139 | }) 140 | .then(function (items) { 141 | expect(items.length).to.equal(3); 142 | }); 143 | }); 144 | 145 | it('returns an empty array if no entity in the store matches the query', 146 | function () { 147 | 148 | return Store.find({ value: 'invalid' }) 149 | .then(function (items) { 150 | expect(items.length).to.equal(0); 151 | }); 152 | }); 153 | }); 154 | 155 | 156 | describe('using find()', function () { 157 | 158 | it('returns all items in the store', 159 | function () { 160 | return Store.find() 161 | .then(function (items) { 162 | expect(items.length).to.equal(5); 163 | }); 164 | }); 165 | 166 | it('returns the full dataset', 167 | function () { 168 | 169 | var testItem = sampleData['test-1']; 170 | 171 | return Store.find({ 172 | nested: { 173 | deep: { 174 | value: testItem.nested.deep.value 175 | } 176 | } 177 | }) 178 | .then(function (items) { 179 | var item = _.first(items); 180 | 181 | expect(item.id).to.exist; 182 | expect(item.id).to.equal(testItem.id); 183 | expect(item.rev).to.exist; 184 | expect(item.value).to.equal('test-1'); 185 | expect(item.nested.deep.value).to.equal('test-nested-deep-1'); 186 | }); 187 | }); 188 | 189 | it('returns an empty array if there is no entity in the store', 190 | function () { 191 | 192 | return clearAllDocs(pouch) 193 | .then(function () { 194 | return Store.find() 195 | .then(function (items) { 196 | expect(items.length).to.equal(0); 197 | }); 198 | }); 199 | }); 200 | 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /test/validation.test.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* global describe, after, before, it, catchIt */ 3 | 4 | // Ignores "Expected an assignment or function call and instead saw an expression" 5 | /* jshint -W030 */ 6 | 7 | 'use strict'; 8 | 9 | var PouchDb = require('pouchdb'); 10 | var PouchDbStore = require('../index'); 11 | 12 | var expect = require('chai').expect; 13 | 14 | var _ = require('lodash'); 15 | var q = require('bluebird'); 16 | var isNode = require('detect-node'); 17 | 18 | require('mocha-qa').global(); 19 | 20 | var Store, pouch, sampleData = {}; 21 | 22 | PouchDb.plugin(PouchDbStore); 23 | 24 | function clearAllDocs (pouch) { 25 | return pouch.allDocs() 26 | .then(function (response) { 27 | return q.all(_.map(response.rows, function (row) { 28 | return pouch.remove(row.id, row.value.rev); 29 | })); 30 | }); 31 | } 32 | 33 | describe('Testing shelfdb schema', function(){ 34 | 35 | describe('adding validations', function () { 36 | 37 | it('will correctly validate an item using type definitions', 38 | function () { 39 | var Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store({ 40 | validates: { 41 | stringValue: 'string', 42 | numberValue: 'number', 43 | dateValue: 'date', 44 | booleanValue: 'boolean', 45 | objectValue: 'object', 46 | arrayValue: 'array' 47 | } 48 | }); 49 | 50 | var test = Store.new({ 51 | stringValue: 'string', 52 | numberValue: 123, 53 | dateValue: new Date(), 54 | booleanValue: true, 55 | objectValue: { a: 123 }, 56 | arrayValue: [1,2,3] 57 | }); 58 | 59 | expect(test.validate()).to.equal(true); 60 | }); 61 | 62 | it('will correctly validate a valid item using required definitions', 63 | function () { 64 | var Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store({ 65 | validates: { 66 | stringValue: { required: true }, 67 | numberValue: { required: true }, 68 | dateValue: { required: true }, 69 | booleanValue: { required: true }, 70 | objectValue: { required: true }, 71 | arrayValue: { required: true } 72 | } 73 | }); 74 | 75 | var test = Store.new({ 76 | stringValue: 'string', 77 | numberValue: 123, 78 | dateValue: new Date(), 79 | booleanValue: true, 80 | objectValue: { a: 123 }, 81 | arrayValue: [1,2,3] 82 | }); 83 | 84 | expect(test.validate()).to.equal(true); 85 | }); 86 | 87 | it('will call a custom validation function for each property specified', 88 | function () { 89 | var count = 0; 90 | 91 | function customValidation () { 92 | count++; 93 | return true; 94 | } 95 | 96 | var Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store({ 97 | validates: { 98 | stringValue: customValidation, 99 | numberValue: customValidation, 100 | dateValue: customValidation, 101 | booleanValue: customValidation, 102 | objectValue: customValidation, 103 | arrayValue: customValidation 104 | } 105 | }); 106 | 107 | var test = Store.new({ 108 | stringValue: 'string', 109 | numberValue: 123, 110 | dateValue: new Date(), 111 | booleanValue: true, 112 | objectValue: { a: 123 }, 113 | arrayValue: [1,2,3] 114 | }); 115 | 116 | test.validate(); 117 | expect(count).to.equal(6); 118 | }); 119 | 120 | 121 | it('will fail validating invalid strings', 122 | function () { 123 | var Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store({ 124 | validates: { 125 | value: 'string' 126 | } 127 | }); 128 | 129 | var test = Store.new({ 130 | value: 123 131 | }); 132 | 133 | expect(test.validate()).to.equal(false); 134 | }); 135 | 136 | it('will fail validating invalid numbers', 137 | function () { 138 | var Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store({ 139 | validates: { 140 | value: 'number' 141 | } 142 | }); 143 | 144 | var test = Store.new({ 145 | value: 'string' 146 | }); 147 | 148 | expect(test.validate()).to.equal(false); 149 | }); 150 | 151 | it('will fail validating invalid dates', 152 | function () { 153 | var Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store({ 154 | validates: { 155 | value: 'date' 156 | } 157 | }); 158 | 159 | var test = Store.new({ 160 | value: 'string' 161 | }); 162 | 163 | expect(test.validate()).to.equal(false); 164 | }); 165 | 166 | 167 | it('will fail validating invalid boolean', 168 | function () { 169 | var Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store({ 170 | validates: { 171 | value: 'boolean' 172 | } 173 | }); 174 | 175 | var test = Store.new({ 176 | value: undefined 177 | }); 178 | 179 | expect(test.validate()).to.equal(false); 180 | }); 181 | 182 | it('will fail validating invalid objects', 183 | function () { 184 | var Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store({ 185 | validates: { 186 | value: 'object' 187 | } 188 | }); 189 | 190 | var test = Store.new({ 191 | value: [] 192 | }); 193 | 194 | expect(test.validate()).to.equal(false); 195 | }); 196 | 197 | it('will fail validating invalid arrays', 198 | function () { 199 | var Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store({ 200 | validates: { 201 | value: 'array' 202 | } 203 | }); 204 | 205 | var test = Store.new({ 206 | value: {} 207 | }); 208 | 209 | expect(test.validate()).to.equal(false); 210 | }); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /test/schema.test.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* global describe, after, before, it, catchIt */ 3 | 4 | // Ignores "Expected an assignment or function call and instead saw an expression" 5 | /* jshint -W030 */ 6 | 7 | 'use strict'; 8 | 9 | var PouchDb = require('pouchdb'); 10 | var PouchDbStore = require('../index'); 11 | 12 | var expect = require('chai').expect; 13 | var _ = require('lodash'); 14 | var q = require('bluebird'); 15 | var isNode = require('detect-node'); 16 | 17 | require('mocha-qa').global(); 18 | 19 | var pouch, sampleData = {}; 20 | 21 | PouchDb.plugin(PouchDbStore); 22 | 23 | function clearAllDocs (pouch) { 24 | return pouch.allDocs() 25 | .then(function (response) { 26 | return q.all(_.map(response.rows, function (row) { 27 | return pouch.remove(row.id, row.value.rev); 28 | })); 29 | }); 30 | } 31 | 32 | var options = isNode ? { 33 | defaultStore: { 34 | db: require('memdown') 35 | }, 36 | associatedStore: { 37 | db: require('leveldown') 38 | } 39 | } : { 40 | defaultStore: { 41 | adapter: 'websql' 42 | }, 43 | associatedStore: { 44 | adapter: 'websql' 45 | } 46 | }; 47 | 48 | describe('Testing shelfdb schema', function () { 49 | 50 | after(function () { 51 | if (isNode) { 52 | var fs = require('fs.extra'); 53 | 54 | fs.rmrfSync('./tracks'); 55 | fs.rmrfSync('./artists'); 56 | fs.rmrfSync('./labels'); 57 | fs.rmrfSync('./likes'); 58 | } 59 | }); 60 | 61 | describe('allows to add hasMany relations', function () { 62 | 63 | it('will automatically infer the store by name, if providing a name only on hasMany()', 64 | function () { 65 | var RecordStore = new PouchDb('records', options.defaultStore).store(); 66 | RecordStore.hasMany('tracks'); 67 | 68 | var TrackStore = RecordStore._schema.hasMany.tracks; 69 | 70 | expect(TrackStore.toString()).to.equal('[object Store]'); 71 | expect(TrackStore._adapter.pouch._db_name).to.equal('tracks'); 72 | expect(TrackStore._adapter.pouch.__opts.db).to.equal(options.defaultStore.db); 73 | }); 74 | 75 | it('will add a hasMany relation given the name of the relation and the name of the store to use for the relation', 76 | function () { 77 | var RecordStore = new PouchDb('records', options.defaultStore).store(); 78 | RecordStore.hasMany('items', 'tracks'); 79 | 80 | var TestStore = RecordStore._schema.hasMany.items; 81 | 82 | expect(TestStore.toString()).to.equal('[object Store]'); 83 | expect(TestStore._adapter.pouch._db_name).to.equal('tracks'); 84 | expect(TestStore._adapter.pouch.__opts.db).to.equal(options.defaultStore.db); 85 | }); 86 | 87 | it('will add a hasMany relation given the name and a store to use for the relation', 88 | function () { 89 | var RecordStore = new PouchDb('playlists', options.defaultStore).store(); 90 | var TrackStore = new PouchDb('tracks', options.associatedStore).store(); 91 | RecordStore.hasMany('tracks', TrackStore); 92 | var TestStore = RecordStore._schema.hasMany.tracks; 93 | 94 | expect(TestStore.toString()).to.equal('[object Store]'); 95 | expect(TestStore._adapter.pouch._db_name).to.equal('tracks'); 96 | expect(TestStore._adapter.pouch.__opts.db).to.equal(options.associatedStore.db); 97 | }); 98 | 99 | it('will allow to add nested hasMany relations when providing a store as the relation', 100 | function () { 101 | var RecordStore = new PouchDb('playlists', options.defaultStore).store(); 102 | var TrackStore = new PouchDb('tracks', options.associatedStore).store(); 103 | 104 | TrackStore.hasMany('likes'); 105 | RecordStore.hasMany('tracks', TrackStore); 106 | 107 | var TestStore = RecordStore._schema.hasMany.tracks; 108 | var LikeStore = TestStore._schema.hasMany.likes; 109 | 110 | expect(LikeStore.toString()).to.equal('[object Store]'); 111 | expect(LikeStore._adapter.pouch._db_name).to.equal('likes'); 112 | expect(LikeStore._adapter.pouch.__opts.db).to.equal(options.associatedStore.db); 113 | }); 114 | 115 | it('will not add nested hasMany relations when providing the store by name', 116 | function () { 117 | var RecordStore = new PouchDb('playlists', options.defaultStore).store(); 118 | 119 | var TrackStore = new PouchDb('tracks', options.associatedStore).store(); 120 | 121 | TrackStore.hasMany('likes'); 122 | RecordStore.hasMany('tracks'); 123 | 124 | var TestStore = RecordStore._schema.hasMany.tracks; 125 | 126 | expect(TestStore._schema.likes).to.equal(undefined); 127 | }); 128 | }); 129 | 130 | describe('allows to add hasMany relations', function () { 131 | 132 | it('will automatically infer the store by name, if providing a name only on hasOne()', 133 | function () { 134 | var TrackStore = new PouchDb('tracks', options.defaultStore).store(); 135 | 136 | TrackStore.hasOne('artist'); 137 | 138 | var ArtistStore = TrackStore._schema.hasOne.artist; 139 | 140 | expect(ArtistStore.toString()).to.equal('[object Store]'); 141 | expect(ArtistStore._adapter.pouch._db_name).to.equal('artist'); 142 | expect(ArtistStore._adapter.pouch.__opts.db).to.equal(options.defaultStore.db); 143 | }); 144 | 145 | it('will add a hasOne relation given the name of the relation and the name of the store to use for the relation', 146 | function () { 147 | var TrackStore = new PouchDb('tracks', options.defaultStore).store(); 148 | 149 | TrackStore.hasOne('artist', 'artists'); 150 | 151 | var ArtistStore = TrackStore._schema.hasOne.artist; 152 | 153 | expect(ArtistStore.toString()).to.equal('[object Store]'); 154 | expect(ArtistStore._adapter.pouch._db_name).to.equal('artists'); 155 | expect(ArtistStore._adapter.pouch.__opts.db).to.equal(options.defaultStore.db); 156 | }); 157 | 158 | it('will add a hasOne relation given the name and a store to use for the relation', 159 | function () { 160 | var TrackStore = new PouchDb('tracks', options.defaultStore).store(); 161 | var AritstCollection = new PouchDb('artists', options.associatedStore).store(); 162 | 163 | TrackStore.hasOne('artist', AritstCollection); 164 | 165 | var TestStore = TrackStore._schema.hasOne.artist; 166 | 167 | expect(TestStore.toString()).to.equal('[object Store]'); 168 | expect(TestStore._adapter.pouch._db_name).to.equal('artists'); 169 | expect(TestStore._adapter.pouch.__opts.db).to.equal(options.associatedStore.db); 170 | }); 171 | 172 | it('will allow to add nested hasOne relations when providing a store as the relation', 173 | function () { 174 | var TrackStore = new PouchDb('tracks', options.defaultStore).store(); 175 | var ArtistStore = new PouchDb('artists', options.associatedStore).store(); 176 | 177 | TrackStore.hasOne('artist', ArtistStore); 178 | ArtistStore.hasOne('label', 'labels'); 179 | 180 | var TestStore = TrackStore._schema.hasOne.artist; 181 | var LabelStore = TestStore._schema.hasOne.label; 182 | 183 | expect(LabelStore.toString()).to.equal('[object Store]'); 184 | expect(LabelStore._adapter.pouch._db_name).to.equal('labels'); 185 | expect(LabelStore._adapter.pouch.__opts.db).to.equal(options.associatedStore.db); 186 | }); 187 | 188 | it('will not add nested hasOne relations when providing the store by name', 189 | function () { 190 | var TrackStore = new PouchDb('tracks', options.defaultStore).store(); 191 | var ArtistStore = new PouchDb('artists', options.associatedStore).store(); 192 | 193 | TrackStore.hasOne('artist', 'artists'); 194 | ArtistStore.hasOne('label', 'labels'); 195 | 196 | var TestStore = TrackStore._schema.hasOne.artist; 197 | 198 | expect(TestStore._schema.likes).to.equal(undefined); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /test/remove.test.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* global describe, afterEach, beforeEach, it, catchIt */ 3 | 4 | // Ignores "Expected an assignment or function call and instead saw an expression" 5 | /* jshint -W030 */ 6 | 7 | 'use strict'; 8 | 9 | var PouchDb = require('pouchdb'); 10 | var PouchDbStore = require('../index'); 11 | 12 | var expect = require('chai').expect; 13 | var _ = require('lodash'); 14 | var q = require('bluebird'); 15 | var isNode = require('detect-node'); 16 | 17 | PouchDb.plugin(PouchDbStore); 18 | 19 | var samples = _.cloneDeep(require('./fixtures/sample-data.json').values); 20 | 21 | require('mocha-qa').global(); 22 | 23 | function clearAllDocs (pouch) { 24 | return pouch.allDocs({ include_docs: true }) 25 | .then(function (response) { 26 | return q.all(_.map(response.rows, function (row) { 27 | return pouch.remove(row.id, row.value.rev); 28 | })); 29 | }); 30 | } 31 | 32 | var Store, pouch, sampleData; 33 | 34 | describe('Testing PouchDbStore deletions', function(){ 35 | 36 | beforeEach(function populateDb () { 37 | 38 | pouch = new PouchDb('tests', isNode ? { db: require('memdown') } : {}); 39 | Store = pouch.store(); 40 | 41 | return clearAllDocs(pouch) 42 | .then(function () { 43 | return Store.store(_.cloneDeep(samples)) 44 | .then(function (items) { 45 | sampleData = {}; 46 | 47 | _.each(items, function (item) { 48 | sampleData[item.value] = item; 49 | }); 50 | }); 51 | }); 52 | }); 53 | 54 | afterEach(function () { 55 | return clearAllDocs(pouch); 56 | }); 57 | 58 | describe('using remove(:item)', function () { 59 | 60 | catchIt('will delete the item provided', 61 | function () { 62 | 63 | var testItem = sampleData['test-1']; 64 | 65 | return Store.remove(testItem) 66 | .then(function () { 67 | return Store.find(testItem.id) 68 | .catch(function (error) { 69 | expect(error.name).to.equal('ShelfDocumentNotFoundConflict'); 70 | expect(error.info._original.reason).to.equal('deleted'); 71 | 72 | throw error; 73 | }); 74 | }); 75 | }); 76 | 77 | it('will only delete the item provided and no other', 78 | function () { 79 | 80 | var testItem = sampleData['test-1']; 81 | 82 | return Store.remove(testItem) 83 | .then(function () { 84 | return Store.find() 85 | .then(function (items) { 86 | var mapped = {}; 87 | _.each(items, function (item) { 88 | mapped[item.id] = item; 89 | }); 90 | 91 | expect(items.length).to.equal(4); 92 | expect(mapped[testItem.id]).to.be.undefined; 93 | }); 94 | }); 95 | }); 96 | 97 | catchIt('throws an ShelfDocumentUpdateConflict if the item is tried to be deleted twice', 98 | function () { 99 | 100 | var item; 101 | 102 | return Store.store(sampleData['test-1']) 103 | .then(function (storedItem) { 104 | item = storedItem; 105 | return Store.remove(storedItem); 106 | }) 107 | .then(function () { 108 | return Store.remove(item) 109 | .catch(function (error) { 110 | expect(error.name).to.equal('ShelfDocumentUpdateConflict'); 111 | expect(error.info._original.message).to.equal('Document update conflict'); 112 | 113 | throw error; 114 | }); 115 | }); 116 | }); 117 | }); 118 | 119 | describe('using remove([:items])', function () { 120 | 121 | 122 | catchIt('will delete the items provided', 123 | function () { 124 | 125 | var testItems = [sampleData['test-1'], sampleData['test-2']]; 126 | 127 | return Store.remove(testItems) 128 | .then(function () { 129 | return Store.find(testItems[0].id) 130 | .catch(function (error) { 131 | expect(error.type).to.equal('not-found'); 132 | expect(error._original.reason).to.equal('deleted'); 133 | 134 | Store.find(testItems[1].id) 135 | .catch(function (error) { 136 | expect(error.name).to.equal('ShelfDocumentNotFoundConflict'); 137 | expect(error.info._original.reason).to.equal('deleted'); 138 | 139 | throw error; 140 | }); 141 | }); 142 | }); 143 | }); 144 | 145 | it('will only delete the items provided and no other', 146 | function () { 147 | 148 | var testItems = [sampleData['test-1'], sampleData['test-2']]; 149 | 150 | return Store.remove(testItems) 151 | .then(function () { 152 | return Store.find() 153 | .then(function (items) { 154 | var mapped = {}; 155 | _.each(items, function (item) { 156 | mapped[item.id] = item; 157 | }); 158 | 159 | expect(items.length).to.equal(3); 160 | expect(mapped[testItems[0].id]).to.be.undefined; 161 | expect(mapped[testItems[1].id]).to.be.undefined; 162 | }); 163 | }); 164 | }); 165 | 166 | catchIt('throws an ShelfDocumentUpdateConflict if the items are tried to be deleted twice', 167 | function () { 168 | 169 | var items; 170 | 171 | return Store.store([sampleData['test-1'], sampleData['test-2']]) 172 | .then(function (storedItems) { 173 | items = _.cloneDeep(storedItems); 174 | return Store.remove(storedItems); 175 | }) 176 | .then(function () { 177 | return Store.remove(items) 178 | .catch(function (error) { 179 | expect(error.name).to.equal('ShelfDocumentUpdateConflict'); 180 | expect(error.info._original.message).to.equal('Document update conflict'); 181 | 182 | throw error; 183 | }); 184 | }); 185 | }); 186 | }); 187 | 188 | describe('using remove(:query)', function () { 189 | 190 | 191 | catchIt('will delete a single item solely identified by a query', 192 | function () { 193 | 194 | var testItem = sampleData['test-1']; 195 | 196 | return Store.remove({ value: testItem.value }) 197 | .then(function () { 198 | var promises = []; 199 | 200 | return Store.find(testItem.id) 201 | .catch(function (error) { 202 | expect(error.name).to.equal('ShelfDocumentNotFoundConflict'); 203 | expect(error.info._original.reason).to.equal('deleted'); 204 | throw error; 205 | }); 206 | }); 207 | }); 208 | 209 | 210 | it('will delete multiple items identified by a query', 211 | function () { 212 | 213 | return Store.remove({ 'shared': 'shared' }) 214 | .then(function () { 215 | var promises = []; 216 | 217 | return Store.find() 218 | .then(function (items) { 219 | expect(items.length).to.equal(1); 220 | }); 221 | }); 222 | }); 223 | 224 | it('will only delete the items provided and no other', 225 | function () { 226 | 227 | var testItem = sampleData['test-1']; 228 | 229 | return Store.remove({ value: testItem.value }) 230 | .then(function () { 231 | return Store.find() 232 | .then(function (items) { 233 | 234 | var mapped = {}; 235 | _.each(items, function (item) { 236 | mapped[item.id] = item; 237 | }); 238 | 239 | expect(items.length).to.equal(4); 240 | expect(mapped[testItem.id]).to.be.undefined; 241 | }); 242 | }); 243 | }); 244 | 245 | it('delete no item when the query does not produce a match', 246 | function () { 247 | 248 | var testItem = sampleData['test-1']; 249 | 250 | return Store.remove({ value: 'invalid' }) 251 | .then(function (deletedItems) { 252 | expect(deletedItems.length).to.equal(0); 253 | }); 254 | }); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /test/store.test.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* global describe, beforeEach, before, after, it, catchIt, isBrowser */ 3 | 4 | // Ignores "Expected an assignment or function call and instead saw an expression" 5 | /* jshint -W030 */ 6 | 7 | 'use strict'; 8 | 9 | var PouchDb = require('pouchdb'); 10 | var PouchDbStore = require('../index'); 11 | 12 | var expect = require('chai').expect; 13 | var _ = require('lodash'); 14 | var q = require('bluebird'); 15 | var isNode = require('detect-node'); 16 | 17 | require('mocha-qa').global(); 18 | 19 | PouchDb.plugin(PouchDbStore); 20 | 21 | var Store, pouch; 22 | 23 | function clearAllDocs (pouch) { 24 | return pouch.allDocs() 25 | .then(function (response) { 26 | return q.all(_.map(response.rows, function (row) { 27 | return pouch.remove(row.id, row.value.rev); 28 | })); 29 | }); 30 | } 31 | 32 | describe('Testing shelfdb storage', function(){ 33 | 34 | before(function setUpTestServer () { 35 | var options = isNode ? { db: require('memdown') } : {}; 36 | 37 | pouch = new PouchDb('tests', options); 38 | Store = pouch.store(); 39 | }); 40 | 41 | beforeEach(function () { 42 | return clearAllDocs(pouch); 43 | }); 44 | 45 | describe('using store() with new items', function () { 46 | 47 | it('successfully creates a new item with a generated _id', 48 | function () { 49 | return Store.store({ 50 | value: 'test' 51 | }).then(function (storedItem) { 52 | return pouch.get(storedItem.id, { include_docs: true }) 53 | .then(function (queriedItem) { 54 | expect(queriedItem._id).to.equal(storedItem.id); 55 | expect(queriedItem._rev).to.equal(storedItem.rev); 56 | expect(queriedItem.value).to.equal(storedItem.value); 57 | }); 58 | }); 59 | }); 60 | 61 | it('successfully returns the newly created item', 62 | function () { 63 | 64 | return Store.store({ 65 | value: 'test' 66 | }).then(function (storedItem) { 67 | expect(storedItem.id).to.exist; 68 | expect(storedItem.rev).to.exist; 69 | expect(storedItem.value).to.equal('test'); 70 | }); 71 | }); 72 | 73 | it('with a single item successfully stores exactly one item', 74 | function () { 75 | 76 | return Store.store({ 77 | value: 'test' 78 | }).then(function (storedItem) { 79 | return pouch.allDocs() 80 | .then(function (response) { 81 | expect(response.rows.length).to.equal(1); 82 | }); 83 | }); 84 | }); 85 | 86 | it('with an array of n items successfully stores exactly the same number of items', 87 | function () { 88 | 89 | return Store.store([{ 90 | value: 'test-1' 91 | }, { 92 | value: 'test-2' 93 | }, { 94 | value: 'test-3' 95 | }]).then(function (storedItem) { 96 | return pouch.allDocs() 97 | .then(function (response) { 98 | expect(response.rows.length).to.equal(3); 99 | }); 100 | }); 101 | }); 102 | 103 | 104 | it('with an array of items successfully stores all items in the array', 105 | function () { 106 | 107 | return Store.store([{ 108 | value: 'test-1' 109 | }, { 110 | value: 'test-2' 111 | }, { 112 | value: 'test-3' 113 | }]).then(function (storedItem) { 114 | return pouch.allDocs({ include_docs: true }) 115 | .then(function (response) { 116 | var values = _.map(response.rows, function (row) { 117 | return row.doc.value; 118 | }); 119 | 120 | expect(values).to.include('test-1'); 121 | expect(values).to.include('test-2'); 122 | expect(values).to.include('test-3'); 123 | }); 124 | }); 125 | }); 126 | 127 | it('with an array of items will store the items correctly', 128 | function () { 129 | 130 | return Store.store([{ 131 | value: 'test-1' 132 | }, { 133 | value: 'test-2' 134 | }, { 135 | value: 'test-3' 136 | }]).then(function () { 137 | return pouch.allDocs({ include_docs: true }) 138 | .then(function (response) { 139 | var sorted = {}; 140 | 141 | _.each(response.rows, function (row) { 142 | sorted[row.doc.value] = { 143 | value: row.doc.value, 144 | id: row.id, 145 | rev: row.value.rev 146 | }; 147 | }); 148 | 149 | var sample = sorted['test-1']; 150 | 151 | expect(sample.value).to.equal('test-1'); 152 | expect(sample.id).to.exist; 153 | expect(sample.rev).to.exist; 154 | }); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('using store() with existing items', function () { 160 | 161 | it('will not create another new item', 162 | function () { 163 | 164 | return Store 165 | .store({ 166 | value: 'test-1' 167 | }) 168 | .then(function (item) { 169 | return Store.store(_.merge(item, { 170 | value: 'test-2' 171 | })); 172 | }) 173 | .then(function (storedItem) { 174 | return pouch.allDocs() 175 | .then(function (response) { 176 | expect(response.rows.length).to.equal(1); 177 | }); 178 | }); 179 | }); 180 | 181 | it('will succeed in overriding an existing item\'s attribute', 182 | function () { 183 | 184 | return Store 185 | .store({ 186 | value: 'test-1' 187 | }) 188 | .then(function (item) { 189 | return Store.store(_.merge(item, { 190 | value: 'test-2' 191 | })); 192 | }) 193 | .then(function (storedItem) { 194 | return pouch.get(storedItem.id) 195 | .then(function (item) { 196 | expect(item.value).to.equal('test-2'); 197 | expect(item._id).to.exist; 198 | expect(item._rev).to.exist; 199 | }); 200 | }); 201 | }); 202 | 203 | it('will succeed in adding an attribute to an existing item', 204 | function () { 205 | 206 | return Store 207 | .store({ 208 | value: 'test-1' 209 | }) 210 | .then(function (item) { 211 | return Store.store(_.merge(item, { 212 | other: 'test-2' 213 | })); 214 | }) 215 | .then(function (storedItem) { 216 | return pouch.get(storedItem.id) 217 | .then(function (item) { 218 | expect(item.value).to.equal('test-1'); 219 | expect(item.other).to.equal('test-2'); 220 | expect(item._id).to.exist; 221 | expect(item._rev).to.exist; 222 | }); 223 | }); 224 | }); 225 | 226 | it('will not update an item by default if the item is unchanged', 227 | function () { 228 | 229 | var initial; 230 | 231 | return Store 232 | .store({ 233 | value: 'test-1' 234 | }) 235 | .then(function (item) { 236 | initial = _.cloneDeep(item); 237 | return Store.store(item); 238 | }) 239 | .then(function (storedItem) { 240 | expect(storedItem.rev).to.equal(initial.rev); 241 | }); 242 | }); 243 | 244 | 245 | it('will not create an additional item when using bulk operation', 246 | function () { 247 | 248 | return Store 249 | .store([{ value: 'test-1' }, { value: 'test-2' }, { value: 'test-3' }]) 250 | .then(function (storedItems) { 251 | return Store.store(_.map(storedItems, function (item) { 252 | item.value = 'test'; 253 | return item; 254 | })); 255 | }) 256 | .then(function (updatedItems) { 257 | pouch.allDocs() 258 | .then(function (response) { 259 | expect(response.rows.length).to.equal(3); 260 | }); 261 | }); 262 | }); 263 | 264 | it('will succeed in udpating multiple items when provided in an array', 265 | function () { 266 | 267 | return Store 268 | .store([{ value: 'test-1' }, { value: 'test-2' }, { value: 'test-3' }]) 269 | .then(function (storedItems) { 270 | return Store.store(_.map(storedItems, function (item) { 271 | item.value = 'test'; 272 | return item; 273 | })); 274 | }) 275 | .then(function (updatedItems) { 276 | pouch.allDocs({ include_docs: true }) 277 | .then(function (response) { 278 | expect(response.rows[0].doc.value).to.equal('test'); 279 | expect(response.rows[1].doc.value).to.equal('test'); 280 | expect(response.rows[2].doc.value).to.equal('test'); 281 | }); 282 | }); 283 | }); 284 | 285 | catchIt('throws a ShelfDocumentUpdateConflict when trying to update an already updated item', 286 | function () { 287 | 288 | var revision; 289 | 290 | return Store 291 | .store({ 292 | value: 'test-1' 293 | }) 294 | .then(function (item) { 295 | revision = item.rev; 296 | return Store.store(_.merge(item, { 297 | value: 'test-2' 298 | })); 299 | }) 300 | .then(function (item) { 301 | return Store.store(_.merge(item, { 302 | value: 'test-3', 303 | rev: revision 304 | })); 305 | }) 306 | .catch(function (error) { 307 | expect(error.name).to.equal('ShelfDocumentUpdateConflict'); 308 | throw error; 309 | }); 310 | }); 311 | }); 312 | }); 313 | -------------------------------------------------------------------------------- /lib/store.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | // ----------------------------------------------------------------- Initialization 5 | // ------------------------------------------------------------ Dependencies 6 | // ------------------------------------------------------- Vendor 7 | var _ = require('lodash'); 8 | var q = require('bluebird'); 9 | 10 | // ------------------------------------------------------- Core 11 | var Item = require('./core/item'); 12 | var ValidationService = require('./core/validations'); 13 | 14 | // ------------------------------------------------------- Adapter 15 | var Adapter = require('./adapter/pouch')(options); 16 | 17 | // ------------------------------------------------------- Config 18 | var options = require('../config/shelfdb.config.json'); 19 | 20 | // ------------------------------------------------------------ Object creation 21 | // ------------------------------------------------------- Constructor 22 | function PouchDbStore (pouch, options) { 23 | 24 | options = _.merge({}, options); 25 | 26 | this._name = pouch._db_name; 27 | this._adapter = new Adapter(this, pouch); 28 | 29 | this.sync(options.sync, options); 30 | this.listen(options.listen, options); 31 | this.schema(options); 32 | } 33 | 34 | // ------------------------------------------------------- Static loader 35 | PouchDbStore.load = function (pouch, options) { 36 | return new PouchDbStore(pouch, options); 37 | }; 38 | 39 | 40 | // ----------------------------------------------------------------- Public interface 41 | // ------------------------------------------------------------ Definition 42 | // ------------------------------------------------------- Schema 43 | /** 44 | * @description 45 | * Sets the schema definition for this store. 46 | * This schema will be used for this store's schema validation. 47 | * 48 | * @params: 49 | * [schema]: The schema to set. Can hold any of the following settings: 50 | * - validates: The properties of this store. Will be used 51 | * for validation (see validates for possible settings). 52 | * - hasMany: A one-to-many relation to another store. The 53 | * related store can be provided as a store 54 | * object or by name, 55 | * - hasOne: A one-to-one relation to another store. The 56 | * related store can be provided as a store 57 | * object or by name, 58 | * 59 | * @returns: 60 | * [store]: This store updated with the given schema definition. 61 | */ 62 | PouchDbStore.prototype.schema = function (schema) { 63 | 64 | // Set initial empty validation 65 | this._schema = { 66 | hasMany: {}, 67 | hasOne: {}, 68 | validates: {} 69 | }; 70 | 71 | if (!schema) { 72 | return; 73 | } 74 | 75 | _.each(schema.validates, function (validation, name) { 76 | this.validates.call(this, name, validation); 77 | }, this); 78 | 79 | // 80 | // Make sure to initialize stores for relations, 81 | // in case relations were provided by name, 82 | // 83 | _.each(schema.hasOne, function (store, name) { 84 | this.hasOne.call(this, name, store); 85 | }, this); 86 | 87 | _.each(schema.hasMany, function (store, name) { 88 | this.hasMany.call(this, name, store); 89 | }, this); 90 | 91 | return this; 92 | }; 93 | 94 | 95 | // ------------------------------------------------------- Properties 96 | /** 97 | * @description 98 | * Adds a new validation definition to this store. 99 | * This validation will be part of the this store's schema validation. 100 | * 101 | * @params: 102 | * [name]: The name of the property to apply the validation on. 103 | * [schema]: The validation to execute on each create / update of an item. 104 | * * type: One of: string, number, boolean, date, object, array 105 | * (default: any) 106 | * * required: Requires the property to be not empty (default false) 107 | * * validate: a regex pattern or valdiation function to validate 108 | * the property with (default: undefined) 109 | * 110 | * @returns: 111 | * [store]: This store updated with an extended schema definition. 112 | */ 113 | PouchDbStore.prototype.validates = function (name, schema) { 114 | var validation = schema; 115 | 116 | if (_.isString(schema)) { 117 | validation = { type: schema }; 118 | } else if (_.isFunction(schema)) { 119 | validation = { validate: schema }; 120 | } 121 | 122 | this._schema.validates[name] = validation; 123 | 124 | return this; 125 | }; 126 | 127 | 128 | // ------------------------------------------------------- Relations 129 | /** 130 | * @description 131 | * Adds a one-to-many relation to this store. 132 | * Related items will be: 133 | * * handled as independent entities, i.e., have their own id and rev 134 | * * will be automatically synced with their corresponding store, 135 | * when their parent item is saved 136 | * * able to be handled independently from the store they are assigned 137 | * i.e., can be manipulated / stored without making changes to the parent 138 | * entity 139 | * 140 | * @params: 141 | * [name]: The name of the relation. This is how the relation will be 142 | * identified when storing / restoring the parent item. 143 | * If no storeName parameter is provided the name will also be 144 | * assumed to be the name of the related store. 145 | * [storeName]: (Optional) The name for the related store, in cases 146 | * the field name and storeName differ. 147 | * 148 | * @returns: 149 | * [store]: This store updated with the given relation. 150 | */ 151 | PouchDbStore.prototype.hasMany = function (name, store) { 152 | store = arguments.length === 1 ? name : store; 153 | 154 | if (_.isString(store)) { 155 | store = PouchDbStore.load(Adapter.load(store, this._adapter.pouch.__opts)); 156 | } 157 | 158 | this._schema.hasMany[name] = store; 159 | 160 | return this; 161 | }; 162 | 163 | /** 164 | * @description 165 | * Adds a one-to-one relation to this store. 166 | * Related items will be: 167 | * * handled as independent entities, i.e., have their own id and rev 168 | * * will be automatically synced with their corresponding store, 169 | * when their parent item is saved 170 | * * able to be handled independently from the store they are assigned 171 | * i.e., can be manipulated / stored without loading and / or making 172 | * changes to the parent entity 173 | * 174 | * @params: 175 | * [name]: The name of the relation. This is how the relation will be 176 | * identified when storing / restoring the parent item. 177 | * If no storeName parameter is provided the name will also be 178 | * assumed to be the name of the related store. 179 | * [storeName]: (Optional) The name for the related store, in cases 180 | * the field name and storeName differ. 181 | * 182 | * @returns: 183 | * [store]: This store updated with the given relation. 184 | */ 185 | PouchDbStore.prototype.hasOne = function (name, store) { 186 | store = arguments.length === 1 ? name : store; 187 | 188 | if (_.isString(store)) { 189 | store = PouchDbStore.load(Adapter.load(store, this._adapter.pouch.__opts)); 190 | } 191 | 192 | this._schema.hasOne[name] = store; 193 | 194 | return this; 195 | }; 196 | 197 | // ------------------------------------------------------------ Data Manipulation 198 | // ------------------------------------------------------- Creation 199 | /** 200 | * @description 201 | * Creation method for this store. 202 | * Creates a new item. If any data is provided, the newly created 203 | * item will hold this data. 204 | * 205 | * @params: 206 | * [data]: (Optional) The initial data for the newly created item. 207 | * 208 | * @returns: 209 | * [Item]: A new item. 210 | */ 211 | PouchDbStore.prototype.new = function (data) { 212 | return new Item(this, data); 213 | }; 214 | 215 | 216 | 217 | /** 218 | * @description 219 | * Creates a new item. If any data is provided 220 | * 221 | * @params: 222 | * [element]: One or many elements to update. The items are expected to 223 | * have a valid id and rev. 224 | * 225 | * @returns: 226 | * [Promise]: A promise that will be resolved once 227 | * the item has been updated. 228 | */ 229 | PouchDbStore.prototype.store = function (items) { 230 | var expectsSingleResult = !_.isArray(items) && arguments.length === 1; 231 | 232 | // Convert arguments to array to allow single approach 233 | // to processing store operation. 234 | items = this._toArray.apply(this, arguments); 235 | 236 | // Store simple objects separately to ensure we can update 237 | // existing items correctly 238 | this._validate(items); 239 | 240 | var self = this; 241 | 242 | return this._adapter.store(items) 243 | .then(function (storedData) { 244 | var updatedItems = _.map(storedData, function (data, index) { 245 | return self._convertToItem(items[index], data); 246 | }); 247 | 248 | return expectsSingleResult ? _.first(updatedItems) : updatedItems; 249 | }); 250 | }; 251 | 252 | 253 | // ------------------------------------------------------- Lookups 254 | /** 255 | * @description 256 | * Lookup method for this store. 257 | * Will search for a persisted item matching the given id or query. 258 | * 259 | * @params (one of): 260 | * [id]: The id by which to identify the item to find. 261 | * [query]: The query object to evaluate. This cane be a simple object, 262 | * i.e., a map of key-value-pairs or a nested object. 263 | * 264 | * @returns: 265 | * [Promise]: A promise that on resolution will return exactly the one 266 | * item that matches the given id. 267 | * If no match could be found, the promise will be rejected. 268 | */ 269 | PouchDbStore.prototype.find = function () { 270 | var self = this; 271 | 272 | return this._adapter.find.apply(this._adapter, arguments) 273 | .then(function (results) { 274 | return self._convertToItem(null, results); 275 | }); 276 | }; 277 | 278 | 279 | // ------------------------------------------------------- Deletions 280 | /** 281 | * @description 282 | * Deletion method for this store. 283 | * Will remove the given item from this store. 284 | * 285 | * @params: 286 | * [item(s)]: One or more items to remove from this store. 287 | * 288 | * @returns: 289 | * [Promise]: A promise that will be resolved on the operation has been 290 | * processed. 291 | */ 292 | PouchDbStore.prototype.remove = function (items) { 293 | return this._adapter.remove.apply(this._adapter, arguments); 294 | }; 295 | 296 | 297 | // ------------------------------------------------------------ Store sync 298 | // ------------------------------------------------------- Setup 299 | PouchDbStore.prototype.sync = function () { 300 | this._sync = this._sync || this._adapter.sync.apply(this._adapter, arguments); 301 | return this._sync; 302 | }; 303 | 304 | 305 | // ------------------------------------------------------------ Store server 306 | // ------------------------------------------------------- Setup 307 | PouchDbStore.prototype.listen = function () { 308 | return this._adapter.listen.apply(this._adapter, arguments); 309 | }; 310 | 311 | 312 | // ------------------------------------------------------------ Event Handling 313 | // ------------------------------------------------------- Subscription 314 | PouchDbStore.prototype.on = function () { 315 | return this._adapter.on.apply(this._adapter, arguments); 316 | }; 317 | 318 | PouchDbStore.prototype.off = function () { 319 | return this._adapter.off.apply(this._adapter, arguments); 320 | }; 321 | 322 | 323 | PouchDbStore.prototype.toString = function () { 324 | return '[object Store]'; 325 | }; 326 | 327 | 328 | // ----------------------------------------------------------------- Private methods 329 | // ------------------------------------------------------------ Conversion 330 | PouchDbStore.prototype._validate = function (items) { 331 | _.each(items, function (item) { 332 | ValidationService.validate(item, this._schema.validates); 333 | }, this); 334 | 335 | return true; 336 | }; 337 | 338 | PouchDbStore.prototype._convertToItem = function (originalItems, storedData) { 339 | var self = this; 340 | 341 | originalItems = originalItems || []; 342 | 343 | function convert (original, storedData) { 344 | // Make sure object identify is kept, while 345 | // when dealing with items 346 | if (original && original instanceof Item) { 347 | return _.extend(original, storedData); 348 | } 349 | else { 350 | return new Item(self, storedData); 351 | } 352 | } 353 | 354 | if (_.isArray(storedData)) { 355 | return _.map(storedData, function (data, index) { 356 | return convert(originalItems[index], data); 357 | }, this); 358 | } 359 | 360 | return convert(originalItems || {}, storedData); 361 | }; 362 | 363 | PouchDbStore.prototype._toArray = function (items) { 364 | if (arguments.length > 1) { 365 | return _.toArray(arguments); 366 | } 367 | return _.isArray(items) ? items : [items]; 368 | }; 369 | 370 | module.exports = PouchDbStore; 371 | -------------------------------------------------------------------------------- /test/relations.test.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* global describe, afterEach, before, it */ 3 | 4 | // Ignores "Expected an assignment or function call and instead saw an expression" 5 | /* jshint -W030 */ 6 | 7 | 'use strict'; 8 | 9 | var PouchDb = require('pouchdb'); 10 | var PouchDbStore = require('../index'); 11 | 12 | var expect = require('chai').expect; 13 | var _ = require('lodash'); 14 | var q = require('bluebird'); 15 | var isNode = require('detect-node'); 16 | 17 | PouchDb.plugin(PouchDbStore); 18 | 19 | require('mocha-qa').global(); 20 | 21 | var Store, VehicleStore, BoatStore, pouch; 22 | 23 | function clearDb () { 24 | 25 | function empty (store) { 26 | var pouch = store._adapter.pouch; 27 | 28 | return pouch.allDocs() 29 | .then(function (response) { 30 | return q.all(_.map(response.rows, function (row) { 31 | return pouch.remove(row.id, row.value.rev); 32 | })); 33 | }); 34 | } 35 | 36 | return q.all([ 37 | empty(Store), 38 | empty(VehicleStore), 39 | empty(BoatStore) 40 | ]); 41 | } 42 | 43 | describe('Testing PouchDbStore relations', function() { 44 | 45 | before(function populateDb () { 46 | 47 | Store = new PouchDb('tests', isNode ? { db: require('memdown') } : {}).store(); 48 | 49 | Store.hasMany('vehicles'); 50 | Store.hasOne('boat', 'boats'); 51 | 52 | VehicleStore = new PouchDb('vehicles', isNode ? { db: require('memdown') } : {}).store(); 53 | BoatStore = new PouchDb('boats', isNode ? { db: require('memdown') } : {}).store(); 54 | 55 | // Just to be sure 56 | return clearDb(); 57 | }); 58 | 59 | afterEach(clearDb); 60 | 61 | describe('using hasMany() and store(:new)', function () { 62 | 63 | it('stores a single related item, when saving the parent item', 64 | function () { 65 | 66 | var testItem = { 67 | value: 'test', 68 | vehicles: [{ 69 | value: 'relation' 70 | }] 71 | }; 72 | 73 | return Store.store(testItem) 74 | .then(function (item) { 75 | 76 | VehicleStore.find() 77 | .then(function (relations) { 78 | expect(relations.length).to.equal(1); 79 | expect(_.first(relations).id).to.exist; 80 | expect(_.first(relations).id).to.equal(_.first(item.vehicles).id); 81 | }); 82 | }); 83 | }); 84 | 85 | it('stores a multiple related items, when saving the parent item', 86 | function () { 87 | 88 | var testItem = { 89 | value: 'test', 90 | vehicles: [{ 91 | value: 'relation-1' 92 | }, { 93 | value: 'relation-2' 94 | }, { 95 | value: 'relation-3' 96 | }] 97 | }; 98 | 99 | return Store.store(testItem) 100 | .then(function (item) { 101 | 102 | VehicleStore.find() 103 | .then(function (relations) { 104 | expect(relations.length).to.equal(3); 105 | }); 106 | }); 107 | }); 108 | 109 | it('stores all related items, when saving multiple parent items at once', 110 | function () { 111 | 112 | var testItems = [{ 113 | value: 'test-1', 114 | vehicles: [{ 115 | value: 'relation-1' 116 | }, { 117 | value: 'relation-2' 118 | }] 119 | }, { 120 | value: 'test-2', 121 | vehicles: [{ 122 | value: 'relation-3' 123 | }, { 124 | value: 'relation-4' 125 | }] 126 | }]; 127 | 128 | return Store.store(testItems) 129 | .then(function (items) { 130 | 131 | VehicleStore.find() 132 | .then(function (relations) { 133 | expect(relations.length).to.equal(4); 134 | }); 135 | }); 136 | }); 137 | 138 | it('does not update related items if they have not changed', 139 | function () { 140 | 141 | return VehicleStore.store({ 142 | value: 'relation-1' 143 | }).then(function (relation) { 144 | // Clone in order to rule out side-effects 145 | relation = _.clone(relation); 146 | 147 | var testItem = { 148 | value: 'test-1', 149 | vehicles: [relation] 150 | }; 151 | 152 | return Store.store(testItem) 153 | .then(function (item) { 154 | 155 | VehicleStore.find() 156 | .then(function (relations) { 157 | expect(relations.length).to.equal(1); 158 | expect(_.first(relations).id).to.equal(relation.id); 159 | expect(_.first(relations).rev).to.equal(relation.rev); 160 | }); 161 | }); 162 | }); 163 | 164 | }); 165 | 166 | it('does update related items if they have changed', 167 | function () { 168 | 169 | return VehicleStore.store({ 170 | value: 'relation-1' 171 | }).then(function (relation) { 172 | // Clone in order to rule out side-effects 173 | relation = _.clone(relation); 174 | 175 | relation.value = 'relation-2'; 176 | 177 | return Store.store({ 178 | value: 'test-1', 179 | vehicles: [relation] 180 | }) 181 | .then(function (item) { 182 | 183 | VehicleStore.find() 184 | .then(function (relations) { 185 | expect(relations.length).to.equal(1); 186 | expect(_.first(relations).id).to.equal(relation.id); 187 | expect(_.first(relations).rev).not.to.equal(relation.rev); 188 | }); 189 | }); 190 | }); 191 | 192 | }); 193 | }); 194 | 195 | describe('using hasOne() and store(:new)', function () { 196 | 197 | it('stores the related item, when saving the parent item', 198 | function () { 199 | 200 | return Store.store({ 201 | value: 'test', 202 | boat: { 203 | value: 'relation' 204 | } 205 | }) 206 | .then(function (item) { 207 | 208 | BoatStore.find() 209 | .then(function (relations) { 210 | expect(relations.length).to.equal(1); 211 | expect(_.first(relations).id).to.exist; 212 | expect(_.first(relations).id).to.equal(item.boat.id); 213 | }); 214 | }); 215 | }); 216 | 217 | 218 | it('does not update related item if it has not changed', 219 | function () { 220 | 221 | return BoatStore.store({ 222 | value: 'relation-1' 223 | }).then(function (relation) { 224 | // Clone in order to rule out side-effects 225 | relation = _.clone(relation); 226 | 227 | var testItem = { 228 | value: 'test-1', 229 | boat: relation 230 | }; 231 | 232 | return Store.store(testItem) 233 | .then(function (item) { 234 | 235 | return BoatStore.find() 236 | .then(function (relations) { 237 | expect(relations.length).to.equal(1); 238 | expect(_.first(relations).id).to.equal(relation.id); 239 | expect(_.first(relations).rev).to.equal(relation.rev); 240 | }); 241 | }); 242 | }); 243 | 244 | }); 245 | 246 | it('does update related item if it has changed', 247 | function () { 248 | 249 | var itemToCompare; 250 | 251 | return BoatStore.store({ 252 | value: 'relation-1' 253 | }) 254 | .then(function (relation) { 255 | 256 | // Clone in order to rule out side-effects 257 | itemToCompare = _.clone(relation); 258 | relation = _.clone(relation); 259 | 260 | relation.value = 'relation-2'; 261 | 262 | var testItem = { 263 | value: 'test-1', 264 | boat: relation 265 | }; 266 | 267 | return Store.store(testItem); 268 | }) 269 | .then(function (item) { 270 | return BoatStore.find(); 271 | }) 272 | .then(function (relations) { 273 | expect(relations.length).to.equal(1); 274 | expect(_.first(relations).id).to.equal(itemToCompare.id); 275 | expect(_.first(relations).rev).not.to.equal(itemToCompare.rev); 276 | }); 277 | }); 278 | }); 279 | 280 | describe('using hasOne() and store(:existing)', function () { 281 | 282 | it('updates the related item, when updating the parent item', 283 | function () { 284 | 285 | var itemToCompare; 286 | 287 | return Store.store({ 288 | value: 'test', 289 | boat: { 290 | value: 'relation' 291 | } 292 | }) 293 | .then(function (item) { 294 | itemToCompare = _.clone(item.boat); 295 | 296 | item.value = 'changed'; 297 | item.boat.value = 'relation-changed'; 298 | 299 | return Store.store(item); 300 | }) 301 | .then(function (item) { 302 | return BoatStore.find(); 303 | }) 304 | .then(function (relations) { 305 | expect(relations.length).to.equal(1); 306 | expect(_.first(relations).id).to.equal(itemToCompare.id); 307 | expect(_.first(relations).rev).not.to.equal(itemToCompare.rev); 308 | }); 309 | }); 310 | 311 | it('does not update the related item, when updating the parent item, if it has not changed', 312 | function () { 313 | 314 | var itemToCompare; 315 | 316 | return Store.store({ 317 | value: 'test', 318 | boat: { 319 | value: 'relation' 320 | } 321 | }) 322 | .then(function (item) { 323 | itemToCompare = _.clone(item.boat); 324 | 325 | item.value = 'changed'; 326 | 327 | return Store.store(item); 328 | }) 329 | .then(function (item) { 330 | return BoatStore.find(); 331 | }) 332 | .then(function (relations) { 333 | expect(relations.length).to.equal(1); 334 | expect(_.first(relations).id).to.equal(itemToCompare.id); 335 | expect(_.first(relations).rev).to.equal(itemToCompare.rev); 336 | }); 337 | }); 338 | 339 | it('updates the related item, when updating the parent item, even if the paren remains unchanged', 340 | function () { 341 | 342 | var itemToCompare, relationToCompare; 343 | 344 | return Store.store({ 345 | value: 'test', 346 | boat: { 347 | value: 'relation' 348 | } 349 | }) 350 | .then(function (item) { 351 | itemToCompare = _.clone(item); 352 | relationToCompare = _.clone(item.boat); 353 | 354 | item.boat.value = 'relation-changed'; 355 | 356 | return Store.store(item); 357 | }) 358 | // Assert parent item 359 | .then(function (item) { 360 | return Store.find(); 361 | }) 362 | .then(function (items) { 363 | expect(items.length).to.equal(1); 364 | expect(_.first(items).rev).to.equal(itemToCompare.rev); 365 | }) 366 | // Assert related item 367 | .then(function (item) { 368 | return BoatStore.find(); 369 | }) 370 | .then(function (relations) { 371 | expect(relations.length).to.equal(1); 372 | expect(_.first(relations).rev).not.to.equal(relationToCompare.rev); 373 | }); 374 | }); 375 | }); 376 | 377 | describe('using hasMany() and store(:existing)', function () { 378 | 379 | it('updates the related items, when updating the parent item', 380 | function () { 381 | 382 | var itemToCompare; 383 | 384 | return Store.store({ 385 | value: 'test', 386 | vehicles: [{ 387 | value: 'relation-1' 388 | }, { 389 | value: 'relation-2' 390 | }] 391 | }) 392 | .then(function (item) { 393 | itemToCompare = _.cloneDeep(item.vehicles); 394 | 395 | item.value = 'changed'; 396 | item.vehicles[0].value = 'relation-changed'; 397 | item.vehicles[1].value = 'relation-changed'; 398 | 399 | return Store.store(item); 400 | }) 401 | .then(function (item) { 402 | return VehicleStore.find(); 403 | }) 404 | .then(function (relations) { 405 | expect(relations.length).to.equal(2); 406 | expect(relations[0].rev).not.to.equal(itemToCompare[0].rev); 407 | expect(relations[1].rev).not.to.equal(itemToCompare[1].rev); 408 | }); 409 | }); 410 | }); 411 | 412 | describe('using hasMany() and find()', function () { 413 | 414 | var persistedItem; 415 | 416 | before(function () { 417 | return Store.store({ 418 | value: 'test', 419 | vehicles: [{ 420 | value: 'relation-1' 421 | }, { 422 | value: 'relation-2' 423 | }] 424 | }) 425 | .then(function (item) { 426 | persistedItem = item; 427 | }); 428 | }); 429 | 430 | it('returns all relations when querying for the parent', 431 | function () { 432 | 433 | var itemToCompare; 434 | 435 | return Store.find(persistedItem.id) 436 | .then(function (item) { 437 | expect(item.vehicles).to.exist(); 438 | expect(item.vehicles.length).to.equal(2); 439 | }); 440 | }); 441 | }); 442 | }); 443 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### *DEPRECATED (This plugin is no longer maintained* 2 | #### Archive: 3 | 4 | --- 5 | Store.PouchDb is a simple interface wrapper plugin around [PouchDB](http://pouchdb.com/api.html) offline first database. 6 | 7 | Inspired by the [Dreamcode API](http://nobackend.org/dreamcode.html) project and object relational mappers like the Ruby on Rails [Active Record](http://guides.rubyonrails.org/active_record_basics.html) pattern this project aims to provide a simple interface to access often used request methods like `store` and `find` as well as providing helpers to allow relationship mapping between multiple databases. 8 | 9 | # Installation 10 | 11 | ``` bash 12 | npm install --save store.pouchdb 13 | ``` 14 | 15 | # Setup 16 | 17 | Setup Store.PouchDb using the [PouchDb plugin](http://pouchdb.com/api.html#plugins) notation: 18 | 19 | ``` javascript 20 | var PouchDb = require('pouchdb').plugin(require('store.pouchdb')); 21 | ``` 22 | 23 | After this you can create new stores from any [PouchDb database](http://pouchdb.com/api.html#create_database) using the `store()` method on the database object: 24 | 25 | ``` javascript 26 | var Records = new PouchDb('records').store({ /* options*/ }); 27 | 28 | Records.store({ 29 | artist: 'Pouchfunk', 30 | title: 'Living in a Pouch' 31 | }); 32 | ``` 33 | 34 | # API 35 | 36 | * [Store](#store) 37 | * [Store.store(:object [,...])](#store-store) 38 | * [Store.find(:id)](#store-find-id) 39 | * [Store.find(:query)](#store-find-query) 40 | * [Store.find()](#store-find-all) 41 | * [Store.remove(:item [,...])](#store-remove-item) 42 | * [Store.remove()](#store-remove-all) 43 | * [Syncronization](#store-sync) 44 | * [Store.sync(:name [,:options])](#store-sync) 45 | * [Item](#item) 46 | * [Store.new()](#store-new) 47 | * [item.store()](#item-store) 48 | * [item.remove()](#item-remove) 49 | * [item.$info](#item-info) 50 | * [Properties](#properties) 51 | * [Store.hasMany(:property [,:store])](#properties-has-many) 52 | * [Store.hasOne(:property [,:store])](#properties-has-one) 53 | * [Store.validates(:property, :validation)](#properties-validates) 54 | * [Store.schema(:schema)](#properties-schema) 55 | * [Events](#events) 56 | * [Store.on(:event, :callback)](#store-on) 57 | * [listener.cancel()](#listener-cancel) 58 | 59 | 60 | # Store 61 | 62 | ## Store.store(:item [,...]) 63 | 64 | Basic persistence methods for the store. Takes one or more [items](#item) or plain objects and creates or updates them in the database of this store. 65 | 66 | * Initializes a new id (optional, can also be set manually) and [revision](#item.rev) when called initially on an item or object 67 | * Returns a [promise](#promises) that on resolution returns the updated [item](#item) instance(s). 68 | * On storage each item will be validated against the [schema](#schema) of this Store. 69 | * Will recursively store all one-to-one / one-to-many [associations](#properties.has-many) defined for this store. 70 | * Will not update the item if none of the attributes have been changed (i.e., deep-equals is true). 71 | 72 | ### Example (Single) 73 | 74 | ``` javascript 75 | Users.store({ name: 'John' }) 76 | .then(/* handle response */); 77 | ``` 78 | 79 | ### Example (Multiple) 80 | 81 | ``` javascript 82 | var john = Users.new({ name: 'John' }); 83 | var jane = Users.new({ name: 'Jane' }); 84 | 85 | Users.store(john, jane) 86 | .then(/* handle response */); 87 | ``` 88 | 89 | ### Example (Update) 90 | 91 | ``` javascript 92 | var john = Store.find(/* john's id */); 93 | 94 | john.phone = '+12345678'; 95 | 96 | Users.store(john) 97 | .then(/* handle response */); 98 | ``` 99 | 100 | ## Store.find(:id) 101 | 102 | Looks up a single [item](#item) by it's id. 103 | 104 | * Returns a [promise](#promises) that on resolution will return the item for the requested id. 105 | * The promise will be rejected if you item could be found for the given id. 106 | 107 | ### Example 108 | 109 | ``` javascript 110 | Users.find('uid-123456') 111 | .then(function (user) { 112 | console.log('Hooray, found user' + user.name); 113 | }) 114 | .catch(function (error) { 115 | console.log('Oh no, the user is no longer available in this store.'); 116 | }); 117 | ``` 118 | 119 | ## Store.find(:query) 120 | 121 | Looks up all [items](#item) that match the given query object. 122 | 123 | * Returns a [promise](#promises) that on resolution will return an array with all items that matched the query. 124 | * Returns an empty array if no items could be found. 125 | * The query can be nested and will be evaluated recursively 126 | 127 | ### Example 128 | 129 | ``` javascript 130 | Users.find({ name: 'John' }) 131 | .then(function (users) { 132 | console.log('Found', users.length, 'by the name of "John" in this store'); 133 | }); 134 | ``` 135 | 136 | ### Store.find() 137 | 138 | Looks up all [itens](#item) currently kept in the store. 139 | 140 | * Returns a [promise](#promises) that on resolution will provide all items currently stored in this store. 141 | * Returns an empty array if the store is empty. 142 | 143 | ### Example 144 | 145 | ``` javascript 146 | Users.find() 147 | .then(function (users) { 148 | console.log('There are currently', users.length, 'in the users store'); 149 | }); 150 | ``` 151 | 152 | ## Store.remove(:item) 153 | 154 | Removes the given [item](#item) from the store. 155 | 156 | * Returns a promise that will be resolved once the item has successfully been removed from the store. 157 | 158 | ### Example 159 | 160 | ``` javascript 161 | Records.remove(record) 162 | .then(function() { 163 | console.log('The record was successfully removed')M 164 | }); 165 | ``` 166 | 167 | ## Store.remove() 168 | 169 | Removes all [items](#item) from the store. 170 | 171 | * Returns a promise that will be resolved once all items have successfully been removed from the store. 172 | 173 | ### Example 174 | 175 | ``` javascript 176 | Records.remove() 177 | .then(function() { 178 | console.log('All items have been successfully removed from this store')M 179 | }); 180 | ``` 181 | 182 | # Synchronization 183 | 184 | ## Store.sync(:store [,:options]) 185 | 186 | Sets up (live) synchronization between multiple stores. 187 | 188 | * Equivalent to [PouchDB Sync](http://pouchdb.com/api.html#sync) 189 | * By default will set those PouchDb options: `{ live: true, retry: true }` 190 | * See [events](#events) for listening to either of the synchronized stores 191 | 192 | ### Example 193 | 194 | ``` javascript 195 | var Tracks = new PouchDb('tracks').store(); 196 | 197 | // Will setup synchronization with a remote pouch 198 | Tracks.sync('http://138.231.22.16:9073/pouch/tracks'); 199 | ``` 200 | 201 | # Items 202 | 203 | ## Store.new([:data]) 204 | 205 | Creates a new item instance. 206 | 207 | * The instance will not have been persisted at this point, i.e., it will not have been assigned it's id and initial [revions](#item.rev) yet 208 | * If the optional data object is provided the item will be initialized with the values provided. 209 | 210 | ### Example 211 | 212 | ``` javascript 213 | // Creates a new empty item instance 214 | var john = Users.new(); 215 | john.name = 'John'; 216 | john.store(); 217 | 218 | // Creates a new item instance with intial values 219 | var jane = Users.new({ name: 'Jane' }); 220 | jane.store(); 221 | ``` 222 | 223 | ## item.store() 224 | 225 | Stores the item in the [store](#store) used for the creation of this item. 226 | 227 | * If not previously stored, creates a new id (which can also be provided) and rev for the item. 228 | * Updates the item on each subsequent call. 229 | * Returns a [promise](#promises) that on resolution will return the updated item 230 | * Will be validated against the [schema](#store.schema) of this Store. 231 | * Will not update the item if none of the attributes have been changed (i.e., deep-equals is true). 232 | * Will recursively store all one-to-one / one-to-many [associations](#properties.has-many) defined for this store. 233 | * This method is the instance equivalent to `Store.store(item);` 234 | 235 | ### Example 236 | 237 | ``` javascript 238 | var john = Users.new(/* properties */); 239 | 240 | john.save() 241 | .then(function () { 242 | console.log('Item successfully stored'); 243 | }); 244 | ``` 245 | 246 | ## item.remove() 247 | 248 | Removes the item from the [store](#store) used for the creation of this item. 249 | 250 | * Returns a promise that will be resolved once the item has been successfully deleted. 251 | * This method is the instance equivalent to `Store.remove(item);` 252 | 253 | ### Example 254 | 255 | ``` javascript 256 | var john = Users.new(/* properties */); 257 | 258 | // ... do magic 259 | john.remove(); 260 | ``` 261 | 262 | # item.$info 263 | 264 | Each item will automatically be extended by the following fields: 265 | 266 | * `$info.createdAt` - The time of creation for this item 267 | * `$info.updatedAt` - The last time this item has been updated. 268 | * `$info.store` - The name of the store that holds this item. 269 | 270 | # item.rev 271 | 272 | Each store operation will update an items revision (`rev`). 273 | 274 | * A revision indicates a specific version of an item. 275 | * By default Store.PouchDb will keep all revisions of an item 276 | * Refer to the [PouchDb API](http://pouchdb.com/api.html) for further details on revision handling 277 | 278 | # Properties 279 | 280 | There are two kind of properties that can be defined on a store. 281 | 282 | Associations define the relationship between items of two associated stores. Associations can be defined as either one-to-one or one-to-many relations. 283 | 284 | * Associated items are stored as separate entities in the store specified by the relation. 285 | * Associated items are stored autimatically when the parent item is stored. 286 | * Associated items are loaded automaticaly when loading the parent item. 287 | * Associated items can be stored / queried independently from their parent. 288 | 289 | Validations allow to restrict the charasteristics of certain properties within the item model to be stored. 290 | 291 | ## Store.hasMany(:property [,:store]) 292 | 293 | Defines a one-to-many association to the store. 294 | 295 | * Provide a store argument if the name of the property and the name of the store do not match. 296 | * The store argument can be be either as string with the name of the store or [store](#store) instance. 297 | * If you want to nest associations deeper than one level you must provide each store argument as a [store](#store) instance 298 | 299 | This association is especially helpful if you either need to handle the association items independent from each other, e.g., in different modules, or will (often) update the content of the associated items independently from each other. 300 | 301 | ### Example 302 | 303 | Consider a data-model for one playlist that contains many tracks, with the tracks updated often, e.g., updating their playback state. 304 | Using a nested approach would cause an update, i.e., a new [revision](#revisions), of the playlist whenever any of the associated tracks are updated. Using the hasMany association you can update the track without updating the playlist, while still keeping easy access to the nesting of. 305 | 306 | ``` javascript 307 | Playlists.hasMany('tracks'); 308 | 309 | var playlist = Playlist.find('my-playlist'); 310 | playlist.tracks[0].played++; 311 | playlist.store(); // Updates the first track, but causes no new revision for the playlist 312 | ``` 313 | 314 | ## Store.hasOne(:property [,:store]) 315 | 316 | Defines a one-to-one relation for the store. 317 | 318 | * Provide a store argument if the name of the property and the name of the store do not match. 319 | * The store argument can be be either as string with the name of the store or [store](#store) instance. 320 | * If you want to nest associations deeper than one level you must provide each store argument as a [store](#store) instance 321 | 322 | Use the hasOne relation to keep the associated items as separate entities, that can be loaded independently from each other, e.g., for usage in different modules. 323 | 324 | ### Example 325 | 326 | Consider a data-model for one track that is associated with one artist. Loading the track in the media-player view you'd also like to display the artist information for the track, while on the artist's profile view you need to display the artist information regardles of a specific track. 327 | 328 | ``` javascript 329 | // Player controller 330 | Tracks.hasOne(artist); 331 | 332 | var track = Tracks.find('pouchfunk--living-in-a-pouch'); 333 | var artist = Artists.find('pouchfunk'); 334 | 335 | assert(track.artist.id === artist.id); // true 336 | assert(track.artist.name === artist.name); // true 337 | ``` 338 | 339 | ## Store.validates(:property, :validation) 340 | 341 | Allows to restrict the type and characterisitics that certain properties of an item must apply to, before they can be stored in the associated store. The following validations are available: 342 | 343 | * Takes either a string argument indicating the type of the property or an object defining at least one of: 344 | * type: Expects a `string`. Allows to specify the type of the property. Possible values are: 345 | * `string` 346 | * `number` 347 | * `boolean` 348 | * `date` 349 | * `object` 350 | * `array` 351 | * required: Expects a `boolean`. Flags the property to be obligatory for storing this item. 352 | * validate: Expects a `function`. Allows to give a function that is executed for the property whenever this item is stored. 353 | 354 | ### Example 355 | 356 | ``` javascript 357 | var Tracks = new PouchDb('tracks').store(); 358 | 359 | // Ensures the type of the 'length' property 360 | Tracks.validates('length', 'number'); 361 | 362 | // Ensures 'url' is provided 363 | Tracks.validates('url', { 364 | type: 'string', 365 | required: true 366 | }); 367 | 368 | // Ensures 'artist' has a non-empty name 369 | Tracks.validates('artist', { 370 | type: 'object', 371 | validate: function (artist, track) { 372 | return artist.name && artist.name.length; 373 | } 374 | }); 375 | ``` 376 | 377 | ## Store.schema(:schema) 378 | 379 | Convenience method to apply multiple association and validation specifications in a single call. 380 | 381 | ### Example 382 | 383 | ``` javascript 384 | MyStore.schema({ 385 | hasOne: [{ 386 | artist: 'artists' 387 | }], 388 | hasMany: ['comments', 'likes'], 389 | validates: [{ 390 | length: 'number', 391 | artist: { 392 | type: 'object', 393 | validate: /* function */ 394 | } 395 | }]; 396 | }); 397 | ``` 398 | 399 | # Events 400 | 401 | Events provide a method to listen to changes made to the store 402 | 403 | ## Store.on(:event, :callback) 404 | 405 | Subscribes to the event provided, causing the callback to be called whenever the event is recorded on any store item. 406 | Returns a listener object that allows to keep track of the subscription and [cancel](listener.cancel) it if no longer needed. 407 | 408 | ### Example 409 | 410 | ``` javascript 411 | var listener = MyStore.on('update', function () { 412 | /* the magic happens here */ 413 | }); 414 | ``` 415 | 416 | ## listener.off() 417 | 418 | Applies to the listener object returned by the [on](#store.on)) function. 419 | The listener allows to keep track of the subscription and provides an alternative way to cancel the listener if no longer needed. 420 | 421 | ``` javascript 422 | var listener = MyStore.on('update', function () {/* the magic happens here */}); 423 | listener.off(); 424 | ``` 425 | 426 | # Promises 427 | 428 | Store.PouchDb uses the [bluebird promise library](https://github.com/petkaantonov/bluebird). For further details on available methods see the [API documentation](https://github.com/petkaantonov/bluebird/blob/master/API.md). 429 | 430 | The most commonly used methods are: 431 | 432 | * [promise.then(:callback)](https://github.com/petkaantonov/bluebird/blob/master/API.md#thenfunction-fulfilledhandler--function-rejectedhandler----promise) - Executes the given callback function once the promise has been fulfilled. 433 | * [promise.catch(:callback)](https://github.com/petkaantonov/bluebird/blob/master/API.md#catchfunction-handler---promise) - Executes the given callback function if the promise gets rejected, e.g., because of a conflict or exception. 434 | * [promise.map(:callback)](https://github.com/petkaantonov/bluebird/blob/master/API.md#mapfunction-mapper--object-options---promise) - Executes the given map-function on each item of the promise previous result and returns the manipulated result set. 435 | * [promise.reduce(:callback)](https://github.com/petkaantonov/bluebird/blob/master/API.md#reducefunction-reducer--dynamic-initialvalue---promise) - Executes the given reduce-function on each item of the previous promise result and returns a single result. 436 | * [promise.filter(:callback)](https://github.com/petkaantonov/bluebird/blob/master/API.md#filterfunction-filterer--object-options---promise) - Executes the given filter-function on each item of the previous promise result and allows to filter items on the criteria defined in the callback function. 437 | 438 | # Contributions 439 | 440 | Contributions are very welcome, both as pull requests or just in the form of discussion. 441 | 442 | # Roadmap / Open Topics 443 | 444 | * Clean up library structure - extract pouch adapter into separate repository and decouple store from PouchDb specific implementation 445 | * Improve performance, especially on find operations (see [PouchDb guide on using views](http://pouchdb.com/guides/queries.html)) 446 | * Clean up and improve test cases 447 | * Finalize API 448 | -------------------------------------------------------------------------------- /lib/adapter/pouch.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | // ----------------------------------------------------------------- Initialization 5 | // ------------------------------------------------------------ Dependencies 6 | // ------------------------------------------------------- Libraries 7 | var q = require('bluebird'); 8 | var _ = require('lodash'); 9 | var deepEqual = require('deep-equal'); 10 | var PouchDb = require('pouchdb'); 11 | 12 | // ------------------------------------------------------- Core 13 | var ExceptionHandler = require('../core/exception-handler'); 14 | var EventHandler = require('./pouch-events'); 15 | var Item = require('../core/item'); 16 | 17 | // ------------------------------------------------------- Internal 18 | var PouchServer = require('./pouch-server'); 19 | var PouchSync = require('./pouch-sync'); 20 | 21 | // ------------------------------------------------------------ Object creation 22 | // ------------------------------------------------------- Constructor 23 | function PouchAdapter (store, pouch) { 24 | 25 | this._store = store; 26 | this.eventHandler = new EventHandler(this); 27 | 28 | this.pouch = pouch; 29 | } 30 | 31 | // ------------------------------------------------------------ PouchStore 32 | // ------------------------------------------------------- Initialization 33 | PouchAdapter.init = function (options) { 34 | // PouchAdapter.Middleware = PouchServer.createMiddleware(PouchAdapter.PouchDb); 35 | 36 | return PouchAdapter; 37 | }; 38 | 39 | PouchAdapter.load = function (name, options) { 40 | return new PouchDb(name, options); 41 | }; 42 | 43 | // // ------------------------------------------------------------ Inserts 44 | // // ------------------------------------------------------- store 45 | PouchAdapter.prototype.store = function (items) { 46 | var self = this; 47 | 48 | // console.log('0. store', _.first(items).rev); 49 | // console.trace(); 50 | 51 | // 52 | // 1. Store all relations attached to the 53 | // items to be persisted. Will generate 54 | // the id by which this item can reference them. 55 | // 56 | // Will return a processed set of item(s) 57 | // coupled to their relations. 58 | // 59 | // console.log('1.', items); 60 | var promises = _.map(items, function (item) { 61 | return self._storeItemRelations(item); 62 | }); 63 | 64 | var stashed, markedItems; 65 | 66 | return q.all(promises) 67 | .then(function (data) { 68 | // 69 | // 2. Split items and relations into separate 70 | // containers. 71 | // 72 | // console.log('2.', data); 73 | return self._separateItemAndRelations(data); 74 | }) 75 | .then(function (mappedData) { 76 | // 77 | // 3. Stash separated item and relations for later use 78 | // 79 | // console.log('3.', mappedData); 80 | stashed = mappedData; 81 | return stashed.items; 82 | }) 83 | .then(function (items) { 84 | // 85 | // 4. Test if item to be stored has changed in case 86 | // of an update operation. Only changed items 87 | // will be stored again (esp. limiting inflating the 88 | // revs count on related items) 89 | // 90 | // console.log('4.', items); 91 | return self._markChangedItems(items); 92 | }) 93 | .then(function (items) { 94 | // 95 | // 5. Stash the marked-items in a variable for later use. 96 | // 97 | // console.log('5.', items); 98 | markedItems = items; 99 | return markedItems; 100 | }) 101 | .then(function (markedItems) { 102 | // 103 | // 6. Filter only values that are marked for change. 104 | // Only those will be added to the db in the next step. 105 | // 106 | // console.log('6.', markedItems); 107 | return _.compact(_.map(markedItems, function (marked) { 108 | return marked.changed ? marked.item : undefined; 109 | })); 110 | }) 111 | .then(function (itemsToStore) { 112 | // 113 | // 7. Add meta information 114 | // 115 | // console.log('7.', itemsToStore); 116 | return self._extendWithMetaInformation(itemsToStore); 117 | }) 118 | .then(function (itemsToStore) { 119 | // 120 | // 8. Convert to format processible by PouchDb, i.e., 121 | // remove meta information like '_schema' and functions. 122 | // 123 | // console.log('8.', itemsToStore); 124 | return self._convertToSimpleObject(itemsToStore); 125 | }) 126 | .then(function (itemsToStore) { 127 | // 128 | // 9. Store changed items. 129 | // At this point items will reference their 130 | // relations only by id. Their information has 131 | // been already been stored in a separate step (1.) 132 | // 133 | // console.log('9.', itemsToStore); 134 | return self.pouch.bulkDocs(self._convertToPouch(itemsToStore)); 135 | }) 136 | .then(function (response) { 137 | // 138 | // 10. Some errors are not thrown by pouch but 139 | // rather returned in the response. 140 | // Look for errors and throw them in case of an 141 | // invalid response. 142 | // 143 | // console.log('10.', response); 144 | var error = convertToShelfExeption(response); 145 | if (error) { 146 | return q.reject(error); 147 | } 148 | return response; 149 | }) 150 | .then(function (responses) { 151 | // 152 | // 11. Merge recently updated and unchanged items 153 | // back together to allow for a consistent response. 154 | // 155 | // console.log('11.', responses, markedItems); 156 | var index = 0; 157 | 158 | return _.map(markedItems, function (marked) { 159 | if (marked.changed) { 160 | var response = responses[index++]; 161 | return _.extend(marked.item, { 162 | id: response.id, 163 | rev: response.rev 164 | }); 165 | } 166 | return marked.item; 167 | }); 168 | }) 169 | .then(function (updated) { 170 | // 171 | // 12. Reassemble items, their relations and the updated 172 | // versioning information (id, rev) obtained from 173 | // the storage operation 174 | // 175 | // console.log('12.', updated); 176 | return self._mergeItemAndRelations(stashed.items, stashed.relations, updated); 177 | }) 178 | .then(function (result) { 179 | // 180 | // 13. For debugging purposes: Allows to print the final 181 | // result. 182 | // 183 | // console.log('13.', result); 184 | return result; 185 | }) 186 | .catch(function (error) { 187 | // 188 | // E. Capture any exception that pops up 189 | // during the pocess and format it 190 | // before throwing it up again 191 | // 192 | // console.log('E.', error); 193 | throw convertToShelfExeption(error); 194 | }); 195 | }; 196 | 197 | PouchAdapter.prototype._separateItemAndRelations = function (data) { 198 | return { 199 | items: _.map(data, function (token) { 200 | return token.item; 201 | }), 202 | relations: _.map(data, function (token) { 203 | return token.relations; 204 | }) 205 | }; 206 | }; 207 | 208 | PouchAdapter.prototype._mergeItemAndRelations = function (items, relations, docs) { 209 | var self = this; 210 | 211 | return _.map(items, function (item, index) { 212 | return _.extend( 213 | {}, 214 | item, 215 | relations[index], 216 | self._convertFromPouch(docs[index]), 217 | { 218 | __store: item.__store, 219 | _store: item._store 220 | } 221 | ); 222 | }); 223 | }; 224 | 225 | PouchAdapter.prototype._markChangedItems = function (items) { 226 | var self = this; 227 | 228 | return q.all(_.map(items, function (item) { 229 | if (item.id) { 230 | return self._findOne(item.id) 231 | .catch(function () { 232 | 233 | // In case item holds an id, but has not been 234 | // persisted previously 235 | return undefined; 236 | }); 237 | } 238 | 239 | return q.resolve(undefined); 240 | })) 241 | .then(function (storedItems) { 242 | return _.map(storedItems, function (storedItem, index) { 243 | 244 | // New items will be stored in any case. 245 | var isChanged = true; 246 | var item = items[index]; 247 | 248 | if (!item.rev && storedItem) { 249 | item.rev = storedItem.rev; 250 | item.$info = storedItem.$info; 251 | } 252 | 253 | // console.log('_markChangedItems'); 254 | // console.log(storedItem); 255 | // console.log(item); 256 | 257 | if (storedItem) { 258 | // Filter function: Don't let rev interfer with object 259 | // value-level comparison. 260 | isChanged = !deepEqual(storedItem, self._convertToSimpleObject(item)); 261 | } 262 | 263 | // console.log('markChanged', isChanged); 264 | 265 | return { 266 | changed: isChanged, 267 | item: isChanged ? item : storedItem 268 | }; 269 | }); 270 | }); 271 | }; 272 | 273 | 274 | // ------------------------------------------------------- Relations 275 | PouchAdapter.prototype._storeItemRelations = function (item) { 276 | 277 | var schema = this._store._schema; 278 | var relations = {}; 279 | 280 | function extractIds (docs) { 281 | return _.map(docs, function (doc) { 282 | return doc.id; 283 | }); 284 | } 285 | 286 | function storeHasManyRelation (item) { 287 | return _.map(schema.hasMany, function (connection, name) { 288 | 289 | var relatedItems = item[name]; 290 | delete item[name]; 291 | 292 | if (!_.isObject(relatedItems)) { 293 | return; 294 | } 295 | 296 | relatedItems = _.filter(relatedItems, function (relatedItem) { 297 | // Skip circular data 298 | return !relatedItem.__marked; 299 | }); 300 | 301 | return connection.store(relatedItems) 302 | .then(function (docs) { 303 | relations[name] = docs; 304 | item[name + '_ids'] = extractIds(docs); 305 | }); 306 | }); 307 | } 308 | 309 | function storeHasOneRelation (item) { 310 | return _.map(schema.hasOne, function (connection, name) { 311 | 312 | var value = item[name]; 313 | delete item[name]; 314 | 315 | if (!_.isObject(value)) { 316 | return; 317 | } 318 | 319 | // Stop when encountering circular data 320 | if (value.__marked) { 321 | return; 322 | } 323 | 324 | return connection.store(value) 325 | .then(function (doc) { 326 | relations[name] = doc; 327 | item[name + '_id'] = doc.id; 328 | }); 329 | }); 330 | } 331 | 332 | var promises = []; 333 | 334 | // Mark items as beeing processed to 335 | // prevent circular data from being processed multiple 336 | // times 337 | item.__marked = true; 338 | 339 | promises.push.apply(promises, storeHasManyRelation(item)); 340 | promises.push.apply(promises, storeHasOneRelation(item)); 341 | 342 | return q.all(promises) 343 | .then(function () { 344 | // Remove processing marker again before storing item 345 | delete item.__marked; 346 | return { 347 | item: item, 348 | relations: relations 349 | }; 350 | }); 351 | }; 352 | 353 | 354 | // // ------------------------------------------------------------ Lookups 355 | // // ------------------------------------------------------- findAll 356 | PouchAdapter.prototype.all = function () { 357 | return this.find(); 358 | }; 359 | 360 | // // ------------------------------------------------------- find 361 | PouchAdapter.prototype.find = function () { 362 | 363 | var query = arguments.length > 0 ? arguments[0] : null; 364 | var promise; 365 | 366 | var expectSingleResult = false; 367 | var isArray = _.isArray(query); 368 | 369 | if (_.isEmpty(query)) { 370 | promise = this._findAll(); 371 | } 372 | else if (_.isObject(query) && !isArray) { 373 | promise = this._findByQuery(query); 374 | } 375 | else { 376 | promise = this._findBulk(isArray ? query : [query]); 377 | expectSingleResult = !isArray; 378 | } 379 | 380 | var self = this; 381 | 382 | return promise 383 | .then(function (items) { 384 | return self._addItemRelations(items); 385 | }) 386 | .then(function (items) { 387 | return expectSingleResult ? _.first(items) : items; 388 | }); 389 | }; 390 | 391 | PouchAdapter.prototype._findOne = function (id) { 392 | var self = this; 393 | 394 | return this.pouch.get(id, { include_docs: true }) 395 | .then(function (item) { 396 | return self._convertFromPouch(item); 397 | }) 398 | .catch(function (error) { 399 | throw convertToShelfExeption(error); 400 | }); 401 | }; 402 | 403 | PouchAdapter.prototype._findBulk = function (ids) { 404 | var self = this; 405 | 406 | var promises = _.map(ids, function (id) { 407 | return self.pouch.get(id, { include_docs: true }) 408 | .then(function (item) { 409 | return self._convertFromPouch(item); 410 | }) 411 | .catch(function (error) { 412 | throw convertToShelfExeption(error); 413 | }); 414 | }); 415 | 416 | return q.all(promises); 417 | }; 418 | 419 | PouchAdapter.prototype._findAll = function () { 420 | var self = this; 421 | var deferred = q.defer(); 422 | 423 | this.pouch.allDocs({ include_docs: true }, 424 | function (error, result) { 425 | if (error) { 426 | throw ExceptionHandler.create('ShelfGenericErrorException', 427 | 'A technical error occured. Check original error for further details', 428 | error); 429 | } 430 | 431 | deferred.resolve(_.map(result.rows, function (item) { 432 | return self._convertFromPouch(item.doc); 433 | })); 434 | }); 435 | 436 | return deferred.promise; 437 | }; 438 | 439 | PouchAdapter.prototype._findByQuery = function (query) { 440 | var self = this; 441 | 442 | return this._findAll() 443 | .then(function (results) { 444 | return _.filter(results, self._matchFunction(query)); 445 | }); 446 | }; 447 | 448 | // ------------------------------------------------------- Relations 449 | PouchAdapter.prototype._addItemRelations = function (items) { 450 | 451 | var self = this; 452 | 453 | function fetchHasManyRelations (item) { 454 | return _.map(self._store._schema.hasMany, function (connection, name) { 455 | 456 | var keys = item[name + '_ids']; 457 | if (_.isEmpty(keys)) { 458 | return; 459 | } 460 | 461 | return connection.find() 462 | .then(function (relations) { 463 | item[name] = _.filter(relations, function (relation) { 464 | return _.include(keys, relation.id); 465 | }); 466 | return item; 467 | }); 468 | }); 469 | } 470 | 471 | function fetchHasOneRelations (item) { 472 | return _.map(self._store._schema.hasOne, function (connection, name) { 473 | 474 | var key = item[name + '_id']; 475 | if (_.isUndefined(key)) { 476 | return; 477 | } 478 | 479 | return connection.find(key) 480 | .then(function (relation) { 481 | item[name] = relation; 482 | }); 483 | }); 484 | } 485 | 486 | var promises = []; 487 | 488 | _.each(items, function (item) { 489 | promises.push.apply(promises, fetchHasManyRelations(item)); 490 | promises.push.apply(promises, fetchHasOneRelations(item)); 491 | }); 492 | 493 | return q.all(promises) 494 | .then(function () { 495 | return items; 496 | }); 497 | }; 498 | 499 | 500 | // // ------------------------------------------------------------ Deletion 501 | // ------------------------------------------------------- remove 502 | PouchAdapter.prototype.remove = function (query) { 503 | 504 | if (!query) { 505 | return this._removeAll(); 506 | } 507 | 508 | var isArray = _.isArray(query); 509 | var isItem = query instanceof Item; 510 | 511 | 512 | if (!isItem && !isArray) { 513 | return this._removeByQuery(query); 514 | } 515 | 516 | return this._removeBulk(isArray ? query : [query]) 517 | .catch(function (error) { 518 | throw convertToShelfExeption(error); 519 | }); 520 | }; 521 | 522 | PouchAdapter.prototype._removeByQuery = function (query) { 523 | var self = this; 524 | 525 | return this._findByQuery(query) 526 | .then(function (items) { 527 | return self._removeBulk(items); 528 | }); 529 | }; 530 | 531 | PouchAdapter.prototype._removeBulk = function (items) { 532 | var pouch = this.pouch; 533 | 534 | return q.all(_.map(items, function (item) { 535 | return pouch.remove(item.id, item.rev); 536 | })); 537 | }; 538 | 539 | PouchAdapter.prototype._removeAll = function () { 540 | var self = this; 541 | 542 | return this.pouch.allDocs() 543 | .then(function (response) { 544 | var docs = _.map(response.rows, function (row) { 545 | return { 546 | id: row.id, 547 | rev: row.value.rev, 548 | }; 549 | }); 550 | 551 | return self._removeBulk(docs); 552 | }) 553 | .catch(function (error) { 554 | throw convertToShelfExeption(error); 555 | }); 556 | }; 557 | 558 | // ------------------------------------------------------------ Store synchronization 559 | // ------------------------------------------------------- Setup 560 | PouchAdapter.prototype.sync = function (syncStore, options) { 561 | return PouchSync.sync(this._store, syncStore, options); 562 | }; 563 | 564 | 565 | // ------------------------------------------------------------ Store server 566 | // ------------------------------------------------------- Setup 567 | PouchAdapter.prototype.listen = function (app, options) { 568 | return PouchServer.listen(this._store, app, options); 569 | }; 570 | 571 | 572 | // ----------------------------------------------------------------- Event handlers 573 | // ------------------------------------------------------------ Listener registration 574 | // ------------------------------------------------------- 575 | PouchAdapter.prototype.on = function () { 576 | return this.eventHandler.register.apply(this.eventHandler, arguments); 577 | }; 578 | 579 | PouchAdapter.prototype.off = function () { 580 | this.eventHandler.unregister.apply(this.eventHandler, arguments); 581 | return this; 582 | }; 583 | 584 | 585 | // ----------------------------------------------------------------- Private helpers 586 | // ------------------------------------------------------------ Conversions 587 | // ------------------------------------------------------- from pouch 588 | PouchAdapter.prototype._convertFromPouch = function (items) { 589 | 590 | var self = this; 591 | 592 | function iterate (item) { 593 | 594 | item = _.cloneDeep(item); 595 | 596 | // Bulk operations will return 'id', find operations will return '_id' 597 | item.id = item._id || item.id; 598 | // Bulk operations will return 'rev', find operations will return '_rev' 599 | item.rev = item._rev || item.rev; 600 | 601 | return _.omit(item, ['_id', '_rev', 'ok']); 602 | } 603 | 604 | return _.isArray(items) ? _.map(items, iterate) : iterate(items); 605 | }; 606 | 607 | // ------------------------------------------------------- to pouch 608 | PouchAdapter.prototype._convertToPouch = function (items) { 609 | 610 | function iterate (item) { 611 | 612 | item = _.cloneDeep(item); 613 | 614 | if (item.id) { 615 | item._id = item.id; 616 | } 617 | if (item.rev) { 618 | item._rev = item.rev; 619 | } 620 | 621 | return _.omit(item, ['id', 'rev', 'ok']); 622 | } 623 | 624 | 625 | return _.isArray(items) ? _.map(items, iterate) : iterate(items); 626 | }; 627 | 628 | PouchAdapter.prototype._convertToSimpleObject = function (items) { 629 | 630 | function clean (item) { 631 | 632 | if (!_.isObject(item)) { 633 | return item; 634 | } 635 | 636 | // Omit internal fields 637 | item = _.omit(item, _.union(['_store', '__store'], _.functions(item))); 638 | 639 | _.each(item, function (value, key) { 640 | if (_.isObject(value) && !_.isArray(value)) { 641 | item[key] = clean(value); 642 | } 643 | else if (_.isArray(value)) { 644 | item[key] = _.map(value, function (current) { 645 | return clean(current); 646 | }); 647 | } 648 | }); 649 | 650 | return item; 651 | } 652 | 653 | if (_.isArray(items)) { 654 | return _.map(items, function (item) { 655 | return clean(item); 656 | }); 657 | } 658 | 659 | return clean(items); 660 | }; 661 | 662 | 663 | // ------------------------------------------------------------ Pouch callbacks 664 | // ------------------------------------------------------- match 665 | /** 666 | * @description 667 | * Recursive callback-function for the PouchDB query function. 668 | * 669 | * @returns function match () 670 | * Returns a function to be called by the PouchDB query function. 671 | * On callback matches each doc in the store against the provided 672 | * query object. Adds docs to the result that equal the provided query 673 | * for each attribute in the query object. 674 | * Item attributes not available in the query object will be ignored 675 | * (i.e., not using full equal). 676 | * 677 | */ 678 | PouchAdapter.prototype._matchFunction = function (query) { 679 | 680 | function matchRecursive (item, query) { 681 | return _.reduce(query, function (result, value, key){ 682 | var itemValue = item[key]; 683 | if (_.isObject(value)) { 684 | return result && _.isObject(itemValue) && matchRecursive(itemValue, value); 685 | } 686 | return result && _.isEqual(itemValue, value); 687 | }, true); 688 | } 689 | 690 | return function match (item) { 691 | return matchRecursive(item, query); 692 | }; 693 | }; 694 | 695 | PouchAdapter.prototype._extendWithMetaInformation = function (items) { 696 | 697 | var self = this; 698 | var now = Date.now(); 699 | 700 | _.each(items, function (item) { 701 | item.$info = _.merge({ 702 | createdAt: now 703 | }, item.$info, { 704 | updatedAt: now, 705 | store: self._store._name 706 | }); 707 | }); 708 | 709 | return items; 710 | }; 711 | 712 | 713 | // ------------------------------------------------------------ Error Handling 714 | // ------------------------------------------------------- convert pouch errors 715 | function convertToShelfExeption (responses) { 716 | responses = _.isArray(responses) ? responses : [responses]; 717 | 718 | for (var i=0; i