├── .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 [](https://travis-ci.org/delta-db/deltadb) [](https://coveralls.io/github/delta-db/deltadb?branch=master) [](https://david-dm.org/delta-db/deltadb)
2 | ===
3 | [](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 |
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 |
--------------------------------------------------------------------------------