├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── EXAMPLES.md ├── LICENSE ├── README.md ├── beautify.json ├── bin └── publish.sh ├── bower.json ├── examples ├── hello │ ├── index-local.html │ └── index.html ├── index.html └── todomvc-angular │ ├── .gitignore │ ├── index.html │ ├── js │ ├── app.js │ ├── controllers │ │ └── todoCtrl.js │ └── directives │ │ ├── todoBlur.js │ │ ├── todoEscape.js │ │ └── todoFocus.js │ ├── node_modules │ ├── angular │ │ └── angular.js │ ├── todomvc-app-css │ │ └── index.css │ └── todomvc-common │ │ ├── base.css │ │ └── base.js │ ├── package.json │ └── readme.md ├── index.js ├── package.json ├── scripts ├── adapter-store.js ├── adapter.js ├── authentication-error.js ├── auto-adapter-store.js ├── collection.js ├── config.js ├── config.json ├── db.js ├── delta-db.js ├── delta-error.js ├── disabled-error.js ├── doc.js ├── index.js ├── log.js ├── sender.js ├── socket.js └── utils.js └── test ├── browser-coverage ├── index.html ├── phantom-hooks.js ├── server.js └── test.js ├── browser ├── index.html ├── index.js ├── sauce-results-updater.js ├── server.js ├── test.js └── webrunner.js ├── config.json ├── index.js ├── node └── index.js ├── spec ├── adapter.js ├── client.js ├── config.js ├── db │ ├── index.js │ ├── mock-socket.js │ ├── with-socket.js │ └── without-socket.js ├── delta-db.js ├── doc.js ├── events.js ├── index.js ├── multiple.js ├── persist.js ├── sender.js ├── socket.js └── utils.js └── utils.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | coverage 3 | dist 4 | /node_modules 5 | test/browser/bundle.js 6 | test/browser-coverage/bundle.js 7 | .DS_Store 8 | *~ 9 | npm-debug.log 10 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | test/browser/bundle.js 2 | test/browser-coverage/bundle.js 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "newcap": true, 6 | "noarg": true, 7 | "sub": true, 8 | "undef": true, 9 | "unused": true, 10 | "eqnull": true, 11 | "browser": true, 12 | "node": true, 13 | "strict": true, 14 | "globalstrict": true, 15 | "white": true, 16 | "indent": 2, 17 | "maxlen": 100, 18 | "globals": { 19 | "process": false, 20 | "global": false, 21 | "require": false, 22 | "console": false, 23 | "describe": false, 24 | "before": false, 25 | "beforeEach": false, 26 | "after": false, 27 | "afterEach": false, 28 | "it": false, 29 | "emit": false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.12" 5 | 6 | script: npm run $COMMAND 7 | 8 | after_script: cat ./coverage/node/lcov.info ./coverage/browser/lcov.info | ./node_modules/coveralls/bin/coveralls.js 9 | 10 | after_failure: 11 | - cat /home/travis/build/delta-db/deltadb/npm-debug.log 12 | 13 | env: 14 | matrix: 15 | - COMMAND=assert-beautified 16 | - COMMAND=node-full-test 17 | - COMMAND=browser-coverage-full-test 18 | 19 | # Saucelabs tests 20 | # 21 | # NOTE: according to http://caniuse.com/#feat=sql-storage, Android 4.1-4.3 should support WebSQL, 22 | # but there appears to be some issues when testing with saucelabs. It appears extra configurations 23 | # are needed to prevent "SECURITY_ERR: DOM Exception 18" exceptions: 24 | # http://stackoverflow.com/questions/16062591. It doesn't appear that these configurations can be 25 | # made in the saucelabs environment so we will not run the IndexedDB tests in Android 4.1-4.3. 26 | - CLIENT="saucelabs:firefox" COMMAND=browser-test 27 | - CLIENT="saucelabs:firefox:34" COMMAND=browser-test 28 | - CLIENT="saucelabs:chrome" COMMAND=browser-test 29 | - CLIENT="saucelabs:internet explorer" COMMAND=browser-test 30 | - CLIENT="saucelabs:internet explorer:10" COMMAND=browser-test 31 | - CLIENT="saucelabs:internet explorer:9" COMMAND=browser-test 32 | - CLIENT="saucelabs:microsoftedge" COMMAND=browser-test 33 | - CLIENT="saucelabs:safari:9" COMMAND=browser-test 34 | - CLIENT="saucelabs:safari:8" COMMAND=browser-test 35 | - CLIENT="saucelabs:safari:7" COMMAND=browser-test 36 | - CLIENT="saucelabs:iphone:7.1" COMMAND=browser-test 37 | - CLIENT="saucelabs:iphone:8.4" COMMAND=browser-test 38 | - CLIENT="saucelabs:android:4.1" NOINDEXEDDB=true COMMAND=browser-test 39 | - CLIENT="saucelabs:android:4.2" NOINDEXEDDB=true COMMAND=browser-test 40 | - CLIENT="saucelabs:android:4.3" NOINDEXEDDB=true COMMAND=browser-test 41 | - CLIENT="saucelabs:android:4.4" COMMAND=browser-test 42 | - CLIENT="saucelabs:android:5.1" COMMAND=browser-test 43 | 44 | # NOTE: there is currently no construct that allows us to encrypt the SAUCE_USERNAME and 45 | # SAUCE_ACCESS_KEY while also allowing saucelabs testing in forked projects. See 46 | # https://github.com/travis-ci/travis-ci/issues/1946 and 47 | # https://github.com/angular/angular.js/issues/5596 for more information. 48 | global: 49 | - SAUCE_USERNAME=deltadb-user 50 | - SAUCE_ACCESS_KEY=f74addf5-f68b-4607-8005-6a1de33a3228 51 | 52 | branches: 53 | only: 54 | - master 55 | - /^pull*$/ 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ==== 3 | 4 | Committing Changes 5 | --- 6 | [Commit Message Format](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit) 7 | 8 | npm run coverage 9 | npm run beautify 10 | git add -A 11 | git commit -m msg 12 | git push 13 | 14 | 15 | Updating Dependencies 16 | --- 17 | This requires having david installed globally, which is already handled by our vagrant setup. 18 | 19 | david update 20 | 21 | 22 | Building 23 | --- 24 | 25 | npm run build 26 | 27 | 28 | Publishing to npm/bowser 29 | --- 30 | 31 | First, make sure that you have previously issued `npm adduser`. Then: 32 | 33 | tin -v VERSION 34 | git diff # check that only version changed 35 | npm run build-and-publish 36 | 37 | 38 | Updating gh-pages 39 | --- 40 | 41 | git checkout gh-pages 42 | git merge master 43 | git push origin gh-pages 44 | git checkout master 45 | 46 | 47 | Run all local tests 48 | --- 49 | 50 | npm run test 51 | 52 | 53 | Run single node test 54 | --- 55 | 56 | node_modules/mocha/bin/mocha -g regex test 57 | 58 | 59 | Run subset of tests and analyze coverage 60 | --- 61 | 62 | node_modules/istanbul/lib/cli.js cover _mocha -- -g regex test 63 | 64 | 65 | Debugging Tests Using Node Inspector 66 | --- 67 | 68 | $ node-inspector # leave this running in this window 69 | Use *Chrome* to visit http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5858 70 | $ mocha -g regex test/index.js --debug-brk 71 | 72 | 73 | Run tests in a browser 74 | --- 75 | 76 | $ npm run browser-server 77 | Use browser to visit http://127.0.0.1:8001/test/browser/index.html 78 | 79 | 80 | Run Saucelabs Tests In a Specific Browser 81 | --- 82 | 83 | $ CLIENT="saucelabs:internet explorer:9" SAUCE_USERNAME=deltadb-user 84 | SAUCE_ACCESS_KEY=f74addf5-f68b-4607-8005-6a1de33a3228 npm run browser-test 85 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | Examples 2 | ==== 3 | 4 | Note: 5 | --- 6 | 7 | The examples are currently configured to work with a remote instance of deltadb-server. We recommend that you start with this configuration, but you can also run the examples against a [local deltadb-server instance](https://github.com/delta-db/deltadb-server/blob/master/INSTALL.md). 8 | 9 | 10 | Launch Web Server & View Examples 11 | --- 12 | 13 | $ npm run browser-server # Runs a http server for serving the example 14 | Visit http://127.0.0.1:8001/examples 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DeltaDB deltadb [![Build Status](https://travis-ci.org/delta-db/deltadb.svg)](https://travis-ci.org/delta-db/deltadb) [![Coverage Status](https://coveralls.io/repos/delta-db/deltadb/badge.svg?branch=master&service=github)](https://coveralls.io/github/delta-db/deltadb?branch=master) [![Dependency Status](https://david-dm.org/delta-db/deltadb.svg)](https://david-dm.org/delta-db/deltadb) 2 | === 3 | [![Selenium Test Status](https://saucelabs.com/browser-matrix/deltadb-user.svg)](https://saucelabs.com/u/deltadb-user) 4 | 5 | DeltaDB is an offline-first database designed to talk directly to clients and works great offline and online. 6 | 7 | 8 | ** Update: Suspending Development of DeltaDB ** 9 | --- 10 | 11 | I have decided to suspend development of DeltaDB for the following reasons: 12 | * Recent enhancements to both PouchDB and CouchDB have made [PouchDB initial replication much faster](https://medium.com/@redgeoff/is-pouchdb-fast-enough-for-my-app-5e36d28a831f#.o7zmldh1t) 13 | * After more analysis of the last-write-wins resolution policy, I am left feeling that the last-write-wins resolution policy is mostly good for real-time systems that are always online. In DeltaDB, this last-write-wins policy results in a [Reasonable Ordering](https://github.com/delta-db/deltadb-server/blob/master/NOTES.md#reasonable-ordering). Moreover, the last-write-wins policy is nice when starting a new project as it is automatic, but other conflict resolution policies that force the user to manually resolve the conflict, like CouchDB’s revision protocol, have become more of the standard in the offline-first world. 14 | * Building a DB that scales and is distributed over many nodes, takes a lot of work. I considered some of the necessary details when initially designing DeltaDB, but have only scratched the surface of what needs to be done. There are other DBs, like CouchDB 2.0, that have nearly solved these problems and CouchDB has been in development since 2005. 15 | 16 | 17 | Live Demos 18 | --- 19 | 20 | * [todomvc-angular](http://delta-db.github.io/deltadb/examples/todomvc-angular) - a todo app. For fun, open it in 2 different browser windows and watch the todos change in the 2nd window when you change the todos in the 1st window. 21 | 22 | * [hello](http://codepen.io/redgeoff/pen/vLKYzN?editors=100) - a simple hello world example with code 23 | 24 | 25 | [Getting Started With DeltaDB](https://medium.com/@redgeoff/getting-started-with-deltadb-137359111282#.tciuz7o6b) 26 | --- 27 | 28 | Check out the [Getting Started With DeltaDB](https://medium.com/@redgeoff/getting-started-with-deltadb-137359111282#.tciuz7o6b) tutorial 29 | 30 | 31 | Main Principles 32 | --- 33 | 34 | * Written in JavaScript 35 | * Framework agnostic 36 | * Works the same whether the client is offline or online 37 | * NoSQL database that works in your browser and automatically syncs with the database cluster 38 | * Stores all data as a series of deltas, which allows for smooth collaborative experiences even in frequently offline scenarios. 39 | * Uses a simple last-write-wins conflict resolution policy and is eventually consistent 40 | * Uses a homegrown ORM to speak to underlying SQL databases. (Support for underlying NoSQL databases will be added) 41 | * Is fast. Clients push their deltas on to the server's queue. The server processes the queue separately and partitions the data so that clients can retrieve all recent changes very quickly. 42 | * Implements a granular authentication system that protects databases, collections, docs and attributes 43 | * Is incredibly scalable. Deltas can be segmented by UUID and the cost to add new nodes has a negligible impact on the cluster as handshaking between servers can be done as frequently as desired. 44 | * Highly available. Clients can switch to talk to any node, even if that node hasn't received the latest deltas from another node. 45 | * Fault tolerant by using the concept of a quorum of servers for recording changes 46 | * Data is auto-restored when a client modifies data that was previously deleted 47 | * Uses timestamps to update records so that transactions and their overhead can be avoided 48 | * Thread-safe so that adding more cores will speed up DB reads and writes 49 | 50 | 51 | Why? 52 | --- 53 | 54 | Because it doesn't exist and true support for offline environments needs to be engineered from the ground up 55 | - PouchDB relies on CouchDB and CouchDB is slow to replicate when there are many revisions and it is not optimized for offline setups like db-per-user 56 | - Firebase doesn't work offline and is not open source 57 | - Meteor doesn't work offline 58 | - See [Inspiration](https://github.com/delta-db/deltadb-server/blob/master/INSPIRATION.md) for more info 59 | 60 | 61 | [Installation](https://github.com/delta-db/deltadb-server/blob/master/INSTALL.md) 62 | --- 63 | 64 | 65 | [Examples](EXAMPLES.md) 66 | --- 67 | 68 | 69 | [API](https://github.com/delta-db/deltadb/wiki) 70 | --- 71 | 72 | 73 | [Roadmap](https://github.com/delta-db/deltadb-server/wiki/DeltaDB-Roadmap) 74 | --- 75 | 76 | 77 | [Contributing](CONTRIBUTING.md) 78 | --- 79 | 80 | 81 | [Notes](https://github.com/delta-db/deltadb-server/blob/master/NOTES.md) 82 | --- 83 | 84 | 85 | [Issues](https://github.com/delta-db/deltadb-server/blob/master/ISSUES.md) 86 | --- 87 | 88 | 89 | [Ideas](https://github.com/delta-db/deltadb-server/blob/master/IDEAS.md) 90 | --- 91 | -------------------------------------------------------------------------------- /beautify.json: -------------------------------------------------------------------------------- 1 | { 2 | "indent_size": 2, 3 | "jslint_happy": true, 4 | "wrap_line_length": 100, 5 | "end_with_newline": true 6 | } 7 | -------------------------------------------------------------------------------- /bin/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make sure deps are up to date 4 | # rm -r node_modules 5 | # npm install 6 | 7 | # get current version 8 | VERSION=$(node --eval "console.log(require('./package.json').version);") 9 | 10 | # Increment the version in master 11 | git add -A 12 | git commit -m "$VERSION" 13 | git push origin master 14 | 15 | # Build 16 | git checkout -b build 17 | 18 | npm run build 19 | 20 | # Publish npm release 21 | npm publish 22 | 23 | # Create git tag, which is also the Bower/Github release 24 | git add dist -f 25 | # git add bower.json component.json package.json lib/version-browser.js 26 | git rm -r bin scripts test 27 | 28 | git commit -m "build $VERSION" 29 | 30 | # Tag and push 31 | git tag $VERSION 32 | # TODO: can the following line be changed to git push origin master --tags $VERSION ?? 33 | git push --tags https://github.com/delta-db/deltadb.git $VERSION 34 | 35 | # Cleanup 36 | git checkout master 37 | git branch -D build 38 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deltadb", 3 | "version": "0.0.11", 4 | "description": "DeltaDB offline-first database", 5 | "main": "dist/deltadb.min.js", 6 | "homepage": "https://github.com/delta-db/deltadb", 7 | "authors": [ 8 | "Geoffrey Cox" 9 | ], 10 | "keywords": [ 11 | "deltadb", 12 | "offline-first", 13 | "database", 14 | "javascript", 15 | "js" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "npm-debug.log" 24 | ], 25 | "devDependencies": {}, 26 | "appPath": "app", 27 | "dependencies": {} 28 | } 29 | -------------------------------------------------------------------------------- /examples/hello/index-local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 |

14 | DeltaDB says 15 |

16 | 17 |

18 | DeltaDB says 19 |

20 | 21 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/hello/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 |

14 | DeltaDB says 15 |

16 | 17 |

18 | DeltaDB says 19 |

20 | 21 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 |

Examples

2 | 3 | 7 | -------------------------------------------------------------------------------- /examples/todomvc-angular/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/angular/* 2 | !node_modules/angular/angular.js 3 | 4 | node_modules/todomvc-app-css/* 5 | !node_modules/todomvc-app-css/index.css 6 | 7 | node_modules/todomvc-common/* 8 | !node_modules/todomvc-common/base.css 9 | !node_modules/todomvc-common/base.js 10 | -------------------------------------------------------------------------------- /examples/todomvc-angular/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DeltaDB & AngularJS • TodoMVC 6 | 7 | 8 | 9 | 10 | 11 |
12 | 18 |
19 | 20 | 21 | 33 |
34 | 51 |
52 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/todomvc-angular/js/app.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | /*jshint unused:false */ 3 | 'use strict'; 4 | 5 | /** 6 | * The main TodoMVC app module 7 | * 8 | * @type {angular.Module} 9 | */ 10 | var todomvc = angular.module('todomvc', []); 11 | 12 | todomvc.filter('todoFilter', function ($location) { 13 | return function (input) { 14 | var filtered = {}; 15 | angular.forEach(input, function (todo, id) { 16 | var path = $location.path(); 17 | if (path === '/active') { 18 | if (!todo.completed) { 19 | filtered[id] = todo; 20 | } 21 | } else if (path === '/completed') { 22 | if (todo.completed) { 23 | filtered[id] = todo; 24 | } 25 | } else { 26 | filtered[id] = todo; 27 | } 28 | }); 29 | return filtered; 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /examples/todomvc-angular/js/controllers/todoCtrl.js: -------------------------------------------------------------------------------- 1 | 2 | /*global todomvc, angular, DeltaDB */ 3 | 'use strict'; 4 | 5 | /** 6 | * The main controller for the app. The controller: 7 | * - retrieves and persists the model via DeltaDB 8 | * - exposes the model to the template and provides event handlers 9 | */ 10 | todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, $timeout) { 11 | 12 | var db = new DeltaDB('todos-db', 'https://delta-dev.deltadb.io'); 13 | 14 | // Uncomment to use locally 15 | // var db = new DeltaDB('todos-db', 'http://localhost:8090'); 16 | 17 | var todos = db.col('todos'); 18 | 19 | $scope.todos = []; 20 | $scope.newTodo = ''; 21 | $scope.editedTodo = null; 22 | 23 | var pushTodo = function (todo) { 24 | $scope.todos.push(todo.get()); 25 | $scope.$apply(); // update UI 26 | }; 27 | 28 | todos.on('doc:create', function (todo) { 29 | // Doc was created so add to array 30 | pushTodo(todo); 31 | }); 32 | 33 | var findIndex = function (id) { 34 | var index = null; 35 | $scope.todos.forEach(function (todo, i) { 36 | if (todo.$id === id) { 37 | index = i; 38 | } 39 | }); 40 | return index; 41 | }; 42 | 43 | var destroyTodo = function (todo) { 44 | var index = findIndex(todo.$id); 45 | if (index !== null) { // found? 46 | $scope.todos.splice(index, 1); 47 | } 48 | }; 49 | 50 | todos.on('doc:update', function (todo) { 51 | var index = findIndex(todo.id()); 52 | if (index !== null) { // found? 53 | $scope.todos[index] = todo.get(); 54 | $scope.$apply(); // update UI 55 | } 56 | }); 57 | 58 | todos.on('doc:destroy', function (todo) { 59 | destroyTodo({ $id: todo.id() }); 60 | $scope.$apply(); 61 | }); 62 | 63 | $scope.$watch('todos', function () { 64 | var total = 0; 65 | var remaining = 0; 66 | 67 | $scope.todos.forEach(function (todo) { 68 | // Skip invalid entries so they don't break the entire app. 69 | if (!todo || !todo.title) { 70 | return; 71 | } 72 | 73 | total++; 74 | if (todo.completed === false) { 75 | remaining++; 76 | } 77 | }); 78 | 79 | $scope.totalCount = total; 80 | $scope.remainingCount = remaining; 81 | $scope.completedCount = total - remaining; 82 | $scope.allChecked = remaining === 0; 83 | }, true); 84 | 85 | $scope.addTodo = function () { 86 | var newTodo = $scope.newTodo.trim(); 87 | if (!newTodo.length) { 88 | return; 89 | } 90 | 91 | var todo = todos.doc({ 92 | title: newTodo, 93 | completed: false 94 | }); 95 | 96 | todo.save(); 97 | 98 | $scope.newTodo = ''; 99 | }; 100 | 101 | $scope.editTodo = function (todo) { 102 | $scope.editedTodo = todo; 103 | $scope.originalTodo = angular.extend({}, $scope.editedTodo); 104 | }; 105 | 106 | $scope.save = function (todo) { 107 | todos.get(todo.$id).then(function (todoDoc) { 108 | return todoDoc.set(todo); 109 | }); 110 | }; 111 | 112 | $scope.doneEditing = function (todo) { 113 | $scope.editedTodo = null; 114 | var title = todo.title.trim(); 115 | if (title) { 116 | $scope.save(todo); 117 | } else { 118 | $scope.removeTodo(todo); 119 | } 120 | }; 121 | 122 | $scope.revertEditing = function (todo) { 123 | todo.title = $scope.originalTodo.title; 124 | $scope.doneEditing(todo); 125 | }; 126 | 127 | $scope.removeTodo = function (todo) { 128 | destroyTodo(todo); 129 | todos.get(todo.$id).then(function (todoDoc) { 130 | return todoDoc.destroy(); 131 | }); 132 | }; 133 | 134 | $scope.clearCompletedTodos = function () { 135 | // Loop in reverse order, instead of using .forEach() as each time we remove an array element via 136 | // splice() we shift the indexes and this can lead to problems. 137 | var len = $scope.todos.length; 138 | for (var i = len - 1; i >= 0; i--) { 139 | if ($scope.todos[i].completed) { 140 | $scope.removeTodo($scope.todos[i]); 141 | } 142 | } 143 | }; 144 | 145 | $scope.markAll = function (allCompleted) { 146 | $scope.todos.forEach(function (todo) { 147 | todo.completed = allCompleted; 148 | $scope.save(todo); 149 | }); 150 | }; 151 | 152 | if ($location.path() === '') { 153 | $location.path('/'); 154 | } 155 | $scope.location = $location; 156 | }); 157 | -------------------------------------------------------------------------------- /examples/todomvc-angular/js/directives/todoBlur.js: -------------------------------------------------------------------------------- 1 | /*global todomvc */ 2 | 'use strict'; 3 | 4 | /** 5 | * Directive that executes an expression when the element it is applied to loses focus 6 | */ 7 | todomvc.directive('todoBlur', function () { 8 | return function (scope, elem, attrs) { 9 | elem.bind('blur', function () { 10 | scope.$apply(attrs.todoBlur); 11 | }); 12 | 13 | scope.$on('$destroy', function () { 14 | elem.unbind('blur'); 15 | }); 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /examples/todomvc-angular/js/directives/todoEscape.js: -------------------------------------------------------------------------------- 1 | /*global todomvc */ 2 | 'use strict'; 3 | 4 | /** 5 | * Directive that executes an expression when the element it is applied to gets 6 | * an `escape` keydown event. 7 | */ 8 | todomvc.directive('todoBlur', function () { 9 | var ESCAPE_KEY = 27; 10 | return function (scope, elem, attrs) { 11 | elem.bind('keydown', function (event) { 12 | if (event.keyCode === ESCAPE_KEY) { 13 | scope.$apply(attrs.todoEscape); 14 | } 15 | }); 16 | 17 | scope.$on('$destroy', function () { 18 | elem.unbind('keydown'); 19 | }); 20 | }; 21 | }); 22 | -------------------------------------------------------------------------------- /examples/todomvc-angular/js/directives/todoFocus.js: -------------------------------------------------------------------------------- 1 | /*global todomvc */ 2 | 'use strict'; 3 | 4 | /** 5 | * Directive that places focus on the element it is applied to when the expression it binds to evaluates to true 6 | */ 7 | todomvc.directive('todoFocus', function todoFocus($timeout) { 8 | return function (scope, elem, attrs) { 9 | scope.$watch(attrs.todoFocus, function (newVal) { 10 | if (newVal) { 11 | $timeout(function () { 12 | elem[0].focus(); 13 | }, 0, false); 14 | } 15 | }); 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /examples/todomvc-angular/node_modules/todomvc-app-css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | #todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | #todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | #todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | #todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | #todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | #new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | #new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | #main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | #toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | #toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | #toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | #todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | #todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | #todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | #todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | #todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | #todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | #todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | #todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | #todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | #todo-list li label { 200 | white-space: pre; 201 | word-break: break-word; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | #todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | #todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | #todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | #todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | #todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | #todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | #todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | #footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | #footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | #todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | #todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | #filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | #filters li { 291 | display: inline; 292 | } 293 | 294 | #filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | #filters li a.selected, 304 | #filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | #filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | #clear-completed, 313 | html #clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | #clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | #info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | #info p { 335 | line-height: 1; 336 | } 337 | 338 | #info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | #info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | #toggle-all, 354 | #todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | #todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | #toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | #footer { 372 | height: 50px; 373 | } 374 | 375 | #filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /examples/todomvc-angular/node_modules/todomvc-common/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /examples/todomvc-angular/node_modules/todomvc-common/base.js: -------------------------------------------------------------------------------- 1 | /* global _ */ 2 | (function () { 3 | 'use strict'; 4 | 5 | /* jshint ignore:start */ 6 | // Underscore's Template Module 7 | // Courtesy of underscorejs.org 8 | var _ = (function (_) { 9 | _.defaults = function (object) { 10 | if (!object) { 11 | return object; 12 | } 13 | for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { 14 | var iterable = arguments[argsIndex]; 15 | if (iterable) { 16 | for (var key in iterable) { 17 | if (object[key] == null) { 18 | object[key] = iterable[key]; 19 | } 20 | } 21 | } 22 | } 23 | return object; 24 | } 25 | 26 | // By default, Underscore uses ERB-style template delimiters, change the 27 | // following template settings to use alternative delimiters. 28 | _.templateSettings = { 29 | evaluate : /<%([\s\S]+?)%>/g, 30 | interpolate : /<%=([\s\S]+?)%>/g, 31 | escape : /<%-([\s\S]+?)%>/g 32 | }; 33 | 34 | // When customizing `templateSettings`, if you don't want to define an 35 | // interpolation, evaluation or escaping regex, we need one that is 36 | // guaranteed not to match. 37 | var noMatch = /(.)^/; 38 | 39 | // Certain characters need to be escaped so that they can be put into a 40 | // string literal. 41 | var escapes = { 42 | "'": "'", 43 | '\\': '\\', 44 | '\r': 'r', 45 | '\n': 'n', 46 | '\t': 't', 47 | '\u2028': 'u2028', 48 | '\u2029': 'u2029' 49 | }; 50 | 51 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 52 | 53 | // JavaScript micro-templating, similar to John Resig's implementation. 54 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 55 | // and correctly escapes quotes within interpolated code. 56 | _.template = function(text, data, settings) { 57 | var render; 58 | settings = _.defaults({}, settings, _.templateSettings); 59 | 60 | // Combine delimiters into one regular expression via alternation. 61 | var matcher = new RegExp([ 62 | (settings.escape || noMatch).source, 63 | (settings.interpolate || noMatch).source, 64 | (settings.evaluate || noMatch).source 65 | ].join('|') + '|$', 'g'); 66 | 67 | // Compile the template source, escaping string literals appropriately. 68 | var index = 0; 69 | var source = "__p+='"; 70 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 71 | source += text.slice(index, offset) 72 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 73 | 74 | if (escape) { 75 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 76 | } 77 | if (interpolate) { 78 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 79 | } 80 | if (evaluate) { 81 | source += "';\n" + evaluate + "\n__p+='"; 82 | } 83 | index = offset + match.length; 84 | return match; 85 | }); 86 | source += "';\n"; 87 | 88 | // If a variable is not specified, place data values in local scope. 89 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 90 | 91 | source = "var __t,__p='',__j=Array.prototype.join," + 92 | "print=function(){__p+=__j.call(arguments,'');};\n" + 93 | source + "return __p;\n"; 94 | 95 | try { 96 | render = new Function(settings.variable || 'obj', '_', source); 97 | } catch (e) { 98 | e.source = source; 99 | throw e; 100 | } 101 | 102 | if (data) return render(data, _); 103 | var template = function(data) { 104 | return render.call(this, data, _); 105 | }; 106 | 107 | // Provide the compiled function source as a convenience for precompilation. 108 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 109 | 110 | return template; 111 | }; 112 | 113 | return _; 114 | })({}); 115 | 116 | if (location.hostname === 'todomvc.com') { 117 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 118 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 119 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 120 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 121 | ga('create', 'UA-31081062-1', 'auto'); 122 | ga('send', 'pageview'); 123 | } 124 | /* jshint ignore:end */ 125 | 126 | function redirect() { 127 | if (location.hostname === 'tastejs.github.io') { 128 | location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); 129 | } 130 | } 131 | 132 | function findRoot() { 133 | var base = location.href.indexOf('examples/'); 134 | return location.href.substr(0, base); 135 | } 136 | 137 | function getFile(file, callback) { 138 | if (!location.host) { 139 | return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.'); 140 | } 141 | 142 | var xhr = new XMLHttpRequest(); 143 | 144 | xhr.open('GET', findRoot() + file, true); 145 | xhr.send(); 146 | 147 | xhr.onload = function () { 148 | if (xhr.status === 200 && callback) { 149 | callback(xhr.responseText); 150 | } 151 | }; 152 | } 153 | 154 | function Learn(learnJSON, config) { 155 | if (!(this instanceof Learn)) { 156 | return new Learn(learnJSON, config); 157 | } 158 | 159 | var template, framework; 160 | 161 | if (typeof learnJSON !== 'object') { 162 | try { 163 | learnJSON = JSON.parse(learnJSON); 164 | } catch (e) { 165 | return; 166 | } 167 | } 168 | 169 | if (config) { 170 | template = config.template; 171 | framework = config.framework; 172 | } 173 | 174 | if (!template && learnJSON.templates) { 175 | template = learnJSON.templates.todomvc; 176 | } 177 | 178 | if (!framework && document.querySelector('[data-framework]')) { 179 | framework = document.querySelector('[data-framework]').dataset.framework; 180 | } 181 | 182 | this.template = template; 183 | 184 | if (learnJSON.backend) { 185 | this.frameworkJSON = learnJSON.backend; 186 | this.frameworkJSON.issueLabel = framework; 187 | this.append({ 188 | backend: true 189 | }); 190 | } else if (learnJSON[framework]) { 191 | this.frameworkJSON = learnJSON[framework]; 192 | this.frameworkJSON.issueLabel = framework; 193 | this.append(); 194 | } 195 | 196 | this.fetchIssueCount(); 197 | } 198 | 199 | Learn.prototype.append = function (opts) { 200 | var aside = document.createElement('aside'); 201 | aside.innerHTML = _.template(this.template, this.frameworkJSON); 202 | aside.className = 'learn'; 203 | 204 | if (opts && opts.backend) { 205 | // Remove demo link 206 | var sourceLinks = aside.querySelector('.source-links'); 207 | var heading = sourceLinks.firstElementChild; 208 | var sourceLink = sourceLinks.lastElementChild; 209 | // Correct link path 210 | var href = sourceLink.getAttribute('href'); 211 | sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http'))); 212 | sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML; 213 | } else { 214 | // Localize demo links 215 | var demoLinks = aside.querySelectorAll('.demo-link'); 216 | Array.prototype.forEach.call(demoLinks, function (demoLink) { 217 | if (demoLink.getAttribute('href').substr(0, 4) !== 'http') { 218 | demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href')); 219 | } 220 | }); 221 | } 222 | 223 | document.body.className = (document.body.className + ' learn-bar').trim(); 224 | document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); 225 | }; 226 | 227 | Learn.prototype.fetchIssueCount = function () { 228 | var issueLink = document.getElementById('issue-count-link'); 229 | if (issueLink) { 230 | var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos'); 231 | var xhr = new XMLHttpRequest(); 232 | xhr.open('GET', url, true); 233 | xhr.onload = function (e) { 234 | var parsedResponse = JSON.parse(e.target.responseText); 235 | if (parsedResponse instanceof Array) { 236 | var count = parsedResponse.length; 237 | if (count !== 0) { 238 | issueLink.innerHTML = 'This app has ' + count + ' open issues'; 239 | document.getElementById('issue-count').style.display = 'inline'; 240 | } 241 | } 242 | }; 243 | xhr.send(); 244 | } 245 | }; 246 | 247 | redirect(); 248 | getFile('learn.json', Learn); 249 | })(); 250 | -------------------------------------------------------------------------------- /examples/todomvc-angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "angular": "^1.3.15", 5 | "todomvc-app-css": "^1.0.1", 6 | "todomvc-common": "^1.0.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/todomvc-angular/readme.md: -------------------------------------------------------------------------------- 1 | TODO 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./scripts'); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deltadb", 3 | "version": "0.0.11", 4 | "description": "DeltaDB offline-first database", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/delta-db/deltadb" 9 | }, 10 | "keywords": [ 11 | "deltadb", 12 | "offline-first", 13 | "database", 14 | "javascript", 15 | "js" 16 | ], 17 | "author": "Geoffrey Cox", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/delta-db/deltadb/issues" 21 | }, 22 | "scripts": { 23 | "assert-beautified": "beautify-proj -i test -c beautify.json -e bundle.js && beautify-proj -i scripts -c beautify.json", 24 | "beautify": "beautify-proj -i test -o . -c beautify.json -e bundle.js && beautify-proj -i scripts -o . -c beautify.json", 25 | "jshint": "jshint -c .jshintrc *.js test scripts", 26 | "node-test": "istanbul test --dir coverage/node ./node_modules/mocha/bin/_mocha test/node/index.js", 27 | "node-full-test": "npm run jshint && npm run node-test --coverage && istanbul check-coverage --lines 100 --function 100 --statements 100 --branches 100", 28 | "browser-server": "./test/browser/server.js", 29 | "browser-test": "./test/browser/test.js", 30 | "browser-test-firefox": "npm run jshint && CLIENT=selenium:firefox npm run browser-test", 31 | "browser-test-chrome": "npm run jshint && CLIENT=selenium:chrome npm run browser-test", 32 | "browser-test-phantomjs": "npm run jshint && CLIENT=selenium:phantomjs npm run browser-test", 33 | "browser-coverage-build": "browserify -t [ browserify-istanbul --ignore **/node_modules/** ] ./test/browser/index.js -o test/browser-coverage/bundle.js -d", 34 | "browser-coverage-server": "./test/browser-coverage/server.js", 35 | "browser-coverage-test": "./test/browser-coverage/test.js", 36 | "browser-coverage-report": "istanbul report --dir coverage/browser --root coverage/browser lcov", 37 | "browser-coverage-check": "istanbul check-coverage --lines 100 --function 100 --statements 100 --branches 100 coverage/browser/coverage.json", 38 | "browser-coverage-full-test": "npm run jshint && npm run browser-coverage-build && npm run browser-coverage-test && npm run browser-coverage-report && npm run browser-coverage-check", 39 | "test": "npm run assert-beautified && npm run node-full-test && npm run browser-coverage-full-test", 40 | "min": "uglifyjs dist/deltadb.js -mc > dist/deltadb.min.js", 41 | "build": "mkdir -p dist && browserify index.js -s DeltaDB -o dist/deltadb.js && npm run min", 42 | "build-and-publish": "./bin/publish.sh", 43 | "link": "npm link ../deltadb-common-utils && npm link ../deltadb-orm-nosql", 44 | "unlink": "npm unlink deltadb-common-utils && npm unlink deltadb-orm-nosql && npm install" 45 | }, 46 | "dependencies": { 47 | "bluebird": "^3.0.5", 48 | "deltadb-common-utils": "0.0.4", 49 | "deltadb-orm-nosql": "0.0.4", 50 | "events": "^1.1.0", 51 | "inherits": "^2.0.1", 52 | "socket.io-client": "^1.4.4" 53 | }, 54 | "devDependencies": { 55 | "beautify-proj": "0.0.4", 56 | "browserify": "^12.0.2", 57 | "browserify-istanbul": "^0.2.1", 58 | "bufferutil": "^1.2.1", 59 | "chai": "^3.4.1", 60 | "chai-as-promised": "^5.2.0", 61 | "coveralls": "^2.11.4", 62 | "es5-shim": "^4.3.1", 63 | "http-server": "^0.8.5", 64 | "istanbul": "^0.4.0", 65 | "jshint": "^2.8.0", 66 | "mocha": "^2.3.4", 67 | "mocha-phantomjs": "^4.0.2", 68 | "request": "^2.67.0", 69 | "sauce-connect-launcher": "^0.14.0", 70 | "saucelabs": "^1.0.1", 71 | "selenium-standalone": "^4.7.2", 72 | "utf-8-validate": "^1.2.1", 73 | "watchify": "^3.6.1", 74 | "wd": "^0.4.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /scripts/adapter-store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MemAdapter = require('deltadb-orm-nosql/scripts/adapters/mem'); 4 | 5 | /** 6 | * This class provides us with a global way of keeping the store selector abstracted so that we 7 | * don't have to include browser specific implementations, e.g. IndexedDB, in our node tests and 8 | * test coverage. 9 | * 10 | * It also allows us to keep a global instance of the adapter so that multiple clients within the 11 | * same app can share the same adapters, which allows us to synchronize reads/writes from/to the 12 | * adapters. 13 | */ 14 | var AdapterStore = function () {}; 15 | 16 | // TODO: test! 17 | /* istanbul ignore next */ 18 | AdapterStore.prototype.newAdapter = function () { 19 | return new MemAdapter(); 20 | }; 21 | 22 | AdapterStore.prototype.getAdapter = function () { 23 | if (!this._adapter) { 24 | this._adapter = this.newAdapter(); 25 | } 26 | return this._adapter; 27 | }; 28 | 29 | module.exports = new AdapterStore(); 30 | -------------------------------------------------------------------------------- /scripts/adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO: should events be moved to deltadb-orm-nosql layer? 4 | 5 | var inherits = require('inherits'), 6 | MemAdapter = require('deltadb-orm-nosql/scripts/adapters/mem'), 7 | DB = require('./db'), 8 | utils = require('deltadb-common-utils'), 9 | Promise = require('bluebird'), 10 | adapterStore = require('./adapter-store'), 11 | config = require('./config'); 12 | 13 | var Adapter = function (localOnly) { 14 | MemAdapter.apply(this, arguments); // apply parent constructor 15 | this._localOnly = localOnly; 16 | 17 | this._store = adapterStore.getAdapter(); 18 | }; 19 | 20 | // We inherit from MemAdapter so that we can have singular references in memory to items like Docs. 21 | // This in turn allows us to emit and listen for events across different modules. The downside is 22 | // that we end up with data duplicated in both local mem and the store. 23 | 24 | inherits(Adapter, MemAdapter); 25 | 26 | Adapter.prototype._emit = function () { // event, arg1, ... argN 27 | this.emit.apply(this, utils.toArgsArray(arguments)); 28 | }; 29 | 30 | Adapter.prototype.uuid = function () { 31 | return utils.uuid(); 32 | }; 33 | 34 | Adapter.prototype._dbStore = function (name, alias) { 35 | return this._store.db({ 36 | db: (alias ? alias : config.vals.dbNamePrefix + name) 37 | }); 38 | }; 39 | 40 | Adapter.prototype.db = function (opts) { 41 | var db = this._dbs[opts.db]; 42 | if (db) { // exists? 43 | return db; 44 | } else { 45 | 46 | if (typeof opts.local === 'undefined') { 47 | opts.local = this._localOnly; 48 | } 49 | 50 | var dbStore = null; 51 | if (typeof opts.store === 'undefined') { 52 | dbStore = this._dbStore(opts.db, opts.alias); 53 | } else { 54 | dbStore = opts.store; 55 | } 56 | 57 | var filter = typeof opts.filter === 'undefined' ? true : opts.filter; 58 | 59 | db = new DB(opts.db, this, opts.url, opts.local, !filter, opts.username, opts.password); 60 | db._import(dbStore); 61 | this._dbs[opts.db] = db; 62 | this.emit('db:create', db); 63 | return db; 64 | } 65 | }; 66 | 67 | Adapter.prototype._unregister = function (dbName) { 68 | delete this._dbs[dbName]; 69 | return Promise.resolve(); 70 | }; 71 | 72 | module.exports = Adapter; 73 | -------------------------------------------------------------------------------- /scripts/authentication-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DeltaError = require('./delta-error'), 4 | inherits = require('inherits'); 5 | 6 | var AuthenticationError = function (message) { 7 | this.name = 'AuthenticationError'; 8 | this.message = message; 9 | }; 10 | 11 | inherits(AuthenticationError, DeltaError); 12 | 13 | module.exports = AuthenticationError; 14 | -------------------------------------------------------------------------------- /scripts/auto-adapter-store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var IDBAdapter = require('deltadb-orm-nosql/scripts/adapters/indexed-db'), 4 | MemAdapter = require('deltadb-orm-nosql/scripts/adapters/mem'), 5 | idbUtils = require('deltadb-orm-nosql/scripts/adapters/indexed-db/utils'), 6 | adapterStore = require('./adapter-store'); 7 | 8 | // Create a store based on the availibility of whether we are using a browser or not 9 | adapterStore.newAdapter = function () { 10 | // TODO: test 11 | /* istanbul ignore next */ 12 | if (global.window && idbUtils.indexedDB()) { // in browser and have IndexedDB? 13 | return new IDBAdapter(); 14 | } else { 15 | return new MemAdapter(); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/collection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var inherits = require('inherits'), 4 | Promise = require('bluebird'), 5 | MemCollection = require('deltadb-orm-nosql/scripts/adapters/mem/collection'), 6 | utils = require('deltadb-common-utils'), 7 | clientUtils = require('./utils'), 8 | Doc = require('./doc'), 9 | Cursor = require('deltadb-orm-nosql/scripts/adapters/mem/cursor'); 10 | 11 | var Collection = function ( /* name, db */ ) { 12 | MemCollection.apply(this, arguments); // apply parent constructor 13 | this._initLoaded(); 14 | }; 15 | 16 | inherits(Collection, MemCollection); 17 | 18 | Collection.prototype._initLoaded = function () { 19 | var self = this; 20 | self._loaded = utils.once(self, 'load'); 21 | }; 22 | 23 | Collection.prototype._import = function (store) { 24 | this._store = store; 25 | this._initStore(); 26 | }; 27 | 28 | Collection.prototype._createStore = function () { 29 | this._store = this._db._store.col(this._name); 30 | }; 31 | 32 | Collection.prototype._ensureStore = function () { 33 | var self = this; 34 | // Wait until db is loaded and then create store. We don't need to return _loaded as this 35 | // _ensureStore() is called by the doc which will create the doc store afterwards and then emit 36 | // the 'load' 37 | return self._db._loaded.then(function () { 38 | if (!self._store) { 39 | self._createStore(); 40 | } 41 | return null; // prevent runaway promise warnings 42 | }); 43 | }; 44 | 45 | Collection.prototype._doc = function (data) { 46 | return new Doc(data, this); 47 | }; 48 | 49 | Collection.prototype._initStore = function () { 50 | var self = this, 51 | promises = []; 52 | 53 | var all = self._store.all(function (docStore) { 54 | var data = {}; 55 | data[self._db._idName] = docStore.id(); 56 | var doc = self.doc(data); 57 | doc._import(docStore); 58 | promises.push(doc._loaded); 59 | }); 60 | 61 | // all resolves when we have executed the callback for all docs and Promise.all(promises) resolves 62 | // after all the docs have been loaded. We need to wait for all first so that we have promises 63 | // set. 64 | self._loaded = all.then(function () { 65 | return Promise.all(promises); 66 | }).then(function () { 67 | self.emit('load'); 68 | return null; // prevent runaway promise warning 69 | }); 70 | }; 71 | 72 | Collection.prototype._setChange = function (change) { 73 | var self = this, 74 | doc = null; 75 | return self.get(change.id).then(function (_doc) { 76 | doc = _doc; 77 | if (!doc) { 78 | var data = {}; 79 | data[self._db._idName] = change.id; 80 | doc = self.doc(data); 81 | } 82 | 83 | // TODO: in future, if sequence of changes for same doc then set for all changes and then issue 84 | // a single save? 85 | return doc._setChange(change); 86 | }); 87 | }; 88 | 89 | Collection.prototype._emit = function (evnt) { // evnt, arg1, ... argN 90 | var args = utils.toArgsArray(arguments); 91 | this.emit.apply(this, args); 92 | 93 | this._db._emit.apply(this._db, args); // also bubble up to db layer 94 | 95 | // Prevent infinite recursion 96 | if (evnt !== 'col:create' && evnt !== 'col:update') { 97 | this._emit.apply(this, ['col:update', this]); 98 | } 99 | 100 | if (evnt === 'doc:record') { 101 | this._emit.apply(this, ['col:record', this]); 102 | } 103 | }; 104 | 105 | Collection.prototype._emitColDestroy = function () { 106 | this._emit('col:destroy', this); 107 | }; 108 | 109 | Collection.prototype.destroy = function () { 110 | // Don't actually destroy the col as we need to keep tombstones 111 | this._emitColDestroy(); // TODO: move to common 112 | return Promise.resolve(); 113 | }; 114 | 115 | Collection.prototype.policy = function (policy) { 116 | var doc = this.doc(); 117 | return doc.policy(policy); 118 | }; 119 | 120 | // Shouldn't be called directly as the colName needs to be set properly 121 | Collection.prototype._createUser = function (userUUID, username, password, status) { 122 | var self = this, 123 | id = clientUtils.toDocUUID(userUUID); 124 | return self.get(id).then(function (doc) { 125 | // If we are updating the user, the doc may already exist 126 | if (!doc) { // doc missing? 127 | var data = {}; 128 | data[self._db._idName] = id; 129 | doc = self.doc(data); 130 | } 131 | return doc._createUser(userUUID, username, password, status); 132 | }); 133 | }; 134 | 135 | Collection.prototype._addRole = function (userUUID, roleName) { 136 | var doc = this.doc(); 137 | return doc._addRole(userUUID, roleName); 138 | }; 139 | 140 | Collection.prototype._removeRole = function (userUUID, roleName) { 141 | var doc = this.doc(); 142 | return doc._removeRole(userUUID, roleName); 143 | }; 144 | 145 | Collection.prototype._createDatabase = function (dbName) { 146 | return this.doc()._createDatabase(dbName); 147 | }; 148 | 149 | Collection.prototype._destroyDatabase = function (dbName) { 150 | return this.doc()._destroyDatabase(dbName); 151 | }; 152 | 153 | Collection.prototype.find = function (query, callback, destroyed) { 154 | return this._find(query, callback, new Cursor(this._docs, this, destroyed)); 155 | }; 156 | 157 | Collection.prototype._localChanges = function (retryAfter, returnSent, limit, nContainer) { 158 | var changes = [], 159 | more = false; 160 | 161 | return this.find(null, function (doc) { 162 | var _changes = doc._localChanges(retryAfter, returnSent, limit, nContainer); 163 | changes = changes.concat(_changes.changes); 164 | 165 | if (_changes.more) { 166 | more = true; 167 | } 168 | 169 | // Have we already retrieved the max batch size? Then exit find loop early 170 | if (limit && nContainer.n === limit) { 171 | // We need to set more here as we may not have reached our limit within a single doc, but we 172 | // do when we consider all the docs in this collection. 173 | more = true; 174 | return false; 175 | } 176 | }, true).then(function () { 177 | return { 178 | changes: changes, 179 | more: more 180 | }; 181 | }); 182 | }; 183 | 184 | module.exports = Collection; 185 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var vals = require('./config.json'); 4 | 5 | var Config = function () { 6 | this.vals = vals; 7 | }; 8 | 9 | Config.prototype.url = function () { 10 | var url = this.vals.url; 11 | return url.scheme + '://' + url.host + (url.port ? ':' + url.port : ''); 12 | }; 13 | 14 | module.exports = new Config(); 15 | -------------------------------------------------------------------------------- /scripts/config.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "dbNamePrefix": "delta_", 4 | 5 | "systemDBNamePrefix": "delta_sys_", 6 | 7 | "url": { 8 | "scheme": "http", 9 | "host": "localhost", 10 | "port": 8090 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /scripts/db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO: later, db should be passed in a constructor so that it doesn't have to be passed to sync?? 4 | 5 | // TODO: move events to deltadb-orm-nosql layer? 6 | 7 | var inherits = require('inherits'), 8 | Promise = require('bluebird'), 9 | utils = require('deltadb-common-utils'), 10 | MemDB = require('deltadb-orm-nosql/scripts/adapters/mem/db'), 11 | Doc = require('./doc'), 12 | Collection = require('./collection'), 13 | clientUtils = require('./utils'), 14 | Sender = require('./sender'), 15 | log = require('./log'), 16 | config = require('./config'), 17 | Socket = require('./socket'); 18 | 19 | // TODO: shouldn't password be a char array? 20 | var DB = function (name, adapter, url, localOnly, noFilters, username, password, hashedPassword) { 21 | this._id = Math.floor(Math.random() * 10000000); // used to debug multiple connections 22 | 23 | name = clientUtils.escapeDBName(name); 24 | 25 | MemDB.apply(this, arguments); // apply parent constructor 26 | 27 | this._socket = new DB._SocketClass(); 28 | 29 | this._batchSize = DB.DEFAULT_BATCH_SIZE; 30 | this._cols = {}; 31 | this._retryAfterMSecs = 180000; 32 | this._recorded = false; 33 | this._sender = new Sender(this); 34 | this._url = url ? url : config.url(); 35 | this._username = username; 36 | this._password = password; 37 | this._hashedPassword = hashedPassword; 38 | 39 | this._prepInitDone(); 40 | 41 | this._initStoreLoaded(); 42 | 43 | this._storesImported = false; 44 | 45 | this._noFilters = noFilters; 46 | 47 | this._localOnly = localOnly; 48 | 49 | if (!localOnly) { 50 | // This is registered immediately so that do not listen for a change after a change has already 51 | this._registerSenderListener(); 52 | 53 | this._connectWhenReady(); 54 | } 55 | 56 | }; 57 | 58 | inherits(DB, MemDB); 59 | 60 | // Used for mocking the socket 61 | DB._SocketClass = Socket; 62 | 63 | DB.PROPS_COL_NAME = '$props'; 64 | 65 | DB.PROPS_DOC_ID = 'props'; 66 | 67 | // Use a version # to allow for patching of the store between versions when the schema changes 68 | DB.VERSION = 1; 69 | 70 | // The max number of deltas to send in a batch 71 | DB.DEFAULT_BATCH_SIZE = 100; 72 | 73 | DB.prototype._prepInitDone = function () { 74 | // This promise ensures that the we have already received init-done from the server 75 | this._initDone = utils.once(this, 'init-done'); 76 | }; 77 | 78 | DB.prototype._initStoreLoaded = function () { 79 | // This promise ensures that the store is ready before we use it. 80 | this._storeLoaded = utils.once(this, 'load'); // TODO: are _storeLoaded and _loaded both needed? 81 | }; 82 | 83 | // TODO: can this be cleaned up? Do we really need _storeLoaded, _loaded and _ready? 84 | DB.prototype._ready = function () { 85 | var self = this; 86 | return self._storeLoaded.then(function () { 87 | return self._loaded; 88 | }); 89 | }; 90 | 91 | DB.prototype._import = function (store) { 92 | var self = this; 93 | 94 | self._store = store; 95 | 96 | // Make sure the store is ready, e.g. opened, before init 97 | return self._store._loaded.then(function () { 98 | return self._initStore(); 99 | }); 100 | }; 101 | 102 | /** 103 | * Flows: 104 | * - Data loaded from store, e.g. from IndexedDB. After which the 'load' event is emitted 105 | * - When registering a doc: 106 | * - Wait for until DB has finished loading store so that we don't create a duplicate 107 | * - Get or create col store 108 | * - Get or create doc store 109 | */ 110 | DB.prototype._initStore = function () { 111 | var self = this, 112 | promises = [], 113 | loadingProps = false; 114 | 115 | self._store.all(function (colStore) { 116 | if (colStore._name === DB.PROPS_COL_NAME) { 117 | loadingProps = true; 118 | promises.push(self._initProps(colStore)); 119 | } else { 120 | var col = self._col(colStore._name); 121 | col._import(colStore); 122 | promises.push(col._loaded); 123 | } 124 | }); 125 | 126 | // All the stores have been imported 127 | self._storesImported = true; 128 | 129 | return Promise.all(promises).then(function () { 130 | if (!loadingProps) { // no props? nothing in store 131 | return self._initProps(); 132 | } 133 | }).then(function () { 134 | self.emit('load'); 135 | return null; // prevent runaway promise warnings 136 | }); 137 | }; 138 | 139 | DB.prototype._initProps = function (colStore) { 140 | var self = this; 141 | 142 | if (colStore) { // reloading? 143 | self._propCol = colStore; 144 | } else { 145 | self._propCol = self._store.col(DB.PROPS_COL_NAME); 146 | } 147 | 148 | return self._propCol.get(DB.PROPS_DOC_ID).then(function (doc) { 149 | if (doc) { // found? 150 | self._props = doc; 151 | } else { 152 | var props = {}; 153 | props[self._store._idName] = DB.PROPS_DOC_ID; 154 | self._props = self._propCol.doc(props); 155 | return self._props.set({ 156 | since: null, 157 | version: DB.VERSION 158 | }); 159 | } 160 | }); 161 | }; 162 | 163 | // TODO: make sure user-defined colName doesn't start with $ 164 | // TODO: make .col() not be promise any more? Works for indexedb and mongo adapters? 165 | DB.prototype._col = function (name) { 166 | if (this._cols[name]) { 167 | return this._cols[name]; 168 | } else { 169 | var col = new Collection(name, this); 170 | this._cols[name] = col; 171 | this._emitColCreate(col); 172 | 173 | return col; 174 | } 175 | }; 176 | 177 | DB.prototype.col = function (name) { 178 | return this._col(name, true); 179 | }; 180 | 181 | DB.prototype._emitColCreate = function (col) { 182 | this.emit('col:create', col); 183 | this._adapter._emit('col:create', col); // also bubble up to adapter layer 184 | }; 185 | 186 | DB.prototype._localChanges = function (retryAfter, returnSent, limit) { 187 | var chain = Promise.resolve(), 188 | changes = [], 189 | more = false; 190 | 191 | // Use a container so that other methods can modify the value 192 | var nContainer = { 193 | n: 0 194 | }; 195 | 196 | this.all(function (col) { 197 | // We need to process changes sequentially so that we can reliably limit the total number of 198 | // deltas sent to the server 199 | chain = chain.then(function () { 200 | // Have we processed the max batch size? Then stop 201 | if (!limit || nContainer.n < limit) { 202 | return col._localChanges(retryAfter, returnSent, limit, nContainer).then(function ( 203 | _changes) { 204 | changes = changes.concat(_changes.changes); 205 | 206 | // More changes that we won't fit in this batch? 207 | if (_changes.more) { 208 | more = true; 209 | } 210 | }); 211 | } else { 212 | // More changes that we won't fit in this batch? We need to set more here as we may not have 213 | // reached our limit within a single col, but we do when we consider all the cols in this 214 | // db. 215 | more = true; 216 | } 217 | }); 218 | }); 219 | 220 | return chain.then(function () { 221 | return { 222 | changes: changes, 223 | more: more 224 | }; 225 | }); 226 | }; 227 | 228 | DB.prototype._setChange = function (change) { 229 | var col = this.col(change.col); 230 | return col._setChange(change); 231 | }; 232 | 233 | // Process changes sequentially or else duplicate collections can be created 234 | DB.prototype._setChanges = function (changes) { 235 | var self = this, 236 | chain = Promise.resolve(); 237 | 238 | if (!changes) { 239 | return chain; 240 | } 241 | 242 | changes.forEach(function (change) { 243 | chain = chain.then(function () { 244 | return self._setChange(change); 245 | }); 246 | }); 247 | 248 | return chain; 249 | }; 250 | 251 | // TODO: rename to _sync as shouldn't be called by user 252 | DB.prototype.sync = function (part, quorum) { 253 | var self = this, 254 | newSince = null; 255 | return self._localChanges(self._retryAfterMSecs).then(function (changes) { 256 | return part.queue(changes.changes, quorum); 257 | }).then(function () { 258 | newSince = new Date(); 259 | return self._loaded; // ensure props have been loaded/created first 260 | }).then(function () { 261 | return part.changes(self._props.get('since')); 262 | }).then(function (changes) { 263 | return self._setChanges(changes); 264 | }).then(function () { 265 | return self._props.set({ 266 | since: newSince 267 | }); 268 | }); 269 | }; 270 | 271 | DB.prototype._emit = function () { // event, arg1, ... argN 272 | var args = utils.toArgsArray(arguments); 273 | this.emit.apply(this, args); 274 | this._adapter._emit.apply(this._adapter, args); // also bubble up to adapter layer 275 | 276 | if (!this._recorded && args[0] === 'attr:record') { // not recorded yet? 277 | this.emit('db:record', this); 278 | this._adapter._emit('db:record', this); // also bubble up to adapter layer 279 | this._recorded = true; 280 | } 281 | }; 282 | 283 | DB.prototype.policy = function (colName, policy) { 284 | // Find/create collection and set policy for new doc 285 | var col = this.col(colName); 286 | return col.policy(policy); 287 | }; 288 | 289 | // TODO: shouldn't the password be a byte/char array so that passwords aren't stored in memory in 290 | // their entirety? See 291 | // http://stackoverflow.com/questions/28511970/javascript-security-force-deletion-of-sensitive-data 292 | DB.prototype.createUser = function (userUUID, username, password, status) { 293 | var col = this.col(Doc._userName); 294 | return col._createUser(userUUID, username, password, status); 295 | }; 296 | 297 | // TODO: shouldn't the password be a byte/char array so that passwords aren't stored in memory in 298 | // their entirety? See 299 | // http://stackoverflow.com/questions/28511970/javascript-security-force-deletion-of-sensitive-data 300 | DB.prototype.updateUser = function (userUUID, username, password, status) { 301 | return this.createUser(userUUID, username, password, status); 302 | }; 303 | 304 | // TODO: better to implement "Generator" doc like create/destroy DB? 305 | DB.prototype._resolveAfterRoleCreated = function (userUUID, roleName, originatingDoc, ts) { 306 | return new Promise(function (resolve) { 307 | // When adding a user to a role, the delta is id-less and so we cannot use an id to reconcile 308 | // the local doc. Instead we listen for a new doc on the parent collection and then delete the 309 | // local doc that was used to originate the delta so that we don't attempt to add the user to 310 | // the role again. 311 | 312 | var listener = function (doc) { 313 | var data = doc.get(); 314 | // The same user-role mapping could have been created before so we need to check the timestamp 315 | 316 | // TODO: test 317 | /* istanbul ignore next */ 318 | if (data[clientUtils.ATTR_NAME_ROLE] && 319 | data[clientUtils.ATTR_NAME_ROLE].action === clientUtils.ACTION_ADD && 320 | data[clientUtils.ATTR_NAME_ROLE].userUUID === userUUID && 321 | data[clientUtils.ATTR_NAME_ROLE].roleName === roleName && 322 | (!doc._dat.recordedAt || doc._dat.recordedAt.getTime() >= ts.getTime())) { 323 | 324 | // Remove listener so that we don't listen for other docs 325 | originatingDoc._col.removeListener('doc:record', listener); 326 | 327 | resolve(originatingDoc._destroyLocally()); 328 | } 329 | }; 330 | 331 | originatingDoc._col.on('doc:record', listener); 332 | }); 333 | }; 334 | 335 | // TODO: better to implement "Generator" doc like create/destroy DB? 336 | DB.prototype.addRole = function (userUUID, roleName) { 337 | var self = this, 338 | ts = new Date(), 339 | colName = clientUtils.NAME_PRE_USER_ROLES + userUUID, 340 | col = self.col(colName); 341 | return col._addRole(userUUID, roleName).then(function (doc) { 342 | return self._resolveAfterRoleCreated(userUUID, roleName, doc, ts); 343 | }); 344 | }; 345 | 346 | // TODO: better to implement "Generator" doc like create/destroy DB? 347 | DB.prototype._resolveAfterRoleDestroyed = function (userUUID, roleName, originatingDoc, ts) { 348 | return new Promise(function (resolve) { 349 | // When removing a user's role, the delta is id-less and so we cannot use an id to reconcile 350 | // the local doc. Instead we listen for a new doc on the parent collection and then delete the 351 | // local doc that was used to originate the delta so that we don't attempt to remove the user's 352 | // role again. 353 | 354 | var listener = function (doc) { 355 | var data = doc.get(); 356 | // The same user-role mapping could have been destroyed before so we need to check the 357 | // timestamp 358 | 359 | // TODO: test 360 | /* istanbul ignore next */ 361 | if (data[clientUtils.ATTR_NAME_ROLE] && 362 | data[clientUtils.ATTR_NAME_ROLE].action === clientUtils.ACTION_REMOVE && 363 | data[clientUtils.ATTR_NAME_ROLE].userUUID === userUUID && 364 | data[clientUtils.ATTR_NAME_ROLE].roleName === roleName && 365 | (!doc._dat.recordedAt || doc._dat.recordedAt.getTime() >= ts.getTime())) { 366 | 367 | // Remove listener so that we don't listen for other docs 368 | originatingDoc._col.removeListener('doc:record', listener); 369 | 370 | resolve(originatingDoc._destroyLocally()); 371 | } 372 | }; 373 | 374 | originatingDoc._col.on('doc:record', listener); 375 | }); 376 | }; 377 | 378 | // TODO: better to implement "Generator" doc like create/destroy DB? 379 | DB.prototype.removeRole = function (userUUID, roleName) { 380 | var self = this, 381 | ts = new Date(), 382 | colName = clientUtils.NAME_PRE_USER_ROLES + userUUID, 383 | col = self.col(colName); 384 | return col._removeRole(userUUID, roleName).then(function (doc) { 385 | return self._resolveAfterRoleDestroyed(userUUID, roleName, doc, ts); 386 | }); 387 | }; 388 | 389 | DB.prototype._createDatabase = function (dbName) { 390 | var colName = clientUtils.DB_COLLECTION_NAME; 391 | var col = this.col(colName); 392 | return col._createDatabase(dbName); 393 | }; 394 | 395 | DB.prototype._destroyDatabase = function (dbName) { 396 | var colName = clientUtils.DB_COLLECTION_NAME; 397 | var col = this.col(colName); 398 | return col._destroyDatabase(dbName); 399 | }; 400 | 401 | DB.prototype.destroy = function (keepRemote, keepLocal) { 402 | var self = this, 403 | promise = null; 404 | 405 | if (keepRemote || self._localOnly || self._name === clientUtils.SYSTEM_DB_NAME) { 406 | promise = Promise.resolve(); 407 | } else { 408 | promise = self._destroyDatabaseViaSystem(self._name); 409 | } 410 | 411 | return promise.then(function () { 412 | if (!self._localOnly) { 413 | // Stop listening to the server entirely 414 | return self._disconnect(); 415 | } 416 | }).then(function () { 417 | if (!keepLocal) { 418 | return self._store.destroy(); 419 | } 420 | }).then(function () { 421 | // Is this DB not a system DB and does it have an associated system DB? 422 | if (self._name !== clientUtils.SYSTEM_DB_NAME && self._sysDB) { 423 | // Also destroy the assoicated system DB. Always keep the remote instance 424 | return self._systemDB().destroy(true, keepLocal); 425 | } 426 | }).then(function () { 427 | return self._adapter._unregister(self._name); 428 | }); 429 | }; 430 | 431 | DB.prototype._emitInitMsg = function () { 432 | return { 433 | db: this._name, 434 | since: this._props.get('since'), 435 | filter: this._noFilters ? false : true, 436 | username: this._username, 437 | password: this._password, 438 | hashed: this._hashedPassword 439 | }; 440 | }; 441 | 442 | DB.prototype._emitInit = function () { 443 | var self = this; 444 | return self._ready().then(function () { // ensure props have been loaded/created first 445 | var msg = self._emitInitMsg(); 446 | log.info(self._id + ' sending init ' + JSON.stringify(msg)); 447 | self._socket.emit('init', msg); 448 | return null; // prevent runaway promise warnings 449 | }); 450 | }; 451 | 452 | DB.prototype._emitChanges = function (changes) { 453 | var msg = { 454 | changes: changes 455 | }; 456 | log.info(this._id + ' sending ' + JSON.stringify(msg)); 457 | this._socket.emit('changes', msg); 458 | }; 459 | 460 | DB.prototype._findAndEmitBatchOfChanges = function () { 461 | // If we happen to disconnect when reading _localChanges then we'll rely on the retry to send 462 | // the deltas later 463 | var self = this; 464 | return self._localChanges(self._retryAfterMSecs, null, self._batchSize).then(function (changes) { 465 | // The length could be zero if there is a race condition where two back-to-back changes result 466 | // in the first change emitting all the changes with a single call to _localChanges. 467 | if (changes.changes && changes.changes.length > 0) { 468 | self._emitChanges(changes.changes); 469 | } 470 | 471 | return changes.more; 472 | }); 473 | }; 474 | 475 | DB.prototype._findAndEmitAllChangesInBatches = function () { 476 | // We have to sequentially find the changes so that we can reliably limit their number 477 | var self = this; 478 | return self._findAndEmitBatchOfChanges().then(function (more) { 479 | if (more) { // more changes? 480 | return self._findAndEmitAllChangesInBatches(); 481 | } 482 | }); 483 | }; 484 | 485 | DB.prototype._findAndEmitChanges = function () { 486 | // TODO: keep sync and this fn so that can test w/o socket, right? If so, then better way to reuse 487 | // code? 488 | var self = this; 489 | 490 | // If we aren't connected then wait for reconnect to send changes during init. 491 | if (!self._connected) { 492 | return Promise.resolve(); 493 | } 494 | 495 | return self._ready().then(function () { // ensure props have been loaded/created first 496 | return self._findAndEmitAllChangesInBatches(); 497 | }); 498 | 499 | }; 500 | 501 | DB.prototype._processChanges = function (msg) { 502 | var self = this; 503 | log.info(self._id + ' received ' + JSON.stringify(msg)); 504 | return self._ready().then(function () { // ensure props have been loaded/created first 505 | return self._setChanges(msg.changes); // Process the server's changes 506 | }).then(function () { 507 | return self._props.set({ // Update since 508 | since: msg.since 509 | }); 510 | }); 511 | }; 512 | 513 | DB.prototype._registerChangesListener = function () { 514 | var self = this; 515 | self._socket.on('changes', function (msg) { 516 | self._processChanges(msg); 517 | }); 518 | }; 519 | 520 | DB.prototype._registerSenderListener = function () { 521 | var self = this; 522 | self.on('change', function () { 523 | // This is registered immediately so that we don't listen for a change after a change has 524 | // already been made; therefore, we need to make sure the _initDone promise has resolved first. 525 | self._initDone.then(function () { 526 | self._sender.send(); 527 | return null; // prevent runaway promise warnings 528 | }); 529 | }); 530 | }; 531 | 532 | DB.prototype._registerDisconnectListener = function () { 533 | var self = this; 534 | self._socket.on('disconnect', function () { 535 | log.info(self._id + ' server disconnected'); 536 | self._connected = false; 537 | self.emit('disconnect'); 538 | }); 539 | }; 540 | 541 | DB.prototype._createDatabaseViaSystem = function (dbName) { 542 | return this._systemDB()._createDatabase(dbName).then(function (doc) { 543 | return utils.once(doc, 'doc:record'); 544 | }); 545 | }; 546 | 547 | DB.prototype._destroyDatabaseViaSystem = function (dbName) { 548 | return this._systemDB()._destroyDatabase(dbName).then(function (doc) { 549 | return utils.once(doc, 'doc:record'); 550 | }); 551 | }; 552 | 553 | DB.prototype._createDatabaseAndInit = function () { 554 | var self = this; 555 | return self._createDatabaseViaSystem(self._name).then(function () { 556 | self._init(); 557 | return null; // prevent runaway promise warning 558 | }); 559 | }; 560 | 561 | DB.prototype._onDeltaError = function (err) { 562 | log.warning(this._id + ' err=' + err.message); 563 | 564 | if (err.name === 'DBMissingError') { 565 | log.info(this._id + ' creating DB ' + this._name); 566 | this._createDatabaseAndInit(); 567 | } else { 568 | this.emit('error', err); 569 | } 570 | }; 571 | 572 | DB.prototype._registerDeltaErrorListener = function () { 573 | var self = this; 574 | self._socket.on('delta-error', function (err) { 575 | self._onDeltaError(err); 576 | }); 577 | }; 578 | 579 | DB.prototype._registerInitDoneListener = function () { 580 | var self = this; 581 | 582 | // Server currently requires init-done before it will start listening to changes 583 | self._socket.on('init-done', function () { 584 | log.info(self._id + ' received init-done'); 585 | self.emit('init-done'); // notify listeners 586 | self._sender.send(); 587 | }); 588 | }; 589 | 590 | DB.prototype._init = function () { 591 | this._connected = true; 592 | this._emitInit(); 593 | }; 594 | 595 | DB.prototype._connect = function () { 596 | var self = this; 597 | 598 | self._socket.connect(self._url); 599 | 600 | self._registerDeltaErrorListener(); 601 | 602 | self._registerDisconnectListener(); 603 | 604 | self._registerChangesListener(); 605 | 606 | self._registerInitDoneListener(); 607 | 608 | self._socket.on('connect', function () { 609 | self._init(); 610 | }); 611 | 612 | }; 613 | 614 | DB.prototype._disconnect = function () { 615 | var self = this; 616 | return self._ready().then(function () { 617 | var promise = utils.once(self, 'disconnect'); 618 | self._socket.disconnect(); 619 | return promise; 620 | }); 621 | }; 622 | 623 | DB.prototype._connectWhenReady = function () { 624 | var self = this; 625 | return self._storeLoaded.then(function () { 626 | return self._connect(); 627 | }); 628 | }; 629 | 630 | /** 631 | * Each DB has an associated SystemDB as the DB needs to be able to point to any DB cluster and we 632 | * may have 2 DBs that point to different clusters so the same SystemDB could not be used. 633 | */ 634 | DB.prototype._systemDB = function () { 635 | if (!this._sysDB) { 636 | 637 | var opts = { 638 | db: clientUtils.SYSTEM_DB_NAME, 639 | alias: config.vals.systemDBNamePrefix + this._name, 640 | url: this._url, 641 | local: this._localOnly 642 | }; 643 | 644 | this._sysDB = this._adapter.db(opts); 645 | } 646 | return this._sysDB; 647 | }; 648 | 649 | module.exports = DB; 650 | -------------------------------------------------------------------------------- /scripts/delta-db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Adapter = require('./adapter'), 4 | client = new Adapter(); 5 | 6 | // TODO: shouldn't password be a char array? 7 | var DeltaDB = function (name, url, username, password, store) { 8 | var opts = { 9 | db: name 10 | }; 11 | 12 | if (url) { 13 | opts.url = url; 14 | } else { 15 | opts.local = true; 16 | } 17 | 18 | if (typeof store !== 'undefined') { 19 | opts.store = store; 20 | } 21 | 22 | opts.username = username; 23 | opts.password = password; 24 | 25 | return client.db(opts); 26 | }; 27 | 28 | var wrapFunction = function (fn) { 29 | DeltaDB[fn] = function () { 30 | return client[fn].apply(client, arguments); 31 | }; 32 | }; 33 | 34 | var wrapFunctions = function () { 35 | for (var fn in client) { 36 | if (typeof client[fn] === 'function') { 37 | wrapFunction(fn); 38 | } 39 | } 40 | }; 41 | 42 | // Expose all methods of client as static DeltaDB functions 43 | wrapFunctions(); 44 | 45 | module.exports = DeltaDB; 46 | -------------------------------------------------------------------------------- /scripts/delta-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DeltaError = function (message) { 4 | this.name = 'DeltaError'; 5 | this.message = message; 6 | }; 7 | 8 | DeltaError.prototype = Object.create(Error.prototype); 9 | DeltaError.prototype.constructor = DeltaError; 10 | 11 | module.exports = DeltaError; 12 | -------------------------------------------------------------------------------- /scripts/disabled-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DeltaError = require('./delta-error'), 4 | inherits = require('inherits'); 5 | 6 | var DisabledError = function (message) { 7 | this.name = 'DisabledError'; 8 | this.message = message; 9 | }; 10 | 11 | inherits(DisabledError, DeltaError); 12 | 13 | module.exports = DisabledError; 14 | -------------------------------------------------------------------------------- /scripts/doc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var inherits = require('inherits'), 4 | utils = require('deltadb-common-utils'), 5 | clientUtils = require('./utils'), 6 | MemDoc = require('deltadb-orm-nosql/scripts/adapters/mem/doc'), 7 | Promise = require('bluebird'); 8 | 9 | var Doc = function (data /* , col */ ) { 10 | MemDoc.apply(this, arguments); // apply parent constructor 11 | this._initDat(); 12 | 13 | this._initLoaded(); 14 | 15 | this._changeDoc(data); 16 | 17 | // Emit on next tick so that the caller has time to listen for events 18 | this._emitDocCreateOnNextTick(); 19 | }; 20 | 21 | inherits(Doc, MemDoc); 22 | 23 | Doc._policyName = '$policy'; 24 | 25 | Doc._userName = '$user'; 26 | 27 | Doc._roleName = clientUtils.ATTR_NAME_ROLE; 28 | 29 | Doc._roleUserName = clientUtils.ATTR_NAME_ROLE_USER; 30 | 31 | Doc.prototype._initLoaded = function () { 32 | var self = this; 33 | self._loaded = utils.once(self, 'load'); 34 | }; 35 | 36 | Doc.prototype._import = function (store) { 37 | this._store = store; 38 | this._initStore(); 39 | }; 40 | 41 | Doc.prototype._createStore = function () { 42 | // Only define the id as the attrs will be set in _loadFromStore 43 | var data = {}; 44 | data[this._idName] = this.id(); 45 | this._import(this._col._store.doc(data)); 46 | }; 47 | 48 | Doc.prototype._pointToData = function () { 49 | this._data = this._dat.data; // point to wrapped location 50 | }; 51 | 52 | Doc.prototype._initDat = function () { 53 | // To reduce reads from the store, we will assume that this._dat is always up-to-date and 54 | // therefore changes can just be committed to the store for persistence 55 | var id = this.id(); // use id generated by CommonDoc 56 | this._dat = { 57 | data: this._data, 58 | changes: [], 59 | latest: {}, // TODO: best name as pending to be written to server? 60 | destroyedAt: null, // needed to exclude from cursor before del recorded 61 | updatedAt: null, 62 | recordedAt: null // used to determine whether doc has been recorded 63 | }; 64 | this._dat[this._idName] = id; 65 | 66 | this._pointToData(); 67 | }; 68 | 69 | Doc.prototype._loadTimestampsFromStore = function (store) { 70 | 71 | // The data in the store is being loaded and must be older than the data that we currently have in 72 | // our doc. Therefore, we only consider timestamps from the store if we are missing those values 73 | // in our doc. 74 | 75 | // Are we missing a value for updatedAt? 76 | if (!this._dat.updatedAt) { 77 | this._dat.updatedAt = store.updatedAt; 78 | 79 | // We have already determined that the store was updated later so take its destroyedAt 80 | this._dat.destroyedAt = store.destroyedAt ? store.destroyedAt : null; 81 | } 82 | 83 | // Are we missing a recordedAt? 84 | if (!this._dat.recordedAt) { 85 | this._dat.recordedAt = store.recordedAt; 86 | } 87 | }; 88 | 89 | Doc.prototype._toISOStringIfTruthy = function (date) { 90 | return date ? date.toISOString() : date; 91 | }; 92 | 93 | Doc.prototype._loadFromStore = function () { 94 | var self = this; 95 | 96 | var store = self._store.getRef(); 97 | 98 | // Prepend any changes 99 | if (store.changes) { 100 | self._dat.changes = store.changes.concat(self._dat.changes); 101 | } 102 | 103 | self._loadTimestampsFromStore(store); 104 | 105 | // Iterate through all attributes and set if latest 106 | utils.each(store.latest, function (attr, name) { 107 | 108 | // Replay change by simulating a delta and tracking the changes 109 | self._saveChange({ 110 | name: name, 111 | val: JSON.stringify(attr.val), 112 | up: attr.up.toISOString(), 113 | re: self._toISOStringIfTruthy(attr.re), 114 | seq: attr.seq 115 | }, false, false); 116 | 117 | }); 118 | }; 119 | 120 | Doc.prototype._emitLoad = function () { 121 | this.emit('load'); 122 | }; 123 | 124 | Doc.prototype._initStore = function () { 125 | this._loadFromStore(); 126 | this._emitLoad(); 127 | }; 128 | 129 | Doc.prototype._ensureStore = function () { 130 | var self = this; 131 | // Wait until col is loaded and then create store 132 | return self._col._ensureStore().then(function () { 133 | if (!self._store) { 134 | self._createStore(); 135 | } 136 | return self._loaded; // resolves once doc has been loaded 137 | }); 138 | }; 139 | 140 | Doc.prototype._saveStore = function () { 141 | var self = this; 142 | return self._ensureStore().then(function () { 143 | return self._store.set(self._dat); 144 | }); 145 | }; 146 | 147 | Doc.prototype.save = function () { 148 | var self = this; 149 | return self._saveStore().then(function () { 150 | return MemDoc.prototype.save.apply(self, arguments); 151 | }); 152 | }; 153 | 154 | Doc.prototype._emitChange = function () { 155 | this._col._db.emit('change'); 156 | }; 157 | 158 | // TODO: split up 159 | Doc.prototype._change = function (name, value, updated, recorded, untracked) { 160 | 161 | if (name === this._idName) { 162 | // Don't track changes to id as the id is sent with every delta already 163 | return; 164 | } 165 | 166 | // TODO: remove as being set in _set? 167 | if (!updated) { 168 | updated = new Date(); 169 | } 170 | 171 | // Determine the event before making any changes to the data and then emit the event after the 172 | // data has been changed 173 | var evnts = this._allEvents(name, value, updated); 174 | 175 | // To account for back-to-back writes, increment the seq number if updated is the same 176 | var seq = this._dat.latest[name] && 177 | this._dat.latest[name].up.getTime() === updated.getTime() ? this._dat.latest[name].seq + 1 : 178 | 0; 179 | 180 | var change = { 181 | up: updated 182 | }; 183 | 184 | if (seq > 0) { 185 | change.seq = seq; 186 | } 187 | 188 | if (name) { 189 | change.name = name; 190 | } 191 | 192 | if (typeof value !== 'undefined') { // undefined is used for destroying 193 | change.val = value; 194 | } 195 | 196 | // Is the value changing? We also need to consider it changing if there is no latest value as this 197 | // can happen when auto restoring 198 | var changing = this._changing(name, value) || !this._dat.latest[name]; 199 | 200 | if (!untracked && changing) { // tracking and value changing? 201 | this._dat.changes.push(change); 202 | this._emitChange(); 203 | } 204 | 205 | if (name) { // update? 206 | this._dat.latest[name] = { 207 | val: value, 208 | up: updated, 209 | seq: seq 210 | }; 211 | 212 | if (recorded) { 213 | this._dat.latest[name].re = recorded; // TODO: is this needed? 214 | this._dat.recordedAt = recorded; 215 | } 216 | 217 | // update after del? 218 | if (this._dat.destroyedAt && updated.getTime() > this._dat.destroyedAt.getTime()) { 219 | this._dat.destroyedAt = null; 220 | } 221 | } 222 | 223 | return evnts.length > 0 ? evnts : null; 224 | }; 225 | 226 | Doc.prototype._emitEvents = function (evnts, name) { 227 | var self = this; 228 | evnts.forEach(function (evnt) { 229 | self._emit(evnt.evnt, name, evnt.val); 230 | }); 231 | }; 232 | 233 | Doc.prototype._eventLayer = function (evnt) { 234 | var parts = evnt.split(':'); 235 | return parts[0]; 236 | }; 237 | 238 | Doc.prototype._emit = function (evnt, name, value, recorded) { 239 | if (this._eventLayer(evnt) === 'doc') { 240 | this.emit(evnt, this); 241 | this._col._emit(evnt, this); 242 | } else { 243 | var attr = { 244 | name: name, 245 | value: value, 246 | recorded: recorded 247 | }; 248 | this.emit(evnt, attr, this); 249 | 250 | this._col._emit(evnt, attr, this); // bubble up to collection layer 251 | } 252 | }; 253 | 254 | Doc.prototype._emitDocCreateOnNextTick = function () { 255 | var self = this; 256 | setTimeout(function () { 257 | self._emitDocCreate(); 258 | }); 259 | }; 260 | 261 | Doc.prototype._emitDocCreate = function () { 262 | // Don't emit if the doc was destroyed 263 | if (!this._dat.destroyedAt) { 264 | // Always emit the id as the creating attr 265 | this._emit('doc:create', this._idName, this.id()); 266 | } 267 | }; 268 | 269 | Doc.prototype._saveRecording = function (name, value, recorded) { 270 | if (name && this._dat.latest[name]) { 271 | 272 | this._emit('attr:record', name, value, recorded); 273 | this._emit('doc:record', name, value); 274 | 275 | this._dat.latest[name].re = recorded; // TODO: is this needed? 276 | this._dat.recordedAt = recorded; 277 | } 278 | }; 279 | 280 | // TODO: better "changes" structure needed so that recording can happen faster? Use Dictionary to 281 | // index by docUUID and attrName? 282 | Doc.prototype._record = function (name, value, updated, seq, recorded) { 283 | var self = this, 284 | found = false; 285 | 286 | // Use >= as doc deletion takes precedence 287 | if (!name && (!self._dat.updatedAt || updated.getTime() >= self._dat.updatedAt.getTime())) { 288 | this._dat.destroyedAt = updated; 289 | } 290 | 291 | utils.each(self._dat.changes, function (change, i) { 292 | var val = change.val; 293 | 294 | var changeSeq = utils.notDefined(change.seq) ? 0 : change.seq; 295 | seq = utils.notDefined(seq) ? 0 : seq; 296 | 297 | // Compare UTC strings as the timestamps with getTime() may be different 298 | if (change.name === name && val === value && 299 | change.up.toISOString() === updated.toISOString() && 300 | changeSeq === seq) { 301 | 302 | found = true; // TODO: stop looping once the change has been found 303 | 304 | self._saveRecording(name, value, recorded); 305 | 306 | // TODO: is it better to use splice here? If so we'd need to iterate through the array 307 | // backwards so that we process all elements 308 | delete self._dat.changes[i]; // the change was recorded with a quorum of servers so destroy it 309 | } 310 | }); 311 | 312 | if (!found) { // change originated from server? 313 | self._saveRecording(name, value, recorded); 314 | } 315 | }; 316 | 317 | Doc.prototype._changeDoc = function (doc) { 318 | var self = this; 319 | utils.each(doc, function (value, name) { 320 | self._change(name, value); 321 | }); 322 | }; 323 | 324 | Doc.prototype._destroying = function (value) { 325 | return typeof value === 'undefined'; 326 | }; 327 | 328 | // Cannot be called _events as this name is used by EventEmitter 329 | Doc.prototype._allEvents = function (name, value, updated) { 330 | var evnts = []; 331 | 332 | if (name) { // attr change? 333 | if (utils.notDefined(this._dat.data[name])) { // attr doesn't exist? 334 | evnts.push({ 335 | evnt: 'attr:create', 336 | val: value 337 | }); 338 | } else if (!this._dat.latest[name] || 339 | updated.getTime() > this._dat.latest[name].up.getTime()) { // change most recent? 340 | if (this._destroying(value)) { // destroying? 341 | evnts.push({ 342 | evnt: 'attr:destroy', 343 | val: this._dat.latest[name].val 344 | }); 345 | } else { // updating 346 | evnts.push({ 347 | evnt: 'attr:update', 348 | val: value 349 | }); 350 | } 351 | } 352 | evnts.push({ 353 | evnt: 'doc:update', 354 | val: value 355 | }); 356 | } else { // destroying doc? 357 | if (!this._dat.updatedAt || updated.getTime() > this._dat.updatedAt.getTime()) { // most recent? 358 | evnts.push({ 359 | evnt: 'doc:destroy', 360 | val: value 361 | }); 362 | } 363 | } 364 | return evnts; 365 | }; 366 | 367 | Doc.prototype._set = function (name, value, updated, recorded, untracked) { 368 | 369 | if (!updated) { 370 | updated = new Date(); 371 | } 372 | 373 | var events = this._change(name, value, updated, recorded, untracked); 374 | 375 | if (updated && (!this._dat.updatedAt || updated.getTime() > this._dat.updatedAt.getTime())) { 376 | this._dat.updatedAt = updated; 377 | } 378 | 379 | // Set the value before any events are emitted by _change() 380 | var ret = MemDoc.prototype._set.apply(this, arguments); 381 | 382 | if (events) { // events is falsey when setting the id 383 | this._emitEvents(events, name); 384 | } 385 | 386 | return ret; 387 | }; 388 | 389 | Doc.prototype.unset = function (name, updated, recorded, untracked) { 390 | // Use undefined to destroy 391 | var events = this._change(name, undefined, updated, recorded, untracked); 392 | 393 | // Unset the value before any events are emitted by _change() 394 | var ret = MemDoc.prototype.unset.apply(this, arguments); 395 | 396 | if (events) { 397 | this._emitEvents(events, name); 398 | } 399 | 400 | return ret; 401 | }; 402 | 403 | // TODO: remove this after enhance id-less docs to reconcile with ids? 404 | Doc.prototype._destroyLocally = function () { 405 | var self = this; 406 | return MemDoc.prototype.destroy.apply(this, arguments).then(function () { 407 | return self._store.destroy(); 408 | }); 409 | }; 410 | 411 | Doc.prototype.destroy = function (destroyedAt, untracked) { 412 | // Doesn't actually remove data as we need to preserve tombstone so that we can ignore any 413 | // outdated changes received for destroyed data 414 | this._dat.destroyedAt = destroyedAt ? destroyedAt : new Date(); 415 | 416 | // undefined is used to identify a destroy 417 | var events = this._change(null, undefined, this._dat.destroyedAt, null, untracked); 418 | 419 | if (events) { 420 | this._emitEvents(events, null); 421 | } 422 | 423 | return this.save(); 424 | }; 425 | 426 | Doc.prototype._fromDeltaValue = function (val) { 427 | // Only parse if value is defined 428 | return typeof val === 'undefined' ? undefined : JSON.parse(val); // val is JSON 429 | }; 430 | 431 | Doc.prototype._saveChange = function (change, tracked, record) { 432 | var self = this; 433 | var updated = new Date(change.up); // date is string 434 | var recorded = change.re ? new Date(change.re) : null; // date is string 435 | var val = self._fromDeltaValue(change.val); 436 | var latest = self._dat.latest[change.name]; 437 | var promise = Promise.resolve(); 438 | var untracked = !tracked; 439 | 440 | self._markedAt = null; 441 | if (latest) { 442 | delete latest.markedAt; 443 | } 444 | 445 | // TODO: why is getTime() needed? 446 | if (change.name) { // changing attr 447 | if ((!latest || updated.getTime() > latest.up.getTime() || 448 | (updated.getTime() === latest.up.getTime() && change.seq > latest.seq) || 449 | (updated.getTime() === latest.up.getTime() && 450 | change.seq === latest.seq))) { 451 | if (typeof val !== 'undefined') { 452 | self._set(change.name, val, updated, recorded, untracked); 453 | } else { 454 | self.unset(change.name, updated, recorded, untracked); 455 | } 456 | promise = self.save(); 457 | } 458 | } else if (!self._dat.updatedAt || 459 | updated.getTime() > self._dat.updatedAt.getTime()) { // destroying doc? 460 | promise = self.destroy(updated, untracked); 461 | } 462 | 463 | return promise.then(function () { 464 | if (record) { 465 | self._record(change.name, val, updated, change.seq, recorded); 466 | } 467 | return null; // prevent runaway promise warnings 468 | }); 469 | }; 470 | 471 | Doc.prototype._setChange = function (change) { 472 | var self = this; 473 | return self._saveChange(change, null, true).then(function () { 474 | // Commit the changes to the store so that they aren't lost 475 | return self._saveStore(); 476 | }); 477 | }; 478 | 479 | Doc.prototype._include = function () { 480 | return this._dat.destroyedAt === null; 481 | }; 482 | 483 | Doc.prototype._setAndSave = function (doc) { 484 | var self = this; 485 | return self.set(doc).then(function () { 486 | // TODO: is the following line needed? Isn't it called by set? 487 | return self.save(); 488 | }).then(function () { 489 | return self; 490 | }); 491 | }; 492 | 493 | Doc.prototype.policy = function (policy) { 494 | var doc = {}; 495 | doc[Doc._policyName] = policy; 496 | return this._setAndSave(doc); 497 | }; 498 | 499 | // Shouldn't be called directly as the docUUID needs to be set properly 500 | Doc.prototype._createUser = function (userUUID, username, password, status) { 501 | var self = this, 502 | doc = {}; 503 | return clientUtils.genUser(userUUID, username, password, status).then(function (user) { 504 | doc[Doc._userName] = user; 505 | return self._setAndSave(doc); 506 | }); 507 | }; 508 | 509 | Doc.prototype._addRole = function (userUUID, roleName) { 510 | var data = {}; 511 | data[Doc._roleName] = { 512 | action: clientUtils.ACTION_ADD, 513 | userUUID: userUUID, 514 | roleName: roleName 515 | }; 516 | return this._setAndSave(data); 517 | }; 518 | 519 | Doc.prototype._removeRole = function (userUUID, roleName) { 520 | var data = {}; 521 | data[Doc._roleName] = { 522 | action: clientUtils.ACTION_REMOVE, 523 | userUUID: userUUID, 524 | roleName: roleName 525 | }; 526 | return this._setAndSave(data); 527 | }; 528 | 529 | // Note: must only be called for System DB 530 | Doc.prototype._createDatabase = function (dbName) { 531 | var data = {}; 532 | data[clientUtils.ATTR_NAME_ACTION] = { 533 | action: clientUtils.ACTION_ADD, 534 | name: dbName 535 | }; 536 | return this._setAndSave(data); 537 | }; 538 | 539 | // Note: must only be called for System DB 540 | Doc.prototype._destroyDatabase = function (dbName) { 541 | var data = {}; 542 | data[clientUtils.ATTR_NAME_ACTION] = { 543 | action: clientUtils.ACTION_REMOVE, 544 | name: dbName 545 | }; 546 | return this._setAndSave(data); 547 | }; 548 | 549 | Doc.prototype._formatChange = function (retryAfter, returnSent, changes, change, now) { 550 | // Use >= to ensure we get all changes when retryAfter=0 551 | if (!change.sent || now >= change.sent.getTime() + retryAfter) { // never sent or retry? 552 | var chng = utils.clone(change); // clone so that we don't modify original data 553 | if (!returnSent) { 554 | delete chng.sent; // server doesn't need sent 555 | } 556 | chng.col = this._col._name; 557 | chng.id = this.id(); 558 | chng.up = change.up.toISOString(); 559 | 560 | if (typeof chng.val !== 'undefined') { // an undefined value represents a destroy 561 | chng.val = JSON.stringify(chng.val); 562 | } else { 563 | delete chng.val; // save some bandwidth and clear if null 564 | } 565 | 566 | // if (!change.seq) { 567 | // delete chng.seq; // save some bandwidth and clear the seq if 0 568 | // } 569 | 570 | changes.push(chng); 571 | change.sent = new Date(); 572 | return true; 573 | } 574 | }; 575 | 576 | Doc.prototype._localChanges = function (retryAfter, returnSent, limit, nContainer) { 577 | var self = this, 578 | changes = [], 579 | now = (new Date()).getTime(), 580 | more = false; 581 | 582 | retryAfter = typeof retryAfter === 'undefined' ? 0 : retryAfter; 583 | 584 | utils.each(self._dat.changes, function (change) { 585 | // newChange is true if this change has already been marked for sending, but isn't yet ready to 586 | // be retried 587 | var newChange = self._formatChange(retryAfter, returnSent, changes, change, now); 588 | 589 | // Have we processed the max batch size? Then exit loop early 590 | if (newChange && limit && ++nContainer.n === limit) { 591 | more = true; 592 | return false; 593 | } 594 | }); 595 | 596 | return { 597 | changes: changes, 598 | more: more 599 | }; 600 | }; 601 | 602 | module.exports = Doc; 603 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./auto-adapter-store'); // automatically select default store 4 | 5 | module.exports = require('./delta-db'); 6 | -------------------------------------------------------------------------------- /scripts/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var inherits = require('inherits'), 4 | ParentLog = require('deltadb-common-utils/scripts/log'); 5 | 6 | var Log = function () { 7 | ParentLog.apply(this, arguments); 8 | }; 9 | 10 | inherits(Log, ParentLog); 11 | 12 | module.exports = new Log(); 13 | -------------------------------------------------------------------------------- /scripts/sender.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var commonUtils = require('deltadb-common-utils'); 4 | 5 | var Sender = function (db) { 6 | this._db = db; 7 | this._sending = false; 8 | this._lastSent = new Date(); 9 | 10 | this._retrySender(); 11 | }; 12 | 13 | Sender.SEND_EVERY_MS = 1000; 14 | 15 | Sender.prototype._doSend = function () { 16 | return this._db._findAndEmitChanges(); 17 | }; 18 | 19 | Sender.prototype._sendLoop = function () { 20 | var self = this; 21 | 22 | if (self._lastSent.getTime() > self._requested.getTime()) { // nothing more to send? 23 | self._sending = false; 24 | } else { 25 | 26 | // Sleep by 1 ms so that _lastSent is != _requested for the first request 27 | return commonUtils.timeout(1).then(function () { 28 | self._lastSent = new Date(); 29 | return self._doSend(); 30 | }).then(function () { 31 | // return commonUtils.timeout(Sender.SEND_EVERY_MS); 32 | // }).then(function () { 33 | // self._sendLoop(); 34 | 35 | // TODO: is it better to use setTimeout below than another promise as it is won't cause a 36 | // stack overflow? 37 | setTimeout(function () { 38 | self._sendLoop(); 39 | }, Sender.SEND_EVERY_MS); 40 | 41 | return null; // prevent runaway promise warnings 42 | }); 43 | } 44 | }; 45 | 46 | Sender.prototype.send = function () { 47 | // - Kick off a send process if not already sending. If already sending then set timestamp 48 | // - When send process completes, check timestamp and determine if need to send again 49 | this._requested = new Date(); 50 | 51 | if (!this._sending) { 52 | this._sending = true; 53 | this._sendLoop(); 54 | } 55 | }; 56 | 57 | // Use another loop to ensure that deltas are resent 58 | Sender.prototype._retrySender = function () { 59 | var self = this; 60 | setTimeout(function () { 61 | self.send(); 62 | self._retrySender(); 63 | }, self._db._retryAfterMSecs + 100); 64 | // Sleep _retryAfterMSecs + 100 so that we send any deltas that need to be retried 65 | }; 66 | 67 | module.exports = Sender; 68 | -------------------------------------------------------------------------------- /scripts/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var io = require('socket.io-client'); 4 | 5 | /** 6 | * Abstract the socket interface for testing and the ability to swap out socket libs 7 | */ 8 | var Socket = function () { 9 | this._io = io; // for mocking 10 | }; 11 | 12 | Socket.prototype.connect = function (url) { 13 | this._socket = this._io.connect(url, { 14 | 'force new connection': true 15 | }); // same client, multiple connections for testing 16 | }; 17 | 18 | Socket.prototype.emit = function (event, msg) { 19 | this._socket.emit(event, msg); 20 | }; 21 | 22 | Socket.prototype.on = function (event, callback) { 23 | this._socket.on(event, callback); 24 | }; 25 | 26 | Socket.prototype.disconnect = function () { 27 | this._socket.disconnect(); 28 | }; 29 | 30 | module.exports = Socket; 31 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('deltadb-common-utils'); 4 | 5 | var Utils = function () {}; 6 | 7 | Utils.prototype.STATUS_ENABLED = 'enabled'; // Also set here so that client doesn't need Users 8 | 9 | Utils.prototype.genUser = function (userUUID, username, password, status) { 10 | // Include uuid in user so that can retrieve userUUIDs using deltas 11 | var user = { 12 | uuid: userUUID, 13 | username: username, 14 | status: status ? status : this.STATUS_ENABLED 15 | }; 16 | return utils.genSaltAndHashPassword(password).then(function (saltAndPwd) { 17 | user.salt = saltAndPwd.salt; 18 | user.password = saltAndPwd.hash; 19 | return user; 20 | }); 21 | }; 22 | 23 | // Use a prefix so that user UUIDs don't conflict with UUIDs of other docs 24 | Utils.prototype.UUID_PRE = '$u'; 25 | 26 | Utils.prototype.toDocUUID = function (userUUID) { 27 | // docUUID is derived from userUUID as we need to create user's dynamically when we first 28 | // encounter a change and need a way to reference that user later 29 | return this.UUID_PRE + userUUID; 30 | }; 31 | 32 | Utils.prototype.NAME_PRE_USER_ROLES = '$ur'; 33 | 34 | Utils.prototype.ACTION_ADD = 'add'; 35 | Utils.prototype.ACTION_REMOVE = 'remove'; 36 | 37 | Utils.prototype.SYSTEM_DB_NAME = 'system'; 38 | Utils.prototype.DB_COLLECTION_NAME = '$db'; 39 | Utils.prototype.DB_ATTR_NAME = '$db'; // TODO: rename to ATTR_NAME_DB?? 40 | 41 | Utils.prototype.COL_NAME_ALL = '$all'; 42 | 43 | Utils.prototype.ATTR_NAME_ROLE = '$role'; 44 | Utils.prototype.ATTR_NAME_ROLE_USER = '$ruser'; 45 | Utils.prototype.ATTR_NAME_ACTION = '$action'; 46 | 47 | Utils.prototype.escapeDBName = function (dbName) { 48 | // Allow hyphens, but convert to underscores in case DB doesn't support hyphens 49 | var pat1 = new RegExp('-', 'g'); 50 | dbName = dbName.replace(pat1, '_').toLowerCase(); 51 | 52 | // Check for any invalid chars 53 | var pat3 = new RegExp('[^0-9a-z_]', 'gim'); 54 | if (pat3.test(dbName)) { 55 | throw new Error('DB name [' + dbName + '] contains invalid chars'); 56 | } 57 | return dbName; 58 | }; 59 | 60 | module.exports = new Utils(); 61 | -------------------------------------------------------------------------------- /test/browser-coverage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tests Reporter Output to File 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 16 | 17 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/browser-coverage/phantom-hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | afterEnd: function (runner) { 5 | var fs = require('fs'); 6 | var coverage = runner.page.evaluate(function () { 7 | return window.__coverage__; 8 | }); 9 | 10 | if (coverage) { 11 | console.log('Writing coverage to coverage/browser/coverage.json'); 12 | fs.write('coverage/browser/coverage.json', JSON.stringify(coverage), 'w'); 13 | } else { 14 | console.log('No coverage data generated'); 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /test/browser-coverage/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var HTTP_PORT = 8001; 6 | var http_server = require("http-server"); 7 | 8 | http_server.createServer().listen(HTTP_PORT); 9 | console.log('Tests: http://127.0.0.1:' + HTTP_PORT + '/test/browser-coverage/index.html'); 10 | -------------------------------------------------------------------------------- /test/browser-coverage/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('./server'); 6 | 7 | // Uncomment for debugging 8 | // (function() { 9 | // var childProcess = require("child_process"); 10 | // var oldSpawn = childProcess.spawn; 11 | // function mySpawn() { 12 | // console.log('spawn called'); 13 | // console.log(arguments); 14 | // var result = oldSpawn.apply(this, arguments); 15 | // return result; 16 | // } 17 | // childProcess.spawn = mySpawn; 18 | // })(); 19 | 20 | var spawn = require('child_process').spawn; 21 | 22 | var options = [ 23 | 'http://127.0.0.1:8001/test/browser-coverage/index.html', 24 | '--timeout', '25000', 25 | '--hooks', 'test/browser-coverage/phantom-hooks.js' 26 | ]; 27 | 28 | if (process.env.GREP) { 29 | options.push('-g'); 30 | options.push(process.env.GREP); 31 | } 32 | 33 | // Unless we have mocha-phantomjs installed globally we have to specify the full path 34 | // var child = spawn('mocha-phantomjs', options); 35 | var child = spawn('./node_modules/mocha-phantomjs/bin/mocha-phantomjs', options); 36 | 37 | child.stdout.on('data', function (data) { 38 | console.log(data.toString()); // echo output, including what could be errors 39 | }); 40 | 41 | child.stderr.on('data', function (data) { 42 | console.error(data.toString()); 43 | }); 44 | 45 | child.on('error', function (err) { 46 | console.error(err); 47 | }); 48 | 49 | child.on('close', function (code) { 50 | console.log('Mocha process exited with code ' + code); 51 | if (code > 0) { 52 | process.exit(1); 53 | } else { 54 | process.exit(0); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mocha Tests 7 | 8 | 9 | 10 | 28 | 29 | 30 | 31 | 32 |
33 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/browser/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../node'); 4 | -------------------------------------------------------------------------------- /test/browser/sauce-results-updater.js: -------------------------------------------------------------------------------- 1 | // TODO: Is this really the best way to update the sauce lab job with the build and test status? If 2 | // so, then this should be made into a separate GH repo. 3 | 4 | 'use strict'; 5 | 6 | var request = require('request'), 7 | SauceLabs = require('saucelabs'), 8 | Promise = require('bluebird'); 9 | 10 | var Sauce = function (username, accessKey) { 11 | this._username = username; 12 | this._sauceLabs = new SauceLabs({ 13 | username: username, 14 | password: accessKey 15 | }); 16 | }; 17 | 18 | Sauce.prototype.findJob = function (jobName) { 19 | // NOTE: it appears that there is no way to retrieve the job id when launching a test via the 20 | // sauce-connect-launcher package. Therefore, we will use the sauce API to look up the job id. The 21 | // saucelabs package doesn't support the full option for getJobs and we don't want to have to make 22 | // an API call for each job to determine whether the job name matches so we will execute this GET 23 | // request manually. 24 | var self = this; 25 | return new Promise(function (resolve, reject) { 26 | var url = 'https://saucelabs.com/rest/v1/' + self._username + '/jobs?full=true'; 27 | request(url, function (err, res, body) { 28 | if (err) { 29 | reject(err); 30 | } else { 31 | var jobs = JSON.parse(body); 32 | jobs.forEach(function (job) { 33 | if (job.name === jobName) { // matching job name? 34 | resolve(job); 35 | } 36 | }); 37 | } 38 | }); 39 | }); 40 | }; 41 | 42 | Sauce.prototype.updateJob = function (id, data) { 43 | var self = this; 44 | return new Promise(function (resolve, reject) { 45 | self._sauceLabs.updateJob(id, data, function (err, res) { 46 | if (err) { 47 | reject(err); 48 | } else { 49 | resolve(res); 50 | } 51 | }); 52 | }); 53 | }; 54 | 55 | Sauce.prototype.setPassed = function (jobName, build, passed) { 56 | var self = this; 57 | return self.findJob(jobName).then(function (job) { 58 | return self.updateJob(job.id, { 59 | build: build, 60 | passed: passed 61 | }); 62 | }); 63 | }; 64 | 65 | module.exports = Sauce; 66 | -------------------------------------------------------------------------------- /test/browser/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var HTTP_PORT = 8001; 6 | 7 | var http_server = require('http-server'); 8 | var fs = require('fs'); 9 | var indexfile = './test/browser/index.js'; 10 | var dotfile = './test/browser/.bundle.js'; 11 | var outfile = './test/browser/bundle.js'; 12 | var watchify = require('watchify'); 13 | var browserify = require('browserify'); 14 | 15 | // TODO: make this configurable via an env var 16 | // Watchify appears to occasionally cause "Error: watch ENOSPC" errors in saucelabs so we'll just 17 | // disable it. 18 | var useWatchify = false; 19 | 20 | var b = browserify(indexfile, { 21 | cache: {}, 22 | packageCache: {}, 23 | fullPaths: true, 24 | debug: true 25 | }); 26 | 27 | var filesWritten = false; 28 | var serverStarted = false; 29 | var readyCallback; 30 | 31 | function bundle() { 32 | var wb = (useWatchify ? w.bundle() : b.bundle()); 33 | wb.on('error', function (err) { 34 | console.error(String(err)); 35 | }); 36 | wb.on('end', end); 37 | wb.pipe(fs.createWriteStream(dotfile)); 38 | 39 | function end() { 40 | fs.rename(dotfile, outfile, function (err) { 41 | if (err) { 42 | return console.error(err); 43 | } 44 | console.log('Updated:', outfile); 45 | filesWritten = true; 46 | checkReady(); 47 | }); 48 | } 49 | } 50 | 51 | if (useWatchify) { 52 | var w = watchify(b); 53 | w.on('update', bundle); 54 | } 55 | 56 | bundle(); 57 | 58 | function startServers(callback) { 59 | readyCallback = callback; 60 | http_server.createServer().listen(HTTP_PORT); 61 | console.log('Tests: http://127.0.0.1:' + HTTP_PORT + '/test/browser/index.html'); 62 | serverStarted = true; 63 | checkReady(); 64 | } 65 | 66 | function checkReady() { 67 | if (filesWritten && serverStarted && readyCallback) { 68 | readyCallback(); 69 | } 70 | } 71 | 72 | if (require.main === module) { 73 | startServers(); 74 | } else { 75 | module.exports.start = startServers; 76 | } 77 | -------------------------------------------------------------------------------- /test/browser/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var wd = require('wd'); 6 | var sauceConnectLauncher = require('sauce-connect-launcher'); 7 | var selenium = require('selenium-standalone'); 8 | var querystring = require('querystring'); 9 | var SauceResultsUpdater = require('./sauce-results-updater'); 10 | 11 | var server = require('./server.js'); 12 | 13 | var testTimeout = 30 * 60 * 1000; 14 | 15 | var retries = 0; 16 | var MAX_RETRIES = 10; 17 | var MS_BEFORE_RETRY = 60000; 18 | 19 | var username = process.env.SAUCE_USERNAME; 20 | var accessKey = process.env.SAUCE_ACCESS_KEY; 21 | 22 | var sauceResultsUpdater = new SauceResultsUpdater(username, accessKey); 23 | 24 | // process.env.CLIENT is a colon seperated list of 25 | // (saucelabs|selenium):browserName:browserVerion:platform 26 | var clientStr = process.env.CLIENT || 'selenium:phantomjs'; 27 | var tmp = clientStr.split(':'); 28 | var client = { 29 | runner: tmp[0] || 'selenium', 30 | browser: tmp[1] || 'phantomjs', 31 | version: tmp[2] || null, // Latest 32 | platform: tmp[3] || null 33 | }; 34 | 35 | var testUrl = 'http://127.0.0.1:8001/test/browser/index.html'; 36 | var qs = {}; 37 | 38 | var sauceClient; 39 | var sauceConnectProcess; 40 | var tunnelId = process.env.TRAVIS_JOB_NUMBER || 'tunnel-' + Date.now(); 41 | 42 | var jobName = tunnelId + '-' + clientStr; 43 | 44 | var build = (process.env.TRAVIS_COMMIT ? process.env.TRAVIS_COMMIT : Date.now()); 45 | 46 | if (client.runner === 'saucelabs') { 47 | qs.saucelabs = true; 48 | } 49 | 50 | if (process.env.GREP) { 51 | qs.grep = process.env.GREP; 52 | } 53 | 54 | if (process.env.NOINDEXEDDB) { 55 | qs.noindexeddb = process.env.NOINDEXEDDB; 56 | } 57 | 58 | testUrl += '?'; 59 | testUrl += querystring.stringify(qs); 60 | 61 | function testError(e) { 62 | console.error(e); 63 | console.error('Doh, tests failed'); 64 | sauceClient.quit(); 65 | process.exit(3); 66 | } 67 | 68 | function postResult(result) { 69 | var failed = !process.env.PERF && result.failed; 70 | if (client.runner === 'saucelabs') { 71 | sauceResultsUpdater.setPassed(jobName, build, !failed).then(function () { 72 | process.exit(failed ? 1 : 0); 73 | }); 74 | } else { 75 | process.exit(failed ? 1 : 0); 76 | } 77 | } 78 | 79 | function testComplete(result) { 80 | sauceClient.quit().then(function () { 81 | if (sauceConnectProcess) { 82 | sauceConnectProcess.close(function () { 83 | postResult(result); 84 | }); 85 | } else { 86 | postResult(result); 87 | } 88 | }); 89 | } 90 | 91 | function startSelenium(callback) { 92 | // Start selenium 93 | var opts = { 94 | version: '2.45.0' 95 | }; 96 | selenium.install(opts, function (err) { 97 | if (err) { 98 | console.error('Failed to install selenium'); 99 | process.exit(1); 100 | } 101 | selenium.start(opts, function ( /* err, server */ ) { 102 | sauceClient = wd.promiseChainRemote(); 103 | callback(); 104 | }); 105 | }); 106 | } 107 | 108 | function startSauceConnect(callback) { 109 | 110 | var options = { 111 | username: username, 112 | accessKey: accessKey, 113 | tunnelIdentifier: tunnelId 114 | }; 115 | 116 | sauceConnectLauncher(options, function (err, _sauceConnectProcess) { 117 | if (err) { 118 | console.error('Failed to connect to saucelabs, err=', err); 119 | 120 | if (++retries > MAX_RETRIES) { 121 | console.log('Max retries reached, exiting'); 122 | process.exit(1); 123 | } else { 124 | console.log('Retry', retries, '...'); 125 | setTimeout(function () { 126 | startSauceConnect(callback); 127 | }, MS_BEFORE_RETRY); 128 | } 129 | 130 | } else { 131 | sauceConnectProcess = _sauceConnectProcess; 132 | sauceClient = wd.promiseChainRemote('localhost', 4445, username, accessKey); 133 | callback(); 134 | } 135 | }); 136 | } 137 | 138 | function startTest() { 139 | 140 | console.log('Starting', client); 141 | 142 | var opts = { 143 | browserName: client.browser, 144 | version: client.version, 145 | platform: client.platform, 146 | tunnelTimeout: testTimeout, 147 | name: jobName, 148 | 'max-duration': 60 * 30, 149 | 'command-timeout': 599, 150 | 'idle-timeout': 599, 151 | 'tunnel-identifier': tunnelId 152 | }; 153 | 154 | sauceClient.init(opts).get(testUrl, function () { 155 | 156 | /* jshint evil: true */ 157 | var interval = setInterval(function () { 158 | 159 | sauceClient.eval('window.results', function (err, results) { 160 | 161 | console.log('=> ', results); 162 | 163 | if (err) { 164 | clearInterval(interval); 165 | testError(err); 166 | } else if (results.completed || results.failures.length) { 167 | clearInterval(interval); 168 | testComplete(results); 169 | } 170 | 171 | }); 172 | }, 10 * 1000); 173 | }); 174 | } 175 | 176 | server.start(function () { 177 | if (client.runner === 'saucelabs') { 178 | startSauceConnect(startTest); 179 | } else { 180 | startSelenium(startTest); 181 | } 182 | }); 183 | -------------------------------------------------------------------------------- /test/browser/webrunner.js: -------------------------------------------------------------------------------- 1 | /* global mocha */ 2 | 3 | (function () { 4 | 'use strict'; 5 | var runner = mocha.run(); 6 | window.results = { 7 | lastPassed: '', 8 | passed: 0, 9 | failed: 0, 10 | failures: [] 11 | }; 12 | 13 | runner.on('pass', function (e) { 14 | window.results.lastPassed = e.title; 15 | window.results.passed++; 16 | }); 17 | 18 | runner.on('fail', function (e) { 19 | window.results.failed++; 20 | window.results.failures.push({ 21 | title: e.title, 22 | message: e.err.message, 23 | stack: e.err.stack 24 | }); 25 | }); 26 | 27 | runner.on('end', function () { 28 | window.results.completed = true; 29 | window.results.passed++; 30 | }); 31 | })(); 32 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "dbNamePrefix": "delta_test_", 4 | 5 | "systemDBNamePrefix": "delta_test_sys_", 6 | 7 | "url": { 8 | "scheme": "http", 9 | "host": "localhost", 10 | "port": 8090 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./node'); 4 | -------------------------------------------------------------------------------- /test/node/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | chai.use(require('chai-as-promised')); 5 | chai.should(); 6 | 7 | // NOTE: auto-adapter-store uses IndexedDB when available. The IndexedDB adapter is tested at the 8 | // deletadb-orm-nosql layer, but to be extra safe we also test it at this layer as IndexedDB is very 9 | // delicate. 10 | require('../../scripts/auto-adapter-store'); 11 | 12 | if (global.deltaDBORMNoSQLNoIDB) { // don't use IndexedDB? 13 | var adapterStore = require('../../scripts/adapter-store'), 14 | MemAdapter = require('deltadb-orm-nosql/scripts/adapters/mem'); 15 | adapterStore.newAdapter = function () { 16 | return new MemAdapter(); 17 | }; 18 | } 19 | 20 | // Set config so that our test server doesn't interfere with any production server. We need to set 21 | // the config first so that all of the following code uses this config. 22 | var config = require('../../scripts/config'), 23 | testConfig = require('../config.json'); 24 | for (var i in testConfig) { 25 | config.vals[i] = testConfig[i]; 26 | } 27 | 28 | require('../spec'); 29 | -------------------------------------------------------------------------------- /test/spec/adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Client = require('../../scripts/adapter'); 4 | 5 | describe('adapter', function () { 6 | 7 | var client = null, 8 | db1 = null; 9 | 10 | beforeEach(function () { 11 | client = new Client(true); 12 | 13 | db1 = client.db({ 14 | db: 'mydb' 15 | }); 16 | }); 17 | 18 | afterEach(function () { 19 | return db1.destroy(); 20 | }); 21 | 22 | it('should reuse dbs', function () { 23 | var db2 = client.db({ 24 | db: 'mydb' 25 | }); 26 | db2.should.eql(db1); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/spec/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('../../scripts/config'); 4 | 5 | describe('config', function () { 6 | 7 | it('should build url when port is missing', function () { 8 | config.vals.url.port = null; 9 | (config.url() === null).should.eql(false); 10 | }); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /test/spec/db/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('db', function () { 4 | 5 | require('./with-socket'); 6 | require('./without-socket'); 7 | 8 | }); 9 | -------------------------------------------------------------------------------- /test/spec/db/mock-socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var inherits = require('inherits'), 4 | EventEmitter = require('events').EventEmitter, 5 | clientUtils = require('../../../scripts/utils'), 6 | DBMissingError = require('deltadb-common-utils/scripts/errors/db-missing-error'); 7 | 8 | var MockSocket = function () { 9 | this._dbs = {}; 10 | this._dbs[clientUtils.SYSTEM_DB_NAME] = true; // system should always be present 11 | }; 12 | 13 | inherits(MockSocket, EventEmitter); 14 | 15 | MockSocket.prototype.connect = function ( /* url */ ) { 16 | var self = this; 17 | setTimeout(function () { // trigger on next tick to allow time for binding 18 | self.emit('connect'); 19 | }); 20 | }; 21 | 22 | MockSocket.prototype._processInit = function (msg) { 23 | if (this._dbs[msg.db]) { // db exists? 24 | this.emit('init-done'); 25 | } else { // db missing 26 | this.emit('delta-error', new DBMissingError(msg.db + ' missing')); 27 | } 28 | }; 29 | 30 | MockSocket.prototype._processDBAction = function (change) { 31 | var val = JSON.parse(change.val); 32 | 33 | switch (val.action) { 34 | case clientUtils.ACTION_ADD: 35 | 36 | break; 37 | 38 | case clientUtils.ACTION_REMOVE: 39 | 40 | break; 41 | } 42 | }; 43 | 44 | MockSocket.prototype._processAction = function (change) { 45 | switch (change.col) { 46 | case clientUtils.DB_COLLECTION_NAME: 47 | this._processDBAction(change); 48 | break; 49 | } 50 | }; 51 | 52 | MockSocket.prototype._processChange = function (change) { 53 | switch (change.name) { 54 | case clientUtils.ATTR_NAME_ACTION: 55 | this._processAction(change); 56 | break; 57 | } 58 | }; 59 | 60 | MockSocket.prototype._processChanges = function (msg) { 61 | var self = this; 62 | msg.changes.forEach(function (change) { 63 | self._processChange(change); 64 | change.re = (new Date()).toISOString(); // simulate recording 65 | }); 66 | 67 | // Simulate recording 68 | this.emit('changes', msg, true); 69 | }; 70 | 71 | MockSocket.prototype.emit = function (event, msg, fromServer) { 72 | 73 | if (!fromServer) { // coming from client? 74 | switch (event) { 75 | case 'init': 76 | this._processInit(msg); 77 | break; 78 | 79 | case 'changes': 80 | this._processChanges(msg); 81 | break; 82 | } 83 | } 84 | 85 | return EventEmitter.prototype.emit.apply(this, arguments); 86 | }; 87 | 88 | MockSocket.prototype.on = function ( /* event, callback */ ) { 89 | return EventEmitter.prototype.on.apply(this, arguments); 90 | }; 91 | 92 | MockSocket.prototype.disconnect = function () { 93 | // TODO: needed? 94 | this.emit('disconnect'); 95 | }; 96 | 97 | module.exports = MockSocket; 98 | -------------------------------------------------------------------------------- /test/spec/db/with-socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DB = require('../../../scripts/db'), 4 | Client = require('../../../scripts/adapter'), 5 | commonUtils = require('deltadb-common-utils'), 6 | MockSocket = require('./mock-socket'); 7 | 8 | describe('with-socket', function () { 9 | 10 | var db = null, 11 | client = null; 12 | 13 | afterEach(function () { 14 | if (db) { 15 | return db.destroy(); 16 | } 17 | }); 18 | 19 | var createClientAndDB = function (opts) { 20 | DB._SocketClass = MockSocket; 21 | 22 | client = new Client(); 23 | 24 | var allOpts = { 25 | db: 'mydb' 26 | }; 27 | 28 | if (opts) { 29 | allOpts = commonUtils.merge(allOpts, opts); 30 | } 31 | 32 | db = client.db(allOpts); 33 | }; 34 | 35 | it('should connect', function () { 36 | createClientAndDB(); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/spec/db/without-socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DB = require('../../../scripts/db'), 4 | MemAdapter = require('deltadb-orm-nosql/scripts/adapters/mem'), 5 | Client = require('../../../scripts/adapter'), 6 | commonUtils = require('deltadb-common-utils'), 7 | commonTestUtils = require('deltadb-common-utils/scripts/test-utils'), 8 | clientUtils = require('../../../scripts/utils'), 9 | Promise = require('bluebird'); 10 | 11 | describe('without-socket', function () { 12 | 13 | var db = null, 14 | client = null; 15 | 16 | afterEach(function () { 17 | if (db) { 18 | return db.destroy(true); 19 | } 20 | }); 21 | 22 | var createClientAndDB = function (opts) { 23 | client = new Client(true); 24 | 25 | var allOpts = { 26 | db: 'mydb' 27 | }; 28 | 29 | if (opts) { 30 | allOpts = commonUtils.merge(allOpts, opts); 31 | } 32 | 33 | db = client.db(allOpts); 34 | }; 35 | 36 | it('should reload properties', function () { 37 | var store = new MemAdapter(); 38 | var client = new Client(true); 39 | 40 | var dbStore = store.db({ 41 | db: 'mydb' 42 | }); 43 | 44 | var propCol = dbStore.col(DB.PROPS_COL_NAME); 45 | 46 | var data = {}; 47 | data[dbStore._idName] = DB.PROPS_DOC_ID; 48 | var doc = propCol.doc(data); 49 | return doc.set({ 50 | since: null 51 | }).then(function () { 52 | client.db({ 53 | db: 'mydb' 54 | }); 55 | return null; // prevent runaway promise warning 56 | }); 57 | }); 58 | 59 | it('should reload db', function () { 60 | 61 | var client = new Client(true); 62 | 63 | // Wait for load after next tick to ensure there is no race condition. The following code was 64 | // failing when the DB store loading was triggered at the adapter layer. 65 | return commonUtils.timeout().then(function () { 66 | db = client.db({ 67 | db: 'mydb', 68 | store: new MemAdapter().db('mydb') 69 | }); 70 | return commonUtils.once(db, 'load'); 71 | }); 72 | }); 73 | 74 | it('should throw delta errors', function () { 75 | createClientAndDB(); 76 | 77 | return commonTestUtils.shouldNonPromiseThrow(function () { 78 | db._onDeltaError(new Error('my err')); 79 | }, new Error('my err')); 80 | }); 81 | 82 | it('should find and emit when no changes', function () { 83 | // It is very hard to reliably guarantee the following race condition using e2e testing so we 84 | // test here 85 | var emitted = false; 86 | 87 | createClientAndDB(); 88 | 89 | db._connected = true; // fake 90 | 91 | db._ready = commonUtils.resolveFactory(); // fake 92 | 93 | db._localChanges = commonUtils.resolveFactory([]); // fake 94 | 95 | db._emitChanges = function () { 96 | emitted = true; 97 | }; 98 | 99 | return db._findAndEmitChanges().then(function () { 100 | emitted.should.eql(false); 101 | }); 102 | }); 103 | 104 | it('should find and emit when not connected', function () { 105 | // It is very hard to reliably guarantee the following race condition using e2e testing so we 106 | // test here 107 | var emitted = false; 108 | 109 | createClientAndDB(); 110 | 111 | db._connected = false; // fake 112 | 113 | db._ready = commonUtils.resolveFactory(); // fake 114 | 115 | db._localChanges = commonUtils.resolveFactory([{ 116 | foo: 'bar' 117 | }]); // fake 118 | 119 | db._emitChanges = function () { 120 | emitted = true; 121 | }; 122 | 123 | return db._findAndEmitChanges().then(function () { 124 | emitted.should.eql(false); 125 | }); 126 | }); 127 | 128 | it('should build init msg with filters turned off', function () { 129 | createClientAndDB({ 130 | filter: false 131 | }); 132 | return commonUtils.once(db, 'load').then(function () { 133 | var msg = db._emitInitMsg(); 134 | msg.filter.should.eql(false); 135 | }); 136 | }); 137 | 138 | it('should limit local changes within collection', function () { 139 | 140 | createClientAndDB(); 141 | 142 | var tasks = db.col('tasks'); 143 | 144 | var limit = 2, 145 | n = 0, 146 | promises = []; 147 | 148 | // Populate docs 149 | for (var i = 0; i < 10; i++) { 150 | var task = tasks.doc({ 151 | thing: 'paint' 152 | }); 153 | promises.push(task.save()); 154 | } 155 | 156 | return Promise.all(promises).then(function () { 157 | return db._localChanges(null, null, limit, n); 158 | }).then(function (changes) { 159 | // Make sure changes limited 160 | // changes.changes.length.should.eql(limit); // doesn't work in IE 9 161 | (changes.changes.length === limit).should.eql(true); 162 | }); 163 | }); 164 | 165 | it('should limit local changes across collections', function () { 166 | var promises = [], 167 | limit = 1, 168 | n = 0; 169 | 170 | createClientAndDB(); 171 | 172 | var tasks = db.col('tasks'); 173 | var users = db.col('users'); 174 | 175 | promises.push(tasks.doc({ 176 | thing: 'paint' 177 | }).save()); 178 | 179 | promises.push(users.doc({ 180 | name: 'myuser' 181 | }).save()); 182 | 183 | return Promise.all(promises).then(function () { 184 | return db._localChanges(null, null, limit, n); 185 | }).then(function (changes) { 186 | // Make sure changes limited 187 | // changes.changes.length.should.eql(limit); // doesn't work in IE 9 188 | (changes.changes.length === limit).should.eql(true); 189 | }); 190 | }); 191 | 192 | it('should find and emit changes in batches', function () { 193 | var timesEmitted = 0, 194 | promises = []; 195 | 196 | createClientAndDB(); 197 | 198 | db._batchSize = 3; 199 | 200 | var tasks = db.col('tasks'); 201 | 202 | db._emitChanges = function () { 203 | timesEmitted++; 204 | }; 205 | 206 | // Populate docs 207 | for (var i = 0; i < 10; i++) { 208 | var task = tasks.doc({ 209 | thing: 'paint' + i 210 | }); 211 | promises.push(task.save()); 212 | } 213 | 214 | return Promise.all(promises).then(function () { 215 | return db._findAndEmitAllChangesInBatches(); 216 | }).then(function () { 217 | // ceil(10/3) = 4 218 | // timesEmitted.should.eql(4); // doesn't work in IE 9 219 | (timesEmitted === 4).should.eql(true); 220 | }); 221 | }); 222 | 223 | it('should add role', function () { 224 | createClientAndDB(); 225 | 226 | var mockDocs = function (doc) { 227 | var nowStr = (new Date()).toISOString(); 228 | 229 | var changes = [{ 230 | id: doc.id(), 231 | col: doc._col._name, 232 | name: clientUtils.ATTR_NAME_ROLE, 233 | val: JSON.stringify({ 234 | action: clientUtils.ACTION_ADD, 235 | userUUID: 'user-uuid', 236 | roleName: 'my-role' 237 | }), 238 | up: nowStr, 239 | re: nowStr 240 | }]; 241 | 242 | db._setChanges(changes); 243 | }; 244 | 245 | // Mock recording of docs 246 | db._resolveAfterRoleCreated = function (userUUID, roleName, doc) { 247 | return Promise.all([ 248 | DB.prototype._resolveAfterRoleCreated.apply(this, arguments), 249 | mockDocs(doc) // mock docs after listener binds 250 | ]); 251 | }; 252 | 253 | // Assume success if there is no error 254 | return db.addRole('user-uuid', 'my-role'); 255 | }); 256 | 257 | it('should remove role', function () { 258 | createClientAndDB(); 259 | 260 | var mockDocs = function (doc) { 261 | var nowStr = (new Date()).toISOString(); 262 | 263 | var changes = [{ 264 | id: doc.id(), 265 | col: doc._col._name, 266 | name: clientUtils.ATTR_NAME_ROLE, 267 | val: JSON.stringify({ 268 | action: clientUtils.ACTION_REMOVE, 269 | userUUID: 'user-uuid', 270 | roleName: 'my-role' 271 | }), 272 | up: nowStr, 273 | re: nowStr 274 | }]; 275 | 276 | db._setChanges(changes); 277 | }; 278 | 279 | // Mock recording of docs 280 | db._resolveAfterRoleDestroyed = function (userUUID, roleName, doc) { 281 | return Promise.all([ 282 | DB.prototype._resolveAfterRoleDestroyed.apply(this, arguments), 283 | mockDocs(doc) // mock docs after listener binds 284 | ]); 285 | }; 286 | 287 | // Assume success if there is no error 288 | return db.removeRole('user-uuid', 'my-role'); 289 | }); 290 | 291 | it('should create database', function () { 292 | createClientAndDB(); 293 | // Assume success if there is no error 294 | return db._createDatabase('my-other-db'); 295 | }); 296 | 297 | it('should destroy database', function () { 298 | createClientAndDB(); 299 | // Assume success if there is no error 300 | return db._destroyDatabase('my-other-db'); 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /test/spec/delta-db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DeltaDB = require('../../scripts/delta-db'), 4 | MemAdapter = require('deltadb-orm-nosql/scripts/adapters/mem'), 5 | MockSocket = require('./db/mock-socket'), 6 | DB = require('../../scripts/db'); 7 | 8 | describe('delta-db', function () { 9 | 10 | it('should create and destroy locally only', function () { 11 | var db = new DeltaDB('mydb'); 12 | return db.destroy(true); 13 | }); 14 | 15 | it('should uuid', function () { 16 | var uuid = DeltaDB.uuid(); 17 | (uuid !== null).should.eql(true); 18 | }); 19 | 20 | it('should construct with store', function () { 21 | var adapter = new MemAdapter(); 22 | var dbStore = adapter.db('mydb'); 23 | var db = new DeltaDB('mydb', null, null, null, dbStore); 24 | return db.destroy(true); 25 | }); 26 | 27 | it('should create with url', function () { 28 | DB._SocketClass = MockSocket; // mock socket 29 | var db = new DeltaDB('mydb', 'https://example.com'); 30 | return db.destroy(true); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/spec/doc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Client = require('../../scripts/adapter'), 4 | Doc = require('../../scripts/doc'), 5 | MemAdapter = require('deltadb-orm-nosql/scripts/adapters/mem'), 6 | utils = require('deltadb-common-utils'); 7 | 8 | describe('doc', function () { 9 | 10 | var client = null, 11 | db = null, 12 | tasks = null, 13 | task = null; 14 | 15 | beforeEach(function () { 16 | client = new Client(true); 17 | 18 | db = client.db({ 19 | db: 'mydb', 20 | store: new MemAdapter().db('mydb') 21 | }); 22 | 23 | tasks = db.col('tasks'); 24 | 25 | task = tasks.doc(); 26 | }); 27 | 28 | afterEach(function () { 29 | return db.destroy(true); 30 | }); 31 | 32 | var shouldSaveChange = function (data) { 33 | return task.set(data).then(function () { 34 | 35 | // Get the last change as we are using delete to delete the array items so the first index may 36 | // not be 0 37 | var change = task._dat.changes[task._dat.changes.length - 1]; 38 | 39 | // Make sure value is being set 40 | if (data.thing === null) { 41 | (change.val === null).should.eql(true); 42 | } else { 43 | change.val.should.eql(data.thing); 44 | } 45 | 46 | // Simulate recording 47 | return task._saveChange({ 48 | name: change.name, 49 | val: JSON.stringify(change.val), 50 | up: change.up.toISOString(), 51 | re: change.up.toISOString() 52 | }, false, true); 53 | }).then(function () { 54 | // Make sure change was removed 55 | utils.empty(task._dat.changes).should.eql(true); 56 | }); 57 | }; 58 | 59 | it('should save change', function () { 60 | return shouldSaveChange({ 61 | thing: 'high' 62 | }); 63 | }); 64 | 65 | it('should save destroy change', function () { 66 | return shouldSaveChange({ 67 | thing: 'high' 68 | }).then(function () { 69 | return task.destroy(); 70 | }).then(function () { 71 | 72 | // Get the last change as we are using delete to delete the array items so the first index may 73 | // not be 0 74 | var change = task._dat.changes[task._dat.changes.length - 1]; 75 | 76 | // Simulate recording of destroy 77 | return task._saveChange({ 78 | name: change.name, 79 | up: change.up.toISOString(), 80 | re: change.up.toISOString() 81 | }, false, true); 82 | }).then(function () { 83 | // Make sure change was removed 84 | utils.empty(task._dat.changes).should.eql(true); 85 | }); 86 | }); 87 | 88 | it('should record when remote change has seq', function () { 89 | var updated = new Date(); 90 | 91 | task._dat.changes = [{ 92 | name: 'priority', 93 | val: 'high', 94 | up: updated, 95 | seq: 1 96 | }]; 97 | 98 | task._record('priority', 'high', updated); 99 | }); 100 | 101 | it('should set policy', function () { 102 | 103 | var policy = { 104 | col: { 105 | read: 'somerole' 106 | } 107 | }; 108 | 109 | return task.policy(policy).then(function () { 110 | var doc = task.get(); 111 | doc[Doc._policyName].should.eql(policy); 112 | }); 113 | 114 | }); 115 | 116 | it('should not format change', function () { 117 | // Exclude from changes when already sent 118 | var change = { 119 | sent: new Date() 120 | }; 121 | var now = change.sent.getTime() - 1; 122 | task._formatChange(0, null, null, change, now); 123 | }); 124 | 125 | it('should format false change', function () { 126 | var change = { // fake 127 | val: false, 128 | up: new Date() 129 | }; 130 | 131 | var changes = []; 132 | 133 | task._formatChange(0, null, changes, change); 134 | 135 | changes[0].val.should.eql('false'); 136 | }); 137 | 138 | it('should get existing doc', function () { 139 | // Set task so that id is generated for future lookup 140 | return task.set({ 141 | thing: 'sing' 142 | }).then(function () { 143 | var doc = tasks.doc(task.get()); 144 | doc.should.eql(task); 145 | }); 146 | }); 147 | 148 | it('should save boolean change and record', function () { 149 | return shouldSaveChange({ 150 | thing: false 151 | }); 152 | }); 153 | 154 | it('should save null change and record', function () { 155 | return shouldSaveChange({ 156 | thing: null 157 | }); 158 | }); 159 | 160 | it('should load deleted at timestamp from store', function () { 161 | var now = new Date(); 162 | 163 | // Fake store 164 | var store = { 165 | updatedAt: now, 166 | destroyedAt: now, 167 | recordedAt: now 168 | }; 169 | 170 | task._loadTimestampsFromStore(store); 171 | 172 | // Make sure later timestamps from store 173 | task._dat.updatedAt.should.eql(now); 174 | task._dat.destroyedAt.should.eql(now); 175 | task._dat.recordedAt.should.eql(now); 176 | 177 | }); 178 | 179 | it('should convert to iso string when truthy', function () { 180 | task._toISOStringIfTruthy(new Date('2015-11-17T04:11:38.620Z')) 181 | .should.eql('2015-11-17T04:11:38.620Z'); 182 | (task._toISOStringIfTruthy(null) === null).should.eql(true); 183 | }); 184 | 185 | it('should set even if contains id', function () { 186 | // The id may be present when we work with a copy of the doc's data 187 | return task.set({ 188 | $id: task.id(), 189 | thing: 'sing' 190 | }); 191 | }); 192 | 193 | }); 194 | -------------------------------------------------------------------------------- /test/spec/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO: error event? 4 | 5 | // TODO: split up tests by event 6 | 7 | var commonTestUtils = require('deltadb-common-utils/scripts/test-utils'), 8 | commonUtils = require('deltadb-common-utils'), 9 | testUtils = require('../utils'), 10 | Client = require('../../scripts/adapter'), 11 | Promise = require('bluebird'), 12 | MemAdapter = require('deltadb-orm-nosql/scripts/adapters/mem'); 13 | 14 | describe('events', function () { 15 | 16 | var client = null, 17 | db = null, 18 | tasks = null, 19 | task = null, 20 | client2 = null, 21 | db2 = null; 22 | 23 | var COMMON_TEST_UTILS_WAIT_MS = commonTestUtils.WAIT_MS; 24 | 25 | // Testing with IE in Saucelabs can be VERY slow therefore we need to increase the timeouts 26 | if (global.deltaDBSaucelabs && testUtils.IE) { // Saucelabs and IE? 27 | commonTestUtils.WAIT_MS = 60000; 28 | } else { 29 | commonTestUtils.WAIT_MS = commonTestUtils.WAIT_MS; 30 | } 31 | 32 | after(function () { 33 | commonTestUtils.WAIT_MS = COMMON_TEST_UTILS_WAIT_MS; 34 | }); 35 | 36 | beforeEach(function () { 37 | client = new Client(true); 38 | 39 | db = client.db({ 40 | db: 'mydb', 41 | store: new MemAdapter().db('mydb') // TODO: test with default store? 42 | }); 43 | 44 | tasks = db.col('tasks'); 45 | 46 | task = tasks.doc({ 47 | $id: '1' 48 | }); 49 | }); 50 | 51 | afterEach(function () { 52 | return Promise.all([db.destroy(true), db2 ? db.destroy(true) : null]); 53 | }); 54 | 55 | var Server = function (changes) { 56 | this.queue = function () {}; 57 | 58 | this.changes = function () { 59 | return changes; 60 | }; 61 | }; 62 | 63 | var now = new Date(), 64 | nowStr = now.toISOString(), 65 | before = new Date('2015-01-01'), 66 | beforeStr = before.toISOString(), 67 | later = new Date(now.getTime() + 1), 68 | laterStr = later.toISOString(); 69 | 70 | var eventArgsShouldEql = function (args, id, name, value) { 71 | args[0].name.should.eql(name); 72 | args[0].value.should.eql(value); 73 | args[1].id().should.eql(id); 74 | }; 75 | 76 | var createLocalShouldEql = function (args) { 77 | eventArgsShouldEql(args, '1', 'priority', 'low'); 78 | }; 79 | 80 | var createRemoteShouldEql = function (args) { 81 | eventArgsShouldEql(args, '1', 'thing', 'sing'); 82 | }; 83 | 84 | // ----- 85 | 86 | var createLocal = function () { 87 | task._set('priority', 'low', now); // use _set so we can force a timestamp 88 | return task.save(); // doc not registered with collection until save() 89 | }; 90 | 91 | var attrShouldCreateLocal = function (emitter) { 92 | return commonTestUtils.shouldDoAndOnce(createLocal, emitter, 'attr:create').then(function ( 93 | args) { 94 | createLocalShouldEql(args); 95 | }); 96 | }; 97 | 98 | it('doc: attr:create local', function () { 99 | attrShouldCreateLocal(task); 100 | }); 101 | 102 | it('col: attr:create local', function () { 103 | attrShouldCreateLocal(tasks); 104 | }); 105 | 106 | it('db: attr:create local', function () { 107 | attrShouldCreateLocal(db); 108 | }); 109 | 110 | it('client: attr:create local', function () { 111 | attrShouldCreateLocal(client); 112 | }); 113 | 114 | var createRemote = function () { 115 | var server = new Server([{ 116 | id: '1', 117 | col: 'tasks', 118 | name: 'thing', 119 | val: '"sing"', 120 | seq: 0, 121 | up: nowStr, 122 | re: nowStr 123 | }]); 124 | return db.sync(server, true); 125 | }; 126 | 127 | var attrShouldCreateRemote = function (emitter) { 128 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 129 | return commonTestUtils.shouldDoAndOnce(createRemote, emitter, 'attr:create'); 130 | }).then(function (args) { 131 | createRemoteShouldEql(args); 132 | }); 133 | }; 134 | 135 | it('doc: attr:create remote', function () { 136 | return attrShouldCreateRemote(task); 137 | }); 138 | 139 | it('col: attr:create remote', function () { 140 | return attrShouldCreateRemote(tasks); 141 | }); 142 | 143 | it('db: attr:create remote', function () { 144 | return attrShouldCreateRemote(db); 145 | }); 146 | 147 | it('client: attr:create remote', function () { 148 | return attrShouldCreateRemote(client); 149 | }); 150 | 151 | var createRemoteSame = function () { 152 | var server = new Server([{ 153 | id: '1', 154 | col: 'tasks', 155 | name: 'priority', 156 | val: '"low"', 157 | seq: 0, 158 | up: nowStr, 159 | re: nowStr 160 | }]); 161 | return db.sync(server, true); 162 | }; 163 | 164 | var attrShouldCreateRemoteSame = function (emitter) { 165 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 166 | return commonTestUtils.shouldDoAndNotOnce(createRemoteSame, emitter, 'attr:create'); 167 | }); 168 | }; 169 | 170 | it('doc: attr:create remote same', function () { 171 | // TODO: does this one test cover the code path for all events? 172 | // TODO: better to just test all cases of Doc._event?? Probably! 173 | return attrShouldCreateRemoteSame(task); 174 | }); 175 | 176 | var createRemoteEarlier = function () { 177 | var server = new Server([{ 178 | id: '1', 179 | col: 'tasks', 180 | name: 'priority', 181 | val: '"low"', 182 | seq: 0, 183 | up: beforeStr, 184 | re: beforeStr 185 | }]); 186 | return db.sync(server, true); 187 | }; 188 | 189 | var attrShouldCreateRemoteEarlier = function (emitter) { 190 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 191 | return commonTestUtils.shouldDoAndNotOnce(createRemoteEarlier, emitter, 192 | 'attr:create'); 193 | }); 194 | }; 195 | 196 | it('doc: attr:create remote earlier', function () { 197 | // TODO: does this one test cover the code path for all events? 198 | // TODO: better to just test all cases of Doc._event?? Probably! 199 | return attrShouldCreateRemoteEarlier(task); 200 | }); 201 | 202 | // ------------------------ 203 | 204 | var updateLocal = function () { 205 | return testUtils.sleep().then(function () { // sleep so update is after create 206 | task.set({ 207 | 'priority': 'high' 208 | }); // use _set so we can force a timestamp 209 | return null; // prevent runaway promise warnings 210 | }); 211 | }; 212 | 213 | var updateShouldEql = function (args) { 214 | eventArgsShouldEql(args, '1', 'priority', 'high'); 215 | }; 216 | 217 | var attrShouldUpdateLocal = function (emitter) { 218 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 219 | return commonTestUtils.shouldDoAndOnce(updateLocal, emitter, 'attr:update'); 220 | }).then(function (args) { 221 | updateShouldEql(args); 222 | }); 223 | }; 224 | 225 | it('doc: attr:update local', function () { 226 | return attrShouldUpdateLocal(task); 227 | }); 228 | 229 | it('col: attr:update local', function () { 230 | return attrShouldUpdateLocal(tasks); 231 | }); 232 | 233 | it('db: attr:update local', function () { 234 | return attrShouldUpdateLocal(db); 235 | }); 236 | 237 | it('client: attr:update local', function () { 238 | return attrShouldUpdateLocal(client); 239 | }); 240 | 241 | var updateRemote = function () { 242 | var server = new Server([{ 243 | id: '1', 244 | col: 'tasks', 245 | name: 'priority', 246 | val: '"high"', 247 | seq: 0, 248 | up: laterStr, 249 | re: laterStr 250 | }]); 251 | return db.sync(server, true); 252 | }; 253 | 254 | var attrShouldUpdateRemote = function (emitter) { 255 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 256 | return commonTestUtils.shouldDoAndOnce(updateRemote, emitter, 'attr:update'); 257 | }).then(function (args) { 258 | updateShouldEql(args); 259 | }); 260 | }; 261 | 262 | it('doc: attr:update remote', function () { 263 | return attrShouldUpdateRemote(task); 264 | }); 265 | 266 | it('col: attr:update remote', function () { 267 | return attrShouldUpdateRemote(tasks); 268 | }); 269 | 270 | it('db: attr:update remote', function () { 271 | return attrShouldUpdateRemote(db); 272 | }); 273 | 274 | it('client: attr:update remote', function () { 275 | return attrShouldUpdateRemote(client); 276 | }); 277 | 278 | // ------------------------ 279 | 280 | var destroyLocal = function () { 281 | return testUtils.sleep().then(function () { // sleep so destroy is after create 282 | task.unset('priority'); // use _set so we can force a timestamp 283 | return null; // prevent runaway promise warnings 284 | }); 285 | }; 286 | 287 | var attrShouldDestroyLocal = function (emitter) { 288 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 289 | return commonTestUtils.shouldDoAndOnce(destroyLocal, emitter, 'attr:destroy'); 290 | }).then(function (args) { 291 | createLocalShouldEql(args); 292 | }); 293 | }; 294 | 295 | it('doc: attr:destroy local', function () { 296 | return attrShouldDestroyLocal(task); 297 | }); 298 | 299 | it('col: attr:destroy local', function () { 300 | return attrShouldDestroyLocal(tasks); 301 | }); 302 | 303 | it('db: attr:destroy local', function () { 304 | return attrShouldDestroyLocal(db); 305 | }); 306 | 307 | it('client: attr:destroy local', function () { 308 | return attrShouldDestroyLocal(client); 309 | }); 310 | 311 | var destroyRemote = function () { 312 | var server = new Server([{ 313 | id: '1', 314 | col: 'tasks', 315 | name: 'priority', 316 | seq: 0, 317 | up: laterStr, 318 | re: laterStr 319 | }]); 320 | 321 | return testUtils.sleep().then(function () { // sleep so destroy is after create 322 | return db.sync(server, true); 323 | }); 324 | }; 325 | 326 | var attrShouldDestroyRemote = function (emitter) { 327 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 328 | return commonTestUtils.shouldDoAndOnce(destroyRemote, emitter, 'attr:destroy'); 329 | }).then(function (args) { 330 | createLocalShouldEql(args); 331 | }); 332 | }; 333 | 334 | it('doc: attr:destroy remote', function () { 335 | return attrShouldDestroyRemote(task); 336 | }); 337 | 338 | it('col: attr:destroy remote', function () { 339 | return attrShouldDestroyRemote(tasks); 340 | }); 341 | 342 | it('db: attr:destroy remote', function () { 343 | return attrShouldDestroyRemote(db); 344 | }); 345 | 346 | it('client: attr:destroy remote', function () { 347 | return attrShouldDestroyRemote(client); 348 | }); 349 | 350 | // ------------------------ 351 | 352 | var recordRemote = function () { 353 | var server = new Server([{ 354 | id: '1', 355 | col: 'tasks', 356 | name: 'priority', 357 | val: '"low"', 358 | seq: 0, 359 | up: nowStr, 360 | re: laterStr 361 | }]); 362 | 363 | return testUtils.sleep().then(function () { // sleep so record is after create 364 | return db.sync(server, true); 365 | }); 366 | }; 367 | 368 | var attrShouldRecord = function (emitter) { 369 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 370 | return commonTestUtils.shouldDoAndOnce(recordRemote, emitter, 'attr:record'); 371 | }).then(function (args) { 372 | createLocalShouldEql(args); 373 | }); 374 | }; 375 | 376 | it('doc: attr:record', function () { 377 | return attrShouldRecord(task); 378 | }); 379 | 380 | it('col: attr:record', function () { 381 | return attrShouldRecord(tasks); 382 | }); 383 | 384 | it('db: attr:record', function () { 385 | return attrShouldRecord(db); 386 | }); 387 | 388 | it('client: attr:record', function () { 389 | return attrShouldRecord(client); 390 | }); 391 | 392 | // ------------------------ 393 | 394 | var docCreateShouldEql = function (args) { 395 | args[0].should.eql(task); 396 | }; 397 | 398 | var docShouldCreateLocal = function (emitter) { 399 | return commonTestUtils.shouldDoAndOnce(createLocal, emitter, 'doc:create').then(function ( 400 | args) { 401 | docCreateShouldEql(args); 402 | }); 403 | }; 404 | 405 | it('doc: doc:create local', function () { 406 | return docShouldCreateLocal(task); 407 | }); 408 | 409 | it('col: doc:create local', function () { 410 | return docShouldCreateLocal(tasks); 411 | }); 412 | 413 | it('db: doc:create local', function () { 414 | return docShouldCreateLocal(db); 415 | }); 416 | 417 | it('client: doc:create local', function () { 418 | return docShouldCreateLocal(client); 419 | }); 420 | 421 | var argsShouldEqlTask = function (args) { 422 | return tasks.get('1').then(function (newTask) { 423 | args[0].should.eql(newTask); 424 | }); 425 | }; 426 | 427 | var docShouldCreateRemote = function (emitter) { 428 | return commonTestUtils.shouldDoAndOnce(createRemote, emitter, 'doc:create').then(function ( 429 | args) { 430 | return argsShouldEqlTask(args); 431 | }); 432 | }; 433 | 434 | it('doc: doc:create remote already local', function () { 435 | return commonUtils.doAndOnce(createLocal, task, 'doc:create').then(function () { 436 | // Assert doc:create not received as already created 437 | return commonTestUtils.shouldDoAndNotOnce(createRemote, task, 'doc:create'); 438 | }); 439 | }); 440 | 441 | it('col: doc:create remote', function () { 442 | return docShouldCreateRemote(tasks); 443 | }); 444 | 445 | it('db: doc:create remote', function () { 446 | return docShouldCreateRemote(db); 447 | }); 448 | 449 | it('client: doc:create remote', function () { 450 | return docShouldCreateRemote(client); 451 | }); 452 | 453 | // ------------------------ 454 | 455 | var docShouldUpdateLocal = function (emitter) { 456 | return commonTestUtils.shouldDoAndOnce(updateLocal, emitter, 'doc:update').then(function ( 457 | args) { 458 | args[0].should.eql(task); 459 | }); 460 | }; 461 | 462 | it('doc: doc:update local', function () { 463 | return docShouldUpdateLocal(task); 464 | }); 465 | 466 | it('col: doc:update local', function () { 467 | return docShouldUpdateLocal(tasks); 468 | }); 469 | 470 | it('db: doc:update local', function () { 471 | return docShouldUpdateLocal(db); 472 | }); 473 | 474 | it('client: doc:update local', function () { 475 | return docShouldUpdateLocal(client); 476 | }); 477 | 478 | var docShouldUpdateRemote = function (emitter) { 479 | return commonUtils.doAndOnce(createLocal, emitter, 'doc:create').then(function () { 480 | return commonTestUtils.shouldDoAndOnce(updateRemote, emitter, 'doc:update'); 481 | }).then(function (args) { 482 | return argsShouldEqlTask(args); 483 | }); 484 | }; 485 | 486 | it('doc: doc:update remote', function () { 487 | return docShouldUpdateRemote(task); 488 | }); 489 | 490 | it('col: doc:update remote', function () { 491 | return docShouldUpdateRemote(tasks); 492 | }); 493 | 494 | it('db: doc:update remote', function () { 495 | return docShouldUpdateRemote(db); 496 | }); 497 | 498 | it('client: doc:update remote', function () { 499 | return docShouldUpdateRemote(client); 500 | }); 501 | 502 | // ------------------------ 503 | 504 | var docDestroyShouldEql = function (args) { 505 | docCreateShouldEql(args); 506 | }; 507 | 508 | var destroyDocLocal = function () { 509 | return testUtils.sleep().then(function () { // sleep so destroy is after create 510 | return task.destroy(); 511 | }); 512 | }; 513 | 514 | var docShouldDestroyLocal = function (emitter) { 515 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 516 | return commonTestUtils.shouldDoAndOnce(destroyDocLocal, emitter, 'doc:destroy'); 517 | }).then(function (args) { 518 | docDestroyShouldEql(args); 519 | }); 520 | }; 521 | 522 | it('doc: doc:destroy local', function () { 523 | return docShouldDestroyLocal(task); 524 | }); 525 | 526 | it('col: doc:destroy local', function () { 527 | return docShouldDestroyLocal(tasks); 528 | }); 529 | 530 | it('db: doc:destroy local', function () { 531 | return docShouldDestroyLocal(db); 532 | }); 533 | 534 | it('client: doc:destroy local', function () { 535 | return docShouldDestroyLocal(client); 536 | }); 537 | 538 | var destroyDocRemote = function () { 539 | var server = new Server([{ 540 | id: '1', 541 | col: 'tasks', 542 | name: null, 543 | seq: 0, 544 | up: laterStr, 545 | re: laterStr 546 | }]); 547 | 548 | return testUtils.sleep().then(function () { // sleep so destroy is after create 549 | return db.sync(server, true); 550 | }); 551 | }; 552 | 553 | var docShouldDestroyRemote = function (emitter) { 554 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 555 | return commonTestUtils.shouldDoAndOnce(destroyDocRemote, emitter, 'doc:destroy'); 556 | }).then(function (args) { 557 | docDestroyShouldEql(args); 558 | }); 559 | }; 560 | 561 | it('doc: doc:destroy remote', function () { 562 | return docShouldDestroyRemote(task); 563 | }); 564 | 565 | it('col: doc:destroy remote', function () { 566 | return docShouldDestroyRemote(tasks); 567 | }); 568 | 569 | it('db: doc:destroy remote', function () { 570 | return docShouldDestroyRemote(db); 571 | }); 572 | 573 | it('client: doc:destroy remote', function () { 574 | return docShouldDestroyRemote(client); 575 | }); 576 | 577 | // ------------------------ 578 | 579 | var docShouldRecord = function (emitter) { 580 | return commonUtils.doAndOnce(createLocal, emitter, 'doc:create').then(function () { 581 | return commonTestUtils.shouldDoAndOnce(recordRemote, emitter, 'doc:record'); 582 | }).then(function (args) { 583 | docCreateShouldEql(args); 584 | }); 585 | }; 586 | 587 | it('doc: doc:record', function () { 588 | return docShouldRecord(task); 589 | }); 590 | 591 | it('col: doc:record', function () { 592 | return docShouldRecord(tasks); 593 | }); 594 | 595 | it('db: doc:record', function () { 596 | return docShouldRecord(db); 597 | }); 598 | 599 | it('client: doc:record', function () { 600 | return docShouldRecord(client); 601 | }); 602 | 603 | // ------------------------ 604 | 605 | var tasks2 = null; 606 | 607 | var colCreateShouldEql = function (args) { 608 | args[0].should.eql(tasks2); 609 | }; 610 | 611 | var colCreateLocal = function () { 612 | tasks2 = db.col('tasks2'); 613 | return Promise.resolve(); 614 | }; 615 | 616 | var colShouldCreateLocal = function (emitter) { 617 | return commonTestUtils.shouldDoAndOnce(colCreateLocal, emitter, 'col:create').then( 618 | function ( 619 | args) { 620 | colCreateShouldEql(args); 621 | }); 622 | }; 623 | 624 | // Note: no col:create at col layer as col:create emitted immediately after db.col() 625 | 626 | it('db: col:create local', function () { 627 | return colShouldCreateLocal(db); 628 | }); 629 | 630 | it('client: col:create local', function () { 631 | return colShouldCreateLocal(client); 632 | }); 633 | 634 | var colCreateRemote = function () { 635 | var server = new Server([{ 636 | id: '2', 637 | col: 'tasks2', 638 | name: 'thing', 639 | val: '"sing"', 640 | seq: 0, 641 | up: nowStr, 642 | re: nowStr 643 | }]); 644 | return db.sync(server, true); 645 | }; 646 | 647 | var colShouldCreateRemote = function (emitter) { 648 | return commonTestUtils.shouldDoAndOnce(colCreateRemote, emitter, 'col:create').then( 649 | function ( 650 | args) { 651 | return args[0].get('2'); 652 | }).then(function (doc) { 653 | var obj = doc.get(); 654 | obj.thing.should.eql('sing'); 655 | }); 656 | }; 657 | 658 | it('db: col:create remote', function () { 659 | return colShouldCreateRemote(db); 660 | }); 661 | 662 | it('client: col:create remote', function () { 663 | return colShouldCreateRemote(client); 664 | }); 665 | 666 | // ------------------------ 667 | 668 | var colShouldUpdateLocal = function (emitter) { 669 | return commonTestUtils.shouldDoAndOnce(updateLocal, emitter, 'col:update').then(function ( 670 | args) { 671 | return args[0].get('1'); 672 | }).then(function (doc) { 673 | var obj = doc.get(); 674 | obj.priority.should.eql('high'); 675 | }); 676 | }; 677 | 678 | it('col: col:update local', function () { 679 | return colShouldUpdateLocal(tasks); 680 | }); 681 | 682 | it('db: col:update local', function () { 683 | return colShouldUpdateLocal(db); 684 | }); 685 | 686 | it('client: col:update local', function () { 687 | return colShouldUpdateLocal(client); 688 | }); 689 | 690 | var colShouldUpdateRemote = function (emitter) { 691 | // We cannot first listen to col:create as the col was created with db.col() 692 | return commonUtils.doAndOnce(createLocal, emitter, 'doc:create').then(function () { 693 | return commonTestUtils.shouldDoAndOnce(updateRemote, emitter, 'col:update'); 694 | }).then(function (args) { 695 | return args[0].get('1'); 696 | }).then(function (doc) { 697 | var obj = doc.get(); 698 | obj.priority.should.eql('high'); 699 | }); 700 | }; 701 | 702 | it('col: col:update remote', function () { 703 | return colShouldUpdateRemote(tasks); 704 | }); 705 | 706 | it('db: col:update remote', function () { 707 | return colShouldUpdateRemote(db); 708 | }); 709 | 710 | it('client: col:update remote', function () { 711 | return colShouldUpdateRemote(client); 712 | }); 713 | 714 | // ------------------------ 715 | 716 | var destroyColLocal = function () { 717 | return testUtils.sleep().then(function () { // sleep so destroy is after create 718 | return tasks.destroy(); 719 | }); 720 | }; 721 | 722 | var colShouldDestroyLocal = function (emitter) { 723 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 724 | return commonTestUtils.shouldDoAndOnce(destroyColLocal, emitter, 'col:destroy'); 725 | }).then(function (args) { 726 | return args[0].get('1'); 727 | }).then(function (doc) { 728 | var obj = doc.get(); 729 | obj.priority.should.eql('low'); 730 | }); 731 | }; 732 | 733 | it('col: col:destroy local', function () { 734 | return colShouldDestroyLocal(tasks); 735 | }); 736 | 737 | it('db: col:destroy local', function () { 738 | return colShouldDestroyLocal(db); 739 | }); 740 | 741 | it('client: col:destroy local', function () { 742 | return colShouldDestroyLocal(client); 743 | }); 744 | 745 | // TODO: create construct for passing col destroy via delta? e.g. { name: '$col', value: null } so 746 | // that we can emit col:destroy when receive this delta? 747 | 748 | // ------------------------ 749 | 750 | var colShouldRecord = function (emitter) { 751 | return commonUtils.doAndOnce(createLocal, emitter, 'doc:create').then(function () { 752 | return commonTestUtils.shouldDoAndOnce(recordRemote, emitter, 'col:record'); 753 | }).then(function (args) { 754 | return args[0].get('1'); 755 | }).then(function (doc) { 756 | var obj = doc.get(); 757 | obj.priority.should.eql('low'); 758 | }); 759 | }; 760 | 761 | it('col: col:record', function () { 762 | return colShouldRecord(tasks); 763 | }); 764 | 765 | it('db: col:record', function () { 766 | return colShouldRecord(db); 767 | }); 768 | 769 | it('client: col:record', function () { 770 | return colShouldRecord(client); 771 | }); 772 | 773 | // ------------------------ 774 | 775 | var dbCreateLocal = function () { 776 | db2 = client2.db({ 777 | db: 'myotherdb', 778 | store: new MemAdapter().db('mydb') // TODO: test with default store? 779 | }); 780 | return Promise.resolve(); 781 | }; 782 | 783 | var dbShouldCreateLocal = function () { 784 | client2 = new Client(true); 785 | return commonTestUtils.shouldDoAndOnce(dbCreateLocal, client2, 'db:create').then(function ( 786 | args) { 787 | args[0].should.eql(db2); 788 | }); 789 | }; 790 | 791 | it('client: db:create local', function () { 792 | return dbShouldCreateLocal(); 793 | }); 794 | 795 | // TODO: how can the client detect that a database has been created remotely? Currently, syncing 796 | // is done per database and instead we would require a notification at the layer above. 797 | 798 | // ------------------------ 799 | 800 | // Note: we don't support db:update as there is no database rename function and no other way to 801 | // update a database. 802 | 803 | // ------------------------ 804 | 805 | // var dbDestroyLocal = function () { 806 | // return client2.connect({ 807 | // db: 'myotherdb' 808 | // }).then(function (_db2) { 809 | // db2 = _db2; 810 | // }).then(function () { 811 | // return db2.destroy(); 812 | // }); 813 | // }; 814 | 815 | // var dbShouldDestroyLocal = function () { 816 | // return commonTestUtils.shouldDoAndOnce(dbDestroyLocal, client2, 'db:destroy').then(function ( 817 | // args) { 818 | // args[0].should.eql(db2); 819 | // }); 820 | // }; 821 | 822 | // TODO: need to first implement db.destroy() 823 | // it('client: db:destroy local', function () { 824 | // return dbShouldDestroyLocal(); 825 | // }); 826 | 827 | // TODO: how can the client detect that a database has been destroyed remotely? Currently, syncing 828 | // is done per database and instead we would require a notification at the layer above. 829 | 830 | // ------------------------ 831 | 832 | var dbShouldRecord = function (emitter) { 833 | return commonUtils.doAndOnce(createLocal, emitter, 'attr:create').then(function () { 834 | return commonTestUtils.shouldDoAndOnce(recordRemote, emitter, 'db:record'); 835 | }).then(function (args) { 836 | args[0].should.eql(db); 837 | }); 838 | }; 839 | 840 | it('db: db:record', function () { 841 | return dbShouldRecord(db); 842 | }); 843 | 844 | it('client: db:record', function () { 845 | return dbShouldRecord(client); 846 | }); 847 | 848 | }); 849 | -------------------------------------------------------------------------------- /test/spec/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../utils'); 4 | 5 | describe('spec', function () { 6 | 7 | this.timeout(utils.TIMEOUT); 8 | 9 | require('./adapter'); 10 | require('./client'); 11 | require('./config'); 12 | require('./db'); 13 | require('./delta-db'); 14 | require('./doc'); 15 | require('./events'); 16 | require('./multiple'); 17 | require('./persist'); 18 | require('./sender'); 19 | require('./socket'); 20 | require('./utils'); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /test/spec/multiple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var commonTestUtils = require('deltadb-common-utils/scripts/test-utils'), 4 | Client = require('../../scripts/adapter'), 5 | Promise = require('bluebird'), 6 | MemAdapter = require('deltadb-orm-nosql/scripts/adapters/mem'); 7 | 8 | describe('multiple', function () { 9 | 10 | var client1 = null, 11 | client2 = null, 12 | db1 = null, 13 | db2 = null, 14 | col1 = null, 15 | col2 = null, 16 | doc1 = null, 17 | doc2 = null; 18 | 19 | beforeEach(function () { 20 | client1 = new Client(true); // local only so no connection to server 21 | client2 = new Client(true); // local only so no connection to server 22 | 23 | db1 = client1.db({ 24 | db: 'mydb', 25 | store: new MemAdapter().db('mydb') 26 | }); 27 | db2 = client2.db({ 28 | db: 'mydb', 29 | store: new MemAdapter().db('mydb') 30 | }); 31 | col1 = db1.col('mycol'); 32 | col2 = db2.col('mycol'); 33 | doc1 = col1.doc(); 34 | doc2 = col2.doc(); 35 | }); 36 | 37 | afterEach(function () { 38 | return Promise.all([db1.destroy(true), db2.destroy(true)]); 39 | }); 40 | 41 | // Note: don't need afterEach as everything created in mem and therefore doesn't need to be purged 42 | 43 | it('should have unique event emitters', function () { 44 | 45 | // Note: the following test was failing with as Doc defined an attribute called "_events" which 46 | // was also in use by EventEmitter. TODO: to prevent this in the future, should Doc contain 47 | // EventEmitter and just provide access functions? 48 | 49 | var promiseFactory = function () { 50 | doc1.emit('test-event'); 51 | return Promise.resolve(); 52 | }; 53 | 54 | return commonTestUtils.shouldDoAndNotOnce(promiseFactory, doc2, 'test-event'); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /test/spec/persist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var commonUtils = require('deltadb-common-utils'), 4 | Client = require('../../scripts/adapter'), 5 | testUtils = require('../utils'); 6 | 7 | describe('persist', function () { 8 | 9 | var client = null, 10 | db = null, 11 | tasks = null, 12 | propsReady = null, 13 | db2 = null; 14 | 15 | beforeEach(function () { 16 | client = new Client(true); 17 | db = client.db({ 18 | db: 'mydb' 19 | }); 20 | propsReady = commonUtils.once(db, 'load'); 21 | tasks = db.col('tasks'); 22 | }); 23 | 24 | afterEach(function () { 25 | // In these tests db and db2 reference the same underlying DB so we do not need to destroy db2. 26 | // Otherwise, the destroy would block. 27 | return db.destroy(true); 28 | }); 29 | 30 | it('should restore from store', function () { 31 | 32 | var client2 = null, 33 | found = false, 34 | task = tasks.doc(), 35 | dbLoaded = commonUtils.once(db, 'load'); 36 | 37 | var nowStr = (new Date().toISOString()); 38 | 39 | return task.set({ 40 | thing: 'sing', 41 | priority: 'high' 42 | }).then(function () { 43 | // Wait until all the data has been loaded from the store 44 | return dbLoaded; 45 | }).then(function () { 46 | // Fake update of since 47 | return db._props.set({ 48 | since: nowStr 49 | }); 50 | }).then(function () { 51 | // Simulate a reload from store, e.g. when an app restarts, by destroying the DB, but keeping 52 | // the local store and then reloading the store 53 | return db.destroy(true, true); 54 | }).then(function () { 55 | client2 = new Client(true); 56 | db2 = client2.db({ 57 | db: 'mydb' 58 | }); 59 | 60 | // Wait until all the docs have been loaded from the store 61 | return commonUtils.once(db2, 'load'); 62 | }).then(function () { 63 | // Verify restoration of since 64 | var props = db2._props.get(); 65 | props.since.should.eql(nowStr); 66 | 67 | // Verify task was loaded 68 | db2.all(function (tasks) { 69 | // Assuming only col is tasks 70 | tasks.find(null, function (task2) { 71 | // Check that all data, e.g. changes, latest, etc... was reloaded, not just the values 72 | task2._dat.should.eql(task._dat); 73 | found = true; 74 | }, true); // include destroyed docs 75 | }); 76 | 77 | found.should.eql(true); 78 | return null; // prevent runaway promise warning 79 | }); 80 | }); 81 | 82 | it('should handle race conditions', function () { 83 | // Make sure there are no race conditions with loading, e.g. 84 | // planner = client.db('planner'); 85 | // tasks = planner.col('tasks'); 86 | // write = tasks.doc({ thing: 'write' }); 87 | // What if thing is already in the store and loads after we have the handles above? 88 | 89 | var task = tasks.doc({ 90 | thing: 'sing', 91 | priority: 'high', 92 | notes: 'some notes' 93 | }), 94 | client2 = null, 95 | tasks2 = null, 96 | task2 = null; 97 | 98 | var setUpClient2 = function () { 99 | client2 = new Client(true); 100 | db2 = client2.db({ 101 | db: 'mydb' 102 | }); 103 | tasks2 = db2.col('tasks'); 104 | task2 = tasks2.doc({ 105 | $id: task.id(), 106 | thing: 'write', 107 | type: 'personal' 108 | }); 109 | task2.unset('notes'); 110 | }; 111 | 112 | // Populate underlying store 113 | return task.save().then(function () { 114 | // Sleep so that timestamps aren't the same and the 2nd set of changes come later 115 | return testUtils.sleep(); 116 | }).then(function () { 117 | // Simulate reload using a second client 118 | setUpClient2(); 119 | return commonUtils.once(db2, 'load'); 120 | }).then(function () { 121 | // Make sure that we take the latest changes 122 | task2.get().should.eql({ 123 | $id: task.id(), 124 | thing: 'write', 125 | type: 'personal', 126 | priority: 'high' 127 | }); 128 | }); 129 | 130 | }); 131 | 132 | }); 133 | -------------------------------------------------------------------------------- /test/spec/sender.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Sender = require('../../scripts/sender'), 4 | Promise = require('bluebird'); 5 | 6 | describe('sender', function () { 7 | 8 | it('should launch retry sender', function () { 9 | var db = { // fake 10 | _retryAfterMSecs: 10 11 | }; 12 | 13 | var sends = 0, 14 | expectedSends = 2; 15 | 16 | var sender = new Sender(db); 17 | 18 | // The test will timeout if something goes wrong 19 | return new Promise(function (resolve) { 20 | sender.send = function () { // fake 21 | if (++sends === expectedSends) { 22 | resolve(); 23 | } 24 | }; 25 | }); 26 | 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/spec/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Socket = require('../../scripts/socket'); 4 | 5 | describe('socket', function () { 6 | 7 | var socket = new Socket(); 8 | 9 | var mockSocket = { 10 | emit: function () {}, 11 | on: function () {}, 12 | disconnect: function () {} 13 | }; 14 | 15 | var mockIO = function () { 16 | socket._io = { 17 | connect: function () { 18 | return mockSocket; 19 | } 20 | }; 21 | }; 22 | 23 | mockIO(); 24 | 25 | it('should connect', function () { 26 | socket.connect(); 27 | }); 28 | 29 | it('should emit', function () { 30 | socket.emit(); 31 | }); 32 | 33 | it('should on', function () { 34 | socket.on(); 35 | }); 36 | 37 | it('should disconnect', function () { 38 | socket.disconnect(); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /test/spec/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var clientUtils = require('../../scripts/utils'), 4 | commonUtils = require('deltadb-common-utils'), 5 | commonTestUtils = require('deltadb-common-utils/scripts/test-utils'); 6 | 7 | describe('utils', function () { 8 | 9 | var shouldGenUser = function (status) { 10 | return clientUtils.genUser('user-uuid', 'username', 'secret', status).then(function (user) { 11 | user.uuid.should.eql('user-uuid'); 12 | user.username.should.eql('username'); 13 | 14 | if (typeof status === 'undefined') { 15 | user.status.should.eql('enabled'); 16 | } else { 17 | user.status.should.eql(status); 18 | } 19 | 20 | (user.salt === null).should.eql(false); 21 | (user.password === null).should.eql(false); 22 | }); 23 | }; 24 | 25 | it('should gen user without status', function () { 26 | return shouldGenUser(); 27 | }); 28 | 29 | it('should gen user with status', function () { 30 | return shouldGenUser(true); 31 | }); 32 | 33 | it('should convert to doc uuid', function () { 34 | var docUUID = clientUtils.toDocUUID('user-uuid'); 35 | docUUID.should.eql(clientUtils.UUID_PRE + 'user-uuid'); 36 | }); 37 | 38 | it('should sleep', function () { 39 | var before = new Date(); 40 | return commonUtils.timeout(1000).then(function () { 41 | var after = new Date(); 42 | var elapsed = after.getTime() - before.getTime(); 43 | (elapsed >= 1000 && elapsed < 1500).should.eql(true); // allow for 500 ms window 44 | }); 45 | }); 46 | 47 | it('should throw when db name is invalid', function () { 48 | return commonTestUtils.shouldNonPromiseThrow(function () { 49 | clientUtils.escapeDBName('my^#invalid$ db%name!'); 50 | }, new Error()); 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var commonUtils = require('deltadb-common-utils'); 4 | 5 | var Utils = function () {}; 6 | 7 | Utils.prototype.IE = global.window && (navigator.appName === 'Microsoft Internet Explorer' || 8 | 'ActiveXObject' in window); 9 | 10 | // Testing with IE in Saucelabs can be VERY slow therefore we need to increase the timeouts 11 | if (global.deltaDBSaucelabs && Utils.prototype.IE) { // Saucelabs and IE? 12 | Utils.prototype.TIMEOUT = 120000; 13 | } else { 14 | Utils.prototype.TIMEOUT = 30000; 15 | } 16 | 17 | Utils.prototype.sleep = function (sleepMs) { 18 | // Ensure a different timestamp will be generated after this function resolves. 19 | // Occasionally, using timeout(1) will not guarantee a different timestamp, e.g.: 20 | // 1. (new Date()).getTime() 21 | // 2. timeout(1) 22 | // 3. (new Date()).getTime() 23 | // It is not clear as to what causes this but the solution is to sleep longer. This function is 24 | // also used to delay between DB writes to create predictable patterns. In this case it may be 25 | // that the DB adapter processes queries out of sequence. 26 | return commonUtils.timeout(sleepMs ? sleepMs : 10); 27 | }; 28 | 29 | Utils.prototype.allShouldEql = function (collection, expected) { 30 | // Index data as order is guaranteed 31 | 32 | var allDocs = {}; 33 | 34 | var allExpDocs = {}; 35 | expected.forEach(function (exp) { 36 | allExpDocs[exp.$id] = exp; 37 | }); 38 | 39 | return collection.all(function (item) { 40 | allDocs[item.id()] = item.get(); 41 | }).then(function () { 42 | allDocs.should.eql(allExpDocs); 43 | }); 44 | }; 45 | 46 | module.exports = new Utils(); 47 | --------------------------------------------------------------------------------