├── test_lac
├── openFdsTestFile
├── openFdsTestFile2
├── openFdsLaunch.sh
├── openFds.test.js
└── loadAndCrash.test.js
├── test
├── mocha.opts
├── customUtil.test.js
├── executor.test.js
└── indexes.test.js
├── index.js
├── browser-version
├── package.json
├── test
│ ├── playground.html
│ ├── testPersistence.html
│ ├── testPersistence2.html
│ ├── testLoad.html
│ ├── testPersistence.js
│ ├── index.html
│ ├── testPersistence2.js
│ ├── mocha.css
│ ├── testLoad.js
│ ├── nedb-browser.js
│ ├── async.js
│ └── underscore.min.js
├── browser-specific
│ └── lib
│ │ ├── customUtils.js
│ │ └── storage.js
└── build.js
├── bower.json
├── .gitignore
├── lib
├── customUtils.js
├── executor.js
├── storage.js
├── cursor.js
├── indexes.js
├── persistence.js
├── datastore.js
└── model.js
├── benchmarks
├── find.js
├── findWithIn.js
├── findOne.js
├── insert.js
├── loadDatabase.js
├── remove.js
├── update.js
├── ensureIndex.js
└── commonUtilities.js
├── LICENSE
└── package.json
/test_lac/openFdsTestFile:
--------------------------------------------------------------------------------
1 | Random stuff
2 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter spec
2 | --timeout 30000
--------------------------------------------------------------------------------
/test_lac/openFdsTestFile2:
--------------------------------------------------------------------------------
1 | Some other random stuff
2 |
--------------------------------------------------------------------------------
/test_lac/openFdsLaunch.sh:
--------------------------------------------------------------------------------
1 | ulimit -n 128
2 | node ./test_lac/openFds.test.js
3 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var Datastore = require('./lib/datastore');
2 |
3 | module.exports = Datastore;
4 |
--------------------------------------------------------------------------------
/browser-version/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "async": "~0.2.9",
4 | "fs-extra": "~0.6.3",
5 | "uglify-js": "~2.3.6",
6 | "browserify": "~2.25.0"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nedb",
3 | "description": "The Javascript Database for Node, nwjs, Electron and the browser",
4 | "ignore": ["benchmarks", "lib", "test", "test_lac"],
5 | "main": ["browser-version/nedb.js", "browser-version/nedb.min.js"]
6 | }
7 |
--------------------------------------------------------------------------------
/browser-version/test/playground.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Playground for NeDB
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib-cov
2 | *.seed
3 | *.log
4 | *.csv
5 | *.dat
6 | *.out
7 | *.pid
8 | *.gz
9 | .idea
10 | pids
11 | logs
12 | results
13 |
14 | npm-debug.log
15 | workspace
16 | node_modules
17 |
18 | browser-version/src
19 | browser-version/node_modules
20 |
21 | *.swp
22 | *~
23 | *.swo
--------------------------------------------------------------------------------
/browser-version/test/testPersistence.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test NeDB persistence in the browser
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/browser-version/test/testPersistence2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test NeDB persistence in the browser - Results
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/browser-version/test/testLoad.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test NeDB persistence load in the browser
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/browser-version/test/testPersistence.js:
--------------------------------------------------------------------------------
1 | console.log("Beginning tests");
2 | console.log("Please note these tests work on Chrome latest, might not work on other browsers due to discrepancies in how local storage works for the file:// protocol");
3 |
4 | function testsFailed () {
5 | document.getElementById("results").innerHTML = "TESTS FAILED";
6 | }
7 |
8 | var filename = 'test';
9 |
10 | var db = new Nedb({ filename: filename, autoload: true });
11 | db.remove({}, { multi: true }, function () {
12 | db.insert({ hello: 'world' }, function (err) {
13 | if (err) {
14 | testsFailed();
15 | return;
16 | }
17 |
18 | window.location = './testPersistence2.html';
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/browser-version/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mocha tests for NeDB
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lib/customUtils.js:
--------------------------------------------------------------------------------
1 | var crypto = require('crypto')
2 | ;
3 |
4 | /**
5 | * Return a random alphanumerical string of length len
6 | * There is a very small probability (less than 1/1,000,000) for the length to be less than len
7 | * (il the base64 conversion yields too many pluses and slashes) but
8 | * that's not an issue here
9 | * The probability of a collision is extremely small (need 3*10^12 documents to have one chance in a million of a collision)
10 | * See http://en.wikipedia.org/wiki/Birthday_problem
11 | */
12 | function uid (len) {
13 | return crypto.randomBytes(Math.ceil(Math.max(8, len * 2)))
14 | .toString('base64')
15 | .replace(/[+\/]/g, '')
16 | .slice(0, len);
17 | }
18 |
19 |
20 | // Interface
21 | module.exports.uid = uid;
22 |
23 |
--------------------------------------------------------------------------------
/test/customUtil.test.js:
--------------------------------------------------------------------------------
1 | var should = require('chai').should()
2 | , assert = require('chai').assert
3 | , customUtils = require('../lib/customUtils')
4 | , fs = require('fs')
5 | ;
6 |
7 |
8 | describe('customUtils', function () {
9 |
10 | describe('uid', function () {
11 |
12 | it('Generates a string of the expected length', function () {
13 | customUtils.uid(3).length.should.equal(3);
14 | customUtils.uid(16).length.should.equal(16);
15 | customUtils.uid(42).length.should.equal(42);
16 | customUtils.uid(1000).length.should.equal(1000);
17 | });
18 |
19 | // Very small probability of conflict
20 | it('Generated uids should not be the same', function () {
21 | customUtils.uid(56).should.not.equal(customUtils.uid(56));
22 | });
23 |
24 | });
25 |
26 | });
27 |
--------------------------------------------------------------------------------
/benchmarks/find.js:
--------------------------------------------------------------------------------
1 | var Datastore = require('../lib/datastore')
2 | , benchDb = 'workspace/find.bench.db'
3 | , fs = require('fs')
4 | , path = require('path')
5 | , async = require('async')
6 | , execTime = require('exec-time')
7 | , profiler = new execTime('FIND BENCH')
8 | , commonUtilities = require('./commonUtilities')
9 | , config = commonUtilities.getConfiguration(benchDb)
10 | , d = config.d
11 | , n = config.n
12 | ;
13 |
14 | async.waterfall([
15 | async.apply(commonUtilities.prepareDb, benchDb)
16 | , function (cb) {
17 | d.loadDatabase(function (err) {
18 | if (err) { return cb(err); }
19 | if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); }
20 | cb();
21 | });
22 | }
23 | , function (cb) { profiler.beginProfiling(); return cb(); }
24 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
25 | , async.apply(commonUtilities.findDocs, d, n, profiler)
26 | ], function (err) {
27 | profiler.step("Benchmark finished");
28 |
29 | if (err) { return console.log("An error was encountered: ", err); }
30 | });
31 |
--------------------------------------------------------------------------------
/benchmarks/findWithIn.js:
--------------------------------------------------------------------------------
1 | var Datastore = require('../lib/datastore')
2 | , benchDb = 'workspace/find.bench.db'
3 | , fs = require('fs')
4 | , path = require('path')
5 | , async = require('async')
6 | , execTime = require('exec-time')
7 | , profiler = new execTime('FIND BENCH')
8 | , commonUtilities = require('./commonUtilities')
9 | , config = commonUtilities.getConfiguration(benchDb)
10 | , d = config.d
11 | , n = config.n
12 | ;
13 |
14 | async.waterfall([
15 | async.apply(commonUtilities.prepareDb, benchDb)
16 | , function (cb) {
17 | d.loadDatabase(function (err) {
18 | if (err) { return cb(err); }
19 | if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); }
20 | cb();
21 | });
22 | }
23 | , function (cb) { profiler.beginProfiling(); return cb(); }
24 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
25 | , async.apply(commonUtilities.findDocsWithIn, d, n, profiler)
26 | ], function (err) {
27 | profiler.step("Benchmark finished");
28 |
29 | if (err) { return console.log("An error was encountered: ", err); }
30 | });
31 |
--------------------------------------------------------------------------------
/benchmarks/findOne.js:
--------------------------------------------------------------------------------
1 | var Datastore = require('../lib/datastore')
2 | , benchDb = 'workspace/findOne.bench.db'
3 | , fs = require('fs')
4 | , path = require('path')
5 | , async = require('async')
6 | , execTime = require('exec-time')
7 | , profiler = new execTime('FINDONE BENCH')
8 | , commonUtilities = require('./commonUtilities')
9 | , config = commonUtilities.getConfiguration(benchDb)
10 | , d = config.d
11 | , n = config.n
12 | ;
13 |
14 | async.waterfall([
15 | async.apply(commonUtilities.prepareDb, benchDb)
16 | , function (cb) {
17 | d.loadDatabase(function (err) {
18 | if (err) { return cb(err); }
19 | if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); }
20 | cb();
21 | });
22 | }
23 | , function (cb) { profiler.beginProfiling(); return cb(); }
24 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
25 | , function (cb) { setTimeout(function () {cb();}, 500); }
26 | , async.apply(commonUtilities.findOneDocs, d, n, profiler)
27 | ], function (err) {
28 | profiler.step("Benchmark finished");
29 |
30 | if (err) { return console.log("An error was encountered: ", err); }
31 | });
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2013 Louis Chatriot <louis.chatriot@gmail.com>
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/benchmarks/insert.js:
--------------------------------------------------------------------------------
1 | var Datastore = require('../lib/datastore')
2 | , benchDb = 'workspace/insert.bench.db'
3 | , async = require('async')
4 | , execTime = require('exec-time')
5 | , profiler = new execTime('INSERT BENCH')
6 | , commonUtilities = require('./commonUtilities')
7 | , config = commonUtilities.getConfiguration(benchDb)
8 | , d = config.d
9 | , n = config.n
10 | ;
11 |
12 | async.waterfall([
13 | async.apply(commonUtilities.prepareDb, benchDb)
14 | , function (cb) {
15 | d.loadDatabase(function (err) {
16 | if (err) { return cb(err); }
17 | if (config.program.withIndex) {
18 | d.ensureIndex({ fieldName: 'docNumber' });
19 | n = 2 * n; // We will actually insert twice as many documents
20 | // because the index is slower when the collection is already
21 | // big. So the result given by the algorithm will be a bit worse than
22 | // actual performance
23 | }
24 | cb();
25 | });
26 | }
27 | , function (cb) { profiler.beginProfiling(); return cb(); }
28 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
29 | ], function (err) {
30 | profiler.step("Benchmark finished");
31 |
32 | if (err) { return console.log("An error was encountered: ", err); }
33 | });
34 |
--------------------------------------------------------------------------------
/browser-version/test/testPersistence2.js:
--------------------------------------------------------------------------------
1 | // Capture F5 to reload the base page testPersistence.html not this one
2 | $(document).on('keydown', function (e) {
3 | if (e.keyCode === 116) {
4 | e.preventDefault();
5 | window.location = 'testPersistence.html';
6 | }
7 | });
8 |
9 |
10 | console.log("Checking tests results");
11 | console.log("Please note these tests work on Chrome latest, might not work on other browsers due to discrepancies in how local storage works for the file:// protocol");
12 |
13 | function testsFailed () {
14 | document.getElementById("results").innerHTML = "TESTS FAILED";
15 | }
16 |
17 | var filename = 'test';
18 |
19 | var db = new Nedb({ filename: filename, autoload: true });
20 | db.find({}, function (err, docs) {
21 | if (docs.length !== 1) {
22 | console.log(docs);
23 | console.log("Unexpected length of document database");
24 | return testsFailed();
25 | }
26 |
27 | if (Object.keys(docs[0]).length !== 2) {
28 | console.log("Unexpected length insert document in database");
29 | return testsFailed();
30 | }
31 |
32 | if (docs[0].hello !== 'world') {
33 | console.log("Unexpected document");
34 | return testsFailed();
35 | }
36 |
37 | document.getElementById("results").innerHTML = "BROWSER PERSISTENCE TEST PASSED";
38 | });
39 |
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nedb",
3 | "version": "1.8.0",
4 | "author": {
5 | "name": "Louis Chatriot",
6 | "email": "louis.chatriot@gmail.com"
7 | },
8 | "contributors": [
9 | "Louis Chatriot"
10 | ],
11 | "description": "File-based embedded data store for node.js",
12 | "keywords": [
13 | "database",
14 | "datastore",
15 | "embedded"
16 | ],
17 | "homepage": "https://github.com/louischatriot/nedb",
18 | "repository": {
19 | "type": "git",
20 | "url": "git@github.com:louischatriot/nedb.git"
21 | },
22 | "dependencies": {
23 | "async": "0.2.10",
24 | "binary-search-tree": "0.2.5",
25 | "localforage": "^1.3.0",
26 | "mkdirp": "~0.5.1",
27 | "underscore": "~1.4.4"
28 | },
29 | "devDependencies": {
30 | "chai": "^3.2.0",
31 | "mocha": "1.4.x",
32 | "request": "2.9.x",
33 | "sinon": "1.3.x",
34 | "exec-time": "0.0.2",
35 | "commander": "1.1.1"
36 | },
37 | "scripts": {
38 | "test": "./node_modules/.bin/mocha --reporter spec --timeout 10000"
39 | },
40 | "main": "index",
41 | "browser": {
42 | "./lib/customUtils.js": "./browser-version/browser-specific/lib/customUtils.js",
43 | "./lib/storage.js": "./browser-version/browser-specific/lib/storage.js"
44 | },
45 | "license": "SEE LICENSE IN LICENSE"
46 | }
47 |
--------------------------------------------------------------------------------
/benchmarks/loadDatabase.js:
--------------------------------------------------------------------------------
1 | var Datastore = require('../lib/datastore')
2 | , benchDb = 'workspace/loaddb.bench.db'
3 | , fs = require('fs')
4 | , path = require('path')
5 | , async = require('async')
6 | , commonUtilities = require('./commonUtilities')
7 | , execTime = require('exec-time')
8 | , profiler = new execTime('LOADDB BENCH')
9 | , d = new Datastore(benchDb)
10 | , program = require('commander')
11 | , n
12 | ;
13 |
14 | program
15 | .option('-n --number [number]', 'Size of the collection to test on', parseInt)
16 | .option('-i --with-index', 'Test with an index')
17 | .parse(process.argv);
18 |
19 | n = program.number || 10000;
20 |
21 | console.log("----------------------------");
22 | console.log("Test with " + n + " documents");
23 | console.log(program.withIndex ? "Use an index" : "Don't use an index");
24 | console.log("----------------------------");
25 |
26 | async.waterfall([
27 | async.apply(commonUtilities.prepareDb, benchDb)
28 | , function (cb) {
29 | d.loadDatabase(cb);
30 | }
31 | , function (cb) { profiler.beginProfiling(); return cb(); }
32 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
33 | , async.apply(commonUtilities.loadDatabase, d, n, profiler)
34 | ], function (err) {
35 | profiler.step("Benchmark finished");
36 |
37 | if (err) { return console.log("An error was encountered: ", err); }
38 | });
39 |
--------------------------------------------------------------------------------
/benchmarks/remove.js:
--------------------------------------------------------------------------------
1 | var Datastore = require('../lib/datastore')
2 | , benchDb = 'workspace/remove.bench.db'
3 | , fs = require('fs')
4 | , path = require('path')
5 | , async = require('async')
6 | , execTime = require('exec-time')
7 | , profiler = new execTime('REMOVE BENCH')
8 | , commonUtilities = require('./commonUtilities')
9 | , config = commonUtilities.getConfiguration(benchDb)
10 | , d = config.d
11 | , n = config.n
12 | ;
13 |
14 | async.waterfall([
15 | async.apply(commonUtilities.prepareDb, benchDb)
16 | , function (cb) {
17 | d.loadDatabase(function (err) {
18 | if (err) { return cb(err); }
19 | if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); }
20 | cb();
21 | });
22 | }
23 | , function (cb) { profiler.beginProfiling(); return cb(); }
24 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
25 |
26 | // Test with remove only one document
27 | , function (cb) { profiler.step('MULTI: FALSE'); return cb(); }
28 | , async.apply(commonUtilities.removeDocs, { multi: false }, d, n, profiler)
29 | // Test with multiple documents
30 | , function (cb) { d.remove({}, { multi: true }, function () { return cb(); }); }
31 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
32 | , function (cb) { profiler.step('MULTI: TRUE'); return cb(); }
33 | , async.apply(commonUtilities.removeDocs, { multi: true }, d, n, profiler)
34 | ], function (err) {
35 | profiler.step("Benchmark finished");
36 |
37 | if (err) { return console.log("An error was encountered: ", err); }
38 | });
39 |
--------------------------------------------------------------------------------
/benchmarks/update.js:
--------------------------------------------------------------------------------
1 | var Datastore = require('../lib/datastore')
2 | , benchDb = 'workspace/update.bench.db'
3 | , fs = require('fs')
4 | , path = require('path')
5 | , async = require('async')
6 | , execTime = require('exec-time')
7 | , profiler = new execTime('UPDATE BENCH')
8 | , commonUtilities = require('./commonUtilities')
9 | , config = commonUtilities.getConfiguration(benchDb)
10 | , d = config.d
11 | , n = config.n
12 | ;
13 |
14 | async.waterfall([
15 | async.apply(commonUtilities.prepareDb, benchDb)
16 | , function (cb) {
17 | d.loadDatabase(function (err) {
18 | if (err) { return cb(err); }
19 | if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); }
20 | cb();
21 | });
22 | }
23 | , function (cb) { profiler.beginProfiling(); return cb(); }
24 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
25 |
26 | // Test with update only one document
27 | , function (cb) { profiler.step('MULTI: FALSE'); return cb(); }
28 | , async.apply(commonUtilities.updateDocs, { multi: false }, d, n, profiler)
29 |
30 | // Test with multiple documents
31 | , function (cb) { d.remove({}, { multi: true }, function (err) { return cb(); }); }
32 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
33 | , function (cb) { profiler.step('MULTI: TRUE'); return cb(); }
34 | , async.apply(commonUtilities.updateDocs, { multi: true }, d, n, profiler)
35 | ], function (err) {
36 | profiler.step("Benchmark finished");
37 |
38 | if (err) { return console.log("An error was encountered: ", err); }
39 | });
40 |
--------------------------------------------------------------------------------
/benchmarks/ensureIndex.js:
--------------------------------------------------------------------------------
1 | var Datastore = require('../lib/datastore')
2 | , benchDb = 'workspace/insert.bench.db'
3 | , async = require('async')
4 | , commonUtilities = require('./commonUtilities')
5 | , execTime = require('exec-time')
6 | , profiler = new execTime('INSERT BENCH')
7 | , d = new Datastore(benchDb)
8 | , program = require('commander')
9 | , n
10 | ;
11 |
12 | program
13 | .option('-n --number [number]', 'Size of the collection to test on', parseInt)
14 | .option('-i --with-index', 'Test with an index')
15 | .parse(process.argv);
16 |
17 | n = program.number || 10000;
18 |
19 | console.log("----------------------------");
20 | console.log("Test with " + n + " documents");
21 | console.log("----------------------------");
22 |
23 | async.waterfall([
24 | async.apply(commonUtilities.prepareDb, benchDb)
25 | , function (cb) {
26 | d.loadDatabase(function (err) {
27 | if (err) { return cb(err); }
28 | cb();
29 | });
30 | }
31 | , function (cb) { profiler.beginProfiling(); return cb(); }
32 | , async.apply(commonUtilities.insertDocs, d, n, profiler)
33 | , function (cb) {
34 | var i;
35 |
36 | profiler.step('Begin calling ensureIndex ' + n + ' times');
37 |
38 | for (i = 0; i < n; i += 1) {
39 | d.ensureIndex({ fieldName: 'docNumber' });
40 | delete d.indexes.docNumber;
41 | }
42 |
43 | console.log("Average time for one ensureIndex: " + (profiler.elapsedSinceLastStep() / n) + "ms");
44 | profiler.step('Finished calling ensureIndex ' + n + ' times');
45 | }
46 | ], function (err) {
47 | profiler.step("Benchmark finished");
48 |
49 | if (err) { return console.log("An error was encountered: ", err); }
50 | });
51 |
52 |
--------------------------------------------------------------------------------
/test_lac/openFds.test.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | , child_process = require('child_process')
3 | , async = require('async')
4 | , Nedb = require('../lib/datastore')
5 | , db = new Nedb({ filename: './workspace/openfds.db', autoload: true })
6 | , N = 64 // Half the allowed file descriptors
7 | , i, fds
8 | ;
9 |
10 | function multipleOpen (filename, N, callback) {
11 | async.whilst( function () { return i < N; }
12 | , function (cb) {
13 | fs.open(filename, 'r', function (err, fd) {
14 | i += 1;
15 | if (fd) { fds.push(fd); }
16 | return cb(err);
17 | });
18 | }
19 | , callback);
20 | }
21 |
22 | async.waterfall([
23 | // Check that ulimit has been set to the correct value
24 | function (cb) {
25 | i = 0;
26 | fds = [];
27 | multipleOpen('./test_lac/openFdsTestFile', 2 * N + 1, function (err) {
28 | if (!err) { console.log("No error occured while opening a file too many times"); }
29 | fds.forEach(function (fd) { fs.closeSync(fd); });
30 | return cb();
31 | })
32 | }
33 | , function (cb) {
34 | i = 0;
35 | fds = [];
36 | multipleOpen('./test_lac/openFdsTestFile2', N, function (err) {
37 | if (err) { console.log('An unexpected error occured when opening file not too many times: ' + err); }
38 | fds.forEach(function (fd) { fs.closeSync(fd); });
39 | return cb();
40 | })
41 | }
42 | // Then actually test NeDB persistence
43 | , function () {
44 | db.remove({}, { multi: true }, function (err) {
45 | if (err) { console.log(err); }
46 | db.insert({ hello: 'world' }, function (err) {
47 | if (err) { console.log(err); }
48 |
49 | i = 0;
50 | async.whilst( function () { return i < 2 * N + 1; }
51 | , function (cb) {
52 | db.persistence.persistCachedDatabase(function (err) {
53 | if (err) { return cb(err); }
54 | i += 1;
55 | return cb();
56 | });
57 | }
58 | , function (err) {
59 | if (err) { console.log("Got unexpected error during one peresistence operation: " + err); }
60 | }
61 | );
62 |
63 | });
64 | });
65 | }
66 | ]);
67 |
68 |
--------------------------------------------------------------------------------
/browser-version/browser-specific/lib/customUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Specific customUtils for the browser, where we don't have access to the Crypto and Buffer modules
3 | */
4 |
5 | /**
6 | * Taken from the crypto-browserify module
7 | * https://github.com/dominictarr/crypto-browserify
8 | * NOTE: Math.random() does not guarantee "cryptographic quality" but we actually don't need it
9 | */
10 | function randomBytes (size) {
11 | var bytes = new Array(size);
12 | var r;
13 |
14 | for (var i = 0, r; i < size; i++) {
15 | if ((i & 0x03) == 0) r = Math.random() * 0x100000000;
16 | bytes[i] = r >>> ((i & 0x03) << 3) & 0xff;
17 | }
18 |
19 | return bytes;
20 | }
21 |
22 |
23 | /**
24 | * Taken from the base64-js module
25 | * https://github.com/beatgammit/base64-js/
26 | */
27 | function byteArrayToBase64 (uint8) {
28 | var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
29 | , extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
30 | , output = ""
31 | , temp, length, i;
32 |
33 | function tripletToBase64 (num) {
34 | return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F];
35 | };
36 |
37 | // go through the array every three bytes, we'll deal with trailing stuff later
38 | for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
39 | temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]);
40 | output += tripletToBase64(temp);
41 | }
42 |
43 | // pad the end with zeros, but make sure to not forget the extra bytes
44 | switch (extraBytes) {
45 | case 1:
46 | temp = uint8[uint8.length - 1];
47 | output += lookup[temp >> 2];
48 | output += lookup[(temp << 4) & 0x3F];
49 | output += '==';
50 | break;
51 | case 2:
52 | temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1]);
53 | output += lookup[temp >> 10];
54 | output += lookup[(temp >> 4) & 0x3F];
55 | output += lookup[(temp << 2) & 0x3F];
56 | output += '=';
57 | break;
58 | }
59 |
60 | return output;
61 | }
62 |
63 |
64 | /**
65 | * Return a random alphanumerical string of length len
66 | * There is a very small probability (less than 1/1,000,000) for the length to be less than len
67 | * (il the base64 conversion yields too many pluses and slashes) but
68 | * that's not an issue here
69 | * The probability of a collision is extremely small (need 3*10^12 documents to have one chance in a million of a collision)
70 | * See http://en.wikipedia.org/wiki/Birthday_problem
71 | */
72 | function uid (len) {
73 | return byteArrayToBase64(randomBytes(Math.ceil(Math.max(8, len * 2)))).replace(/[+\/]/g, '').slice(0, len);
74 | }
75 |
76 |
77 |
78 | module.exports.uid = uid;
79 |
--------------------------------------------------------------------------------
/lib/executor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Responsible for sequentially executing actions on the database
3 | */
4 |
5 | var async = require('async')
6 | ;
7 |
8 | function Executor () {
9 | this.buffer = [];
10 | this.ready = false;
11 |
12 | // This queue will execute all commands, one-by-one in order
13 | this.queue = async.queue(function (task, cb) {
14 | var newArguments = [];
15 |
16 | // task.arguments is an array-like object on which adding a new field doesn't work, so we transform it into a real array
17 | for (var i = 0; i < task.arguments.length; i += 1) { newArguments.push(task.arguments[i]); }
18 | var lastArg = task.arguments[task.arguments.length - 1];
19 |
20 | // Always tell the queue task is complete. Execute callback if any was given.
21 | if (typeof lastArg === 'function') {
22 | // Callback was supplied
23 | newArguments[newArguments.length - 1] = function () {
24 | if (typeof setImmediate === 'function') {
25 | setImmediate(cb);
26 | } else {
27 | process.nextTick(cb);
28 | }
29 | lastArg.apply(null, arguments);
30 | };
31 | } else if (!lastArg && task.arguments.length !== 0) {
32 | // false/undefined/null supplied as callbback
33 | newArguments[newArguments.length - 1] = function () { cb(); };
34 | } else {
35 | // Nothing supplied as callback
36 | newArguments.push(function () { cb(); });
37 | }
38 |
39 |
40 | task.fn.apply(task.this, newArguments);
41 | }, 1);
42 | }
43 |
44 |
45 | /**
46 | * If executor is ready, queue task (and process it immediately if executor was idle)
47 | * If not, buffer task for later processing
48 | * @param {Object} task
49 | * task.this - Object to use as this
50 | * task.fn - Function to execute
51 | * task.arguments - Array of arguments, IMPORTANT: only the last argument may be a function (the callback)
52 | * and the last argument cannot be false/undefined/null
53 | * @param {Boolean} forceQueuing Optional (defaults to false) force executor to queue task even if it is not ready
54 | */
55 | Executor.prototype.push = function (task, forceQueuing) {
56 | if (this.ready || forceQueuing) {
57 | this.queue.push(task);
58 | } else {
59 | this.buffer.push(task);
60 | }
61 | };
62 |
63 |
64 | /**
65 | * Queue all tasks in buffer (in the same order they came in)
66 | * Automatically sets executor as ready
67 | */
68 | Executor.prototype.processBuffer = function () {
69 | var i;
70 | this.ready = true;
71 | for (i = 0; i < this.buffer.length; i += 1) { this.queue.push(this.buffer[i]); }
72 | this.buffer = [];
73 | };
74 |
75 |
76 |
77 | // Interface
78 | module.exports = Executor;
79 |
--------------------------------------------------------------------------------
/browser-version/browser-specific/lib/storage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Way data is stored for this database
3 | * For a Node.js/Node Webkit database it's the file system
4 | * For a browser-side database it's localforage, which uses the best backend available (IndexedDB then WebSQL then localStorage)
5 | *
6 | * This version is the browser version
7 | */
8 |
9 | var localforage = require('localforage')
10 |
11 | // Configure localforage to display NeDB name for now. Would be a good idea to let user use his own app name
12 | localforage.config({
13 | name: 'NeDB'
14 | , storeName: 'nedbdata'
15 | });
16 |
17 |
18 | function exists (filename, callback) {
19 | localforage.getItem(filename, function (err, value) {
20 | if (value !== null) { // Even if value is undefined, localforage returns null
21 | return callback(true);
22 | } else {
23 | return callback(false);
24 | }
25 | });
26 | }
27 |
28 |
29 | function rename (filename, newFilename, callback) {
30 | localforage.getItem(filename, function (err, value) {
31 | if (value === null) {
32 | localforage.removeItem(newFilename, function () { return callback(); });
33 | } else {
34 | localforage.setItem(newFilename, value, function () {
35 | localforage.removeItem(filename, function () { return callback(); });
36 | });
37 | }
38 | });
39 | }
40 |
41 |
42 | function writeFile (filename, contents, options, callback) {
43 | // Options do not matter in browser setup
44 | if (typeof options === 'function') { callback = options; }
45 | localforage.setItem(filename, contents, function () { return callback(); });
46 | }
47 |
48 |
49 | function appendFile (filename, toAppend, options, callback) {
50 | // Options do not matter in browser setup
51 | if (typeof options === 'function') { callback = options; }
52 |
53 | localforage.getItem(filename, function (err, contents) {
54 | contents = contents || '';
55 | contents += toAppend;
56 | localforage.setItem(filename, contents, function () { return callback(); });
57 | });
58 | }
59 |
60 |
61 | function readFile (filename, options, callback) {
62 | // Options do not matter in browser setup
63 | if (typeof options === 'function') { callback = options; }
64 | localforage.getItem(filename, function (err, contents) { return callback(null, contents || ''); });
65 | }
66 |
67 |
68 | function unlink (filename, callback) {
69 | localforage.removeItem(filename, function () { return callback(); });
70 | }
71 |
72 |
73 | // Nothing to do, no directories will be used on the browser
74 | function mkdirp (dir, callback) {
75 | return callback();
76 | }
77 |
78 |
79 | // Nothing to do, no data corruption possible in the brower
80 | function ensureDatafileIntegrity (filename, callback) {
81 | return callback(null);
82 | }
83 |
84 |
85 | // Interface
86 | module.exports.exists = exists;
87 | module.exports.rename = rename;
88 | module.exports.writeFile = writeFile;
89 | module.exports.crashSafeWriteFile = writeFile; // No need for a crash safe function in the browser
90 | module.exports.appendFile = appendFile;
91 | module.exports.readFile = readFile;
92 | module.exports.unlink = unlink;
93 | module.exports.mkdirp = mkdirp;
94 | module.exports.ensureDatafileIntegrity = ensureDatafileIntegrity;
95 |
96 |
--------------------------------------------------------------------------------
/browser-version/build.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build the browser version of nedb
3 | */
4 |
5 | var fs = require('fs')
6 | , path = require('path')
7 | , child_process = require('child_process')
8 | , toCopy = ['lib', 'node_modules']
9 | , async, browserify, uglify
10 | ;
11 |
12 | // Ensuring both node_modules (the source one and build one), src and out directories exist
13 | function ensureDirExists (name) {
14 | try {
15 | fs.mkdirSync(path.join(__dirname, name));
16 | } catch (e) {
17 | if (e.code !== 'EEXIST') {
18 | console.log("Error ensuring that node_modules exists");
19 | process.exit(1);
20 | }
21 | }
22 | }
23 | ensureDirExists('../node_modules');
24 | ensureDirExists('node_modules');
25 | ensureDirExists('out');
26 | ensureDirExists('src');
27 |
28 |
29 | // Installing build dependencies and require them
30 | console.log("Installing build dependencies");
31 | child_process.exec('npm install', { cwd: __dirname }, function (err, stdout, stderr) {
32 | if (err) { console.log("Error reinstalling dependencies"); process.exit(1); }
33 |
34 | fs = require('fs-extra');
35 | async = require('async');
36 | browserify = require('browserify');
37 | uglify = require('uglify-js');
38 |
39 | async.waterfall([
40 | function (cb) {
41 | console.log("Installing source dependencies if needed");
42 |
43 | child_process.exec('npm install', { cwd: path.join(__dirname, '..') }, function (err) { return cb(err); });
44 | }
45 | , function (cb) {
46 | console.log("Removing contents of the src directory");
47 |
48 | async.eachSeries(fs.readdirSync(path.join(__dirname, 'src')), function (item, _cb) {
49 | fs.remove(path.join(__dirname, 'src', item), _cb);
50 | }, cb);
51 | }
52 | , function (cb) {
53 | console.log("Copying source files");
54 |
55 | async.eachSeries(toCopy, function (item, _cb) {
56 | fs.copy(path.join(__dirname, '..', item), path.join(__dirname, 'src', item), _cb);
57 | }, cb);
58 | }
59 | , function (cb) {
60 | console.log("Copying browser specific files to replace their server-specific counterparts");
61 |
62 | async.eachSeries(fs.readdirSync(path.join(__dirname, 'browser-specific')), function (item, _cb) {
63 | fs.copy(path.join(__dirname, 'browser-specific', item), path.join(__dirname, 'src', item), _cb);
64 | }, cb);
65 | }
66 | , function (cb) {
67 | console.log("Browserifying the code");
68 |
69 | var b = browserify()
70 | , srcPath = path.join(__dirname, 'src/lib/datastore.js');
71 |
72 | b.add(srcPath);
73 | b.bundle({ standalone: 'Nedb' }, function (err, out) {
74 | if (err) { return cb(err); }
75 | fs.writeFile(path.join(__dirname, 'out/nedb.js'), out, 'utf8', function (err) {
76 | if (err) {
77 | return cb(err);
78 | } else {
79 | return cb(null, out);
80 | }
81 | });
82 | });
83 | }
84 | , function (out, cb) {
85 | console.log("Creating the minified version");
86 |
87 | var compressedCode = uglify.minify(out, { fromString: true });
88 | fs.writeFile(path.join(__dirname, 'out/nedb.min.js'), compressedCode.code, 'utf8', cb);
89 | }
90 | ], function (err) {
91 | if (err) {
92 | console.log("Error during build");
93 | console.log(err);
94 | } else {
95 | console.log("Build finished with success");
96 | }
97 | });
98 | });
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/test_lac/loadAndCrash.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Load and modify part of fs to ensure writeFile will crash after writing 5000 bytes
3 | */
4 | var fs = require('fs');
5 |
6 | function rethrow() {
7 | // Only enable in debug mode. A backtrace uses ~1000 bytes of heap space and
8 | // is fairly slow to generate.
9 | if (DEBUG) {
10 | var backtrace = new Error();
11 | return function(err) {
12 | if (err) {
13 | backtrace.stack = err.name + ': ' + err.message +
14 | backtrace.stack.substr(backtrace.name.length);
15 | throw backtrace;
16 | }
17 | };
18 | }
19 |
20 | return function(err) {
21 | if (err) {
22 | throw err; // Forgot a callback but don't know where? Use NODE_DEBUG=fs
23 | }
24 | };
25 | }
26 |
27 | function maybeCallback(cb) {
28 | return typeof cb === 'function' ? cb : rethrow();
29 | }
30 |
31 | function isFd(path) {
32 | return (path >>> 0) === path;
33 | }
34 |
35 | function assertEncoding(encoding) {
36 | if (encoding && !Buffer.isEncoding(encoding)) {
37 | throw new Error('Unknown encoding: ' + encoding);
38 | }
39 | }
40 |
41 | var onePassDone = false;
42 | function writeAll(fd, isUserFd, buffer, offset, length, position, callback_) {
43 | var callback = maybeCallback(arguments[arguments.length - 1]);
44 |
45 | if (onePassDone) { process.exit(1); } // Crash on purpose before rewrite done
46 | var l = Math.min(5000, length); // Force write by chunks of 5000 bytes to ensure data will be incomplete on crash
47 |
48 | // write(fd, buffer, offset, length, position, callback)
49 | fs.write(fd, buffer, offset, l, position, function(writeErr, written) {
50 | if (writeErr) {
51 | if (isUserFd) {
52 | if (callback) callback(writeErr);
53 | } else {
54 | fs.close(fd, function() {
55 | if (callback) callback(writeErr);
56 | });
57 | }
58 | } else {
59 | onePassDone = true;
60 | if (written === length) {
61 | if (isUserFd) {
62 | if (callback) callback(null);
63 | } else {
64 | fs.close(fd, callback);
65 | }
66 | } else {
67 | offset += written;
68 | length -= written;
69 | if (position !== null) {
70 | position += written;
71 | }
72 | writeAll(fd, isUserFd, buffer, offset, length, position, callback);
73 | }
74 | }
75 | });
76 | }
77 |
78 | fs.writeFile = function(path, data, options, callback_) {
79 | var callback = maybeCallback(arguments[arguments.length - 1]);
80 |
81 | if (!options || typeof options === 'function') {
82 | options = { encoding: 'utf8', mode: 438, flag: 'w' }; // Mode 438 == 0o666 (compatibility with older Node releases)
83 | } else if (typeof options === 'string') {
84 | options = { encoding: options, mode: 438, flag: 'w' }; // Mode 438 == 0o666 (compatibility with older Node releases)
85 | } else if (typeof options !== 'object') {
86 | throwOptionsError(options);
87 | }
88 |
89 | assertEncoding(options.encoding);
90 |
91 | var flag = options.flag || 'w';
92 |
93 | if (isFd(path)) {
94 | writeFd(path, true);
95 | return;
96 | }
97 |
98 | fs.open(path, flag, options.mode, function(openErr, fd) {
99 | if (openErr) {
100 | if (callback) callback(openErr);
101 | } else {
102 | writeFd(fd, false);
103 | }
104 | });
105 |
106 | function writeFd(fd, isUserFd) {
107 | var buffer = (data instanceof Buffer) ? data : new Buffer('' + data,
108 | options.encoding || 'utf8');
109 | var position = /a/.test(flag) ? null : 0;
110 |
111 | writeAll(fd, isUserFd, buffer, 0, buffer.length, position, callback);
112 | }
113 | };
114 |
115 |
116 |
117 |
118 | // End of fs modification
119 | var Nedb = require('../lib/datastore.js')
120 | , db = new Nedb({ filename: 'workspace/lac.db' })
121 | ;
122 |
123 | db.loadDatabase();
124 |
--------------------------------------------------------------------------------
/browser-version/test/mocha.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 | body {
3 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
4 | padding: 60px 50px;
5 | }
6 |
7 | #mocha ul, #mocha li {
8 | margin: 0;
9 | padding: 0;
10 | }
11 |
12 | #mocha ul {
13 | list-style: none;
14 | }
15 |
16 | #mocha h1, #mocha h2 {
17 | margin: 0;
18 | }
19 |
20 | #mocha h1 {
21 | margin-top: 15px;
22 | font-size: 1em;
23 | font-weight: 200;
24 | }
25 |
26 | #mocha h1 a {
27 | text-decoration: none;
28 | color: inherit;
29 | }
30 |
31 | #mocha h1 a:hover {
32 | text-decoration: underline;
33 | }
34 |
35 | #mocha .suite .suite h1 {
36 | margin-top: 0;
37 | font-size: .8em;
38 | }
39 |
40 | #mocha h2 {
41 | font-size: 12px;
42 | font-weight: normal;
43 | cursor: pointer;
44 | }
45 |
46 | #mocha .suite {
47 | margin-left: 15px;
48 | }
49 |
50 | #mocha .test {
51 | margin-left: 15px;
52 | }
53 |
54 | #mocha .test:hover h2::after {
55 | position: relative;
56 | top: 0;
57 | right: -10px;
58 | content: '(view source)';
59 | font-size: 12px;
60 | font-family: arial;
61 | color: #888;
62 | }
63 |
64 | #mocha .test.pending:hover h2::after {
65 | content: '(pending)';
66 | font-family: arial;
67 | }
68 |
69 | #mocha .test.pass.medium .duration {
70 | background: #C09853;
71 | }
72 |
73 | #mocha .test.pass.slow .duration {
74 | background: #B94A48;
75 | }
76 |
77 | #mocha .test.pass::before {
78 | content: '✓';
79 | font-size: 12px;
80 | display: block;
81 | float: left;
82 | margin-right: 5px;
83 | color: #00d6b2;
84 | }
85 |
86 | #mocha .test.pass .duration {
87 | font-size: 9px;
88 | margin-left: 5px;
89 | padding: 2px 5px;
90 | color: white;
91 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
92 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
93 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
94 | -webkit-border-radius: 5px;
95 | -moz-border-radius: 5px;
96 | -ms-border-radius: 5px;
97 | -o-border-radius: 5px;
98 | border-radius: 5px;
99 | }
100 |
101 | #mocha .test.pass.fast .duration {
102 | display: none;
103 | }
104 |
105 | #mocha .test.pending {
106 | color: #0b97c4;
107 | }
108 |
109 | #mocha .test.pending::before {
110 | content: '◦';
111 | color: #0b97c4;
112 | }
113 |
114 | #mocha .test.fail {
115 | color: #c00;
116 | }
117 |
118 | #mocha .test.fail pre {
119 | color: black;
120 | }
121 |
122 | #mocha .test.fail::before {
123 | content: '✖';
124 | font-size: 12px;
125 | display: block;
126 | float: left;
127 | margin-right: 5px;
128 | color: #c00;
129 | }
130 |
131 | #mocha .test pre.error {
132 | color: #c00;
133 | }
134 |
135 | #mocha .test pre {
136 | display: inline-block;
137 | font: 12px/1.5 monaco, monospace;
138 | margin: 5px;
139 | padding: 15px;
140 | border: 1px solid #eee;
141 | border-bottom-color: #ddd;
142 | -webkit-border-radius: 3px;
143 | -webkit-box-shadow: 0 1px 3px #eee;
144 | }
145 |
146 | #report.pass .test.fail {
147 | display: none;
148 | }
149 |
150 | #report.fail .test.pass {
151 | display: none;
152 | }
153 |
154 | #error {
155 | color: #c00;
156 | font-size: 1.5 em;
157 | font-weight: 100;
158 | letter-spacing: 1px;
159 | }
160 |
161 | #stats {
162 | position: fixed;
163 | top: 15px;
164 | right: 10px;
165 | font-size: 12px;
166 | margin: 0;
167 | color: #888;
168 | }
169 |
170 | #stats .progress {
171 | float: right;
172 | padding-top: 0;
173 | }
174 |
175 | #stats em {
176 | color: black;
177 | }
178 |
179 | #stats a {
180 | text-decoration: none;
181 | color: inherit;
182 | }
183 |
184 | #stats a:hover {
185 | border-bottom: 1px solid #eee;
186 | }
187 |
188 | #stats li {
189 | display: inline-block;
190 | margin: 0 5px;
191 | list-style: none;
192 | padding-top: 11px;
193 | }
194 |
195 | code .comment { color: #ddd }
196 | code .init { color: #2F6FAD }
197 | code .string { color: #5890AD }
198 | code .keyword { color: #8A6343 }
199 | code .number { color: #2F6FAD }
200 |
--------------------------------------------------------------------------------
/lib/storage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Way data is stored for this database
3 | * For a Node.js/Node Webkit database it's the file system
4 | * For a browser-side database it's localforage which chooses the best option depending on user browser (IndexedDB then WebSQL then localStorage)
5 | *
6 | * This version is the Node.js/Node Webkit version
7 | * It's essentially fs, mkdirp and crash safe write and read functions
8 | */
9 |
10 | var fs = require('fs')
11 | , mkdirp = require('mkdirp')
12 | , async = require('async')
13 | , path = require('path')
14 | , storage = {}
15 | ;
16 |
17 | storage.exists = fs.exists;
18 | storage.rename = fs.rename;
19 | storage.writeFile = fs.writeFile;
20 | storage.unlink = fs.unlink;
21 | storage.appendFile = fs.appendFile;
22 | storage.readFile = fs.readFile;
23 | storage.mkdirp = mkdirp;
24 |
25 |
26 | /**
27 | * Explicit name ...
28 | */
29 | storage.ensureFileDoesntExist = function (file, callback) {
30 | storage.exists(file, function (exists) {
31 | if (!exists) { return callback(null); }
32 |
33 | storage.unlink(file, function (err) { return callback(err); });
34 | });
35 | };
36 |
37 |
38 | /**
39 | * Flush data in OS buffer to storage if corresponding option is set
40 | * @param {String} options.filename
41 | * @param {Boolean} options.isDir Optional, defaults to false
42 | * If options is a string, it is assumed that the flush of the file (not dir) called options was requested
43 | */
44 | storage.flushToStorage = function (options, callback) {
45 | var filename, flags;
46 | if (typeof options === 'string') {
47 | filename = options;
48 | flags = 'r+';
49 | } else {
50 | filename = options.filename;
51 | flags = options.isDir ? 'r' : 'r+';
52 | }
53 |
54 | // Windows can't fsync (FlushFileBuffers) directories. We can live with this as it cannot cause 100% dataloss
55 | // except in the very rare event of the first time database is loaded and a crash happens
56 | if (flags === 'r' && (process.platform === 'win32' || process.platform === 'win64')) { return callback(null); }
57 |
58 | fs.open(filename, flags, function (err, fd) {
59 | if (err) { return callback(err); }
60 | fs.fsync(fd, function (errFS) {
61 | fs.close(fd, function (errC) {
62 | if (errFS || errC) {
63 | var e = new Error('Failed to flush to storage');
64 | e.errorOnFsync = errFS;
65 | e.errorOnClose = errC;
66 | return callback(e);
67 | } else {
68 | return callback(null);
69 | }
70 | });
71 | });
72 | });
73 | };
74 |
75 |
76 | /**
77 | * Fully write or rewrite the datafile, immune to crashes during the write operation (data will not be lost)
78 | * @param {String} filename
79 | * @param {String} data
80 | * @param {Function} cb Optional callback, signature: err
81 | */
82 | storage.crashSafeWriteFile = function (filename, data, cb) {
83 | var callback = cb || function () {}
84 | , tempFilename = filename + '~';
85 |
86 | async.waterfall([
87 | async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true })
88 | , function (cb) {
89 | storage.exists(filename, function (exists) {
90 | if (exists) {
91 | storage.flushToStorage(filename, function (err) { return cb(err); });
92 | } else {
93 | return cb();
94 | }
95 | });
96 | }
97 | , function (cb) {
98 | storage.writeFile(tempFilename, data, function (err) { return cb(err); });
99 | }
100 | , async.apply(storage.flushToStorage, tempFilename)
101 | , function (cb) {
102 | storage.rename(tempFilename, filename, function (err) { return cb(err); });
103 | }
104 | , async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true })
105 | ], function (err) { return callback(err); })
106 | };
107 |
108 |
109 | /**
110 | * Ensure the datafile contains all the data, even if there was a crash during a full file write
111 | * @param {String} filename
112 | * @param {Function} callback signature: err
113 | */
114 | storage.ensureDatafileIntegrity = function (filename, callback) {
115 | var tempFilename = filename + '~';
116 |
117 | storage.exists(filename, function (filenameExists) {
118 | // Write was successful
119 | if (filenameExists) { return callback(null); }
120 |
121 | storage.exists(tempFilename, function (oldFilenameExists) {
122 | // New database
123 | if (!oldFilenameExists) {
124 | return storage.writeFile(filename, '', 'utf8', function (err) { callback(err); });
125 | }
126 |
127 | // Write failed, use old version
128 | storage.rename(tempFilename, filename, function (err) { return callback(err); });
129 | });
130 | });
131 | };
132 |
133 |
134 |
135 | // Interface
136 | module.exports = storage;
137 |
--------------------------------------------------------------------------------
/browser-version/test/testLoad.js:
--------------------------------------------------------------------------------
1 | console.log('BEGINNING');
2 |
3 | var N = 50000
4 | , db = new Nedb({ filename: 'loadTest', autoload: true })
5 | , t, i
6 | , sample = JSON.stringify({ data: Math.random(), _id: Math.random() });
7 | ;
8 |
9 | // Some inserts in sequence, using the default storage mechanism (IndexedDB in my case)
10 | function someInserts (sn, N, callback) {
11 | var i = 0, beg = Date.now();
12 | async.whilst( function () { return i < N; }
13 | , function (_cb) {
14 | db.insert({ data: Math.random() }, function (err) { i += 1; return _cb(err); });
15 | }
16 | , function (err) {
17 | console.log("Inserts, series " + sn + " " + (Date.now() - beg));
18 | return callback(err);
19 | });
20 | }
21 |
22 | // Manually updating the localStorage on the same variable
23 | function someLS (sn, N, callback) {
24 | var i = 0, beg = Date.now();
25 | for (i = 0; i < N; i += 1) {
26 | localStorage.setItem('loadTestLS', getItem('loadTestLS') + sample);
27 | }
28 | console.log("localStorage, series " + sn + " " + (Date.now() - beg));
29 | return callback();
30 | }
31 |
32 | // Manually updating the localStorage on different variables
33 | function someLSDiff (sn, N, callback) {
34 | var i = 0, beg = Date.now();
35 | for (i = 0; i < N; i += 1) {
36 | localStorage.setItem('loadTestLS-' + i, sample);
37 | }
38 | console.log("localStorage, series " + sn + " " + (Date.now() - beg));
39 | return callback();
40 | }
41 |
42 | // Manually updating the localforage default on the same variable (IndexedDB on my machine)
43 | function someLF (sn, N, callback) {
44 | var i = 0, beg = Date.now();
45 | async.whilst( function () { return i < N; }
46 | , function (_cb) {
47 | localforage.getItem('loadTestLF', function (err, value) {
48 | if (err) { return _cb(err); }
49 | localforage.setItem('loadTestLF', value + sample, function (err) { i += 1; return _cb(err); });
50 | });
51 | }
52 | , function (err) {
53 | console.log("localForage/IDB, series " + sn + " " + (Date.now() - beg));
54 | return callback(err);
55 | });
56 | }
57 |
58 | // Manually updating the localforage default on the different variables (IndexedDB on my machine)
59 | function someLFDiff (sn, N, callback) {
60 | var i = 0, beg = Date.now();
61 | async.whilst( function () { return i < N; }
62 | , function (_cb) {
63 | localforage.setItem('loadTestLF-' + i, sample, function (err) { i += 1; return _cb(err); });
64 | }
65 | , function (err) {
66 | console.log("localForage/IDB, series " + sn + " " + (Date.now() - beg));
67 | return callback(err);
68 | });
69 | }
70 |
71 |
72 |
73 | localStorage.setItem('loadTestLS', '');
74 | async.waterfall([
75 | function (cb) { db.remove({}, { multi: true }, function (err) { return cb(err); }); }
76 |
77 | // Slow and gets slower with database size
78 | //, async.apply(someInserts, "#1", N) // N=5000, 141s
79 | //, async.apply(someInserts, "#2", N) // N=5000, 208s
80 | //, async.apply(someInserts, "#3", N) // N=5000, 281s
81 | //, async.apply(someInserts, "#4", N) // N=5000, 350s
82 |
83 | // Slow and gets slower really fast with database size, then outright crashes
84 | //, async.apply(someLS, "#1", N) // N=4000, 2.5s
85 | //, async.apply(someLS, "#2", N) // N=4000, 8.0s
86 | //, async.apply(someLS, "#3", N) // N=4000, 26.5s
87 | //, async.apply(someLS, "#4", N) // N=4000, 47.8s then crash, can't get string (with N=5000 crash happens on second pass)
88 |
89 | // Much faster and more consistent
90 | //, async.apply(someLSDiff, "#1", N) // N=50000, 0.7s
91 | //, async.apply(someLSDiff, "#2", N) // N=50000, 0.5s
92 | //, async.apply(someLSDiff, "#3", N) // N=50000, 0.5s
93 | //, async.apply(someLSDiff, "#4", N) // N=50000, 0.5s
94 |
95 | // Slow and gets slower with database size
96 | //, function (cb) { localforage.setItem('loadTestLF', '', function (err) { return cb(err) }) }
97 | //, async.apply(someLF, "#1", N) // N=5000, 69s
98 | //, async.apply(someLF, "#2", N) // N=5000, 108s
99 | //, async.apply(someLF, "#3", N) // N=5000, 137s
100 | //, async.apply(someLF, "#4", N) // N=5000, 169s
101 |
102 | // Quite fast and speed doesn't change with database size (tested with N=10000 and N=50000, still no slow-down)
103 | //, async.apply(someLFDiff, "#1", N) // N=5000, 18s
104 | //, async.apply(someLFDiff, "#2", N) // N=5000, 18s
105 | //, async.apply(someLFDiff, "#3", N) // N=5000, 18s
106 | //, async.apply(someLFDiff, "#4", N) // N=5000, 18s
107 | ]);
108 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/lib/cursor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Manage access to data, be it to find, update or remove it
3 | */
4 | var model = require('./model')
5 | , _ = require('underscore')
6 | ;
7 |
8 |
9 |
10 | /**
11 | * Create a new cursor for this collection
12 | * @param {Datastore} db - The datastore this cursor is bound to
13 | * @param {Query} query - The query this cursor will operate on
14 | * @param {Function} execFn - Handler to be executed after cursor has found the results and before the callback passed to find/findOne/update/remove
15 | */
16 | function Cursor (db, query, execFn) {
17 | this.db = db;
18 | this.query = query || {};
19 | if (execFn) { this.execFn = execFn; }
20 | }
21 |
22 |
23 | /**
24 | * Set a limit to the number of results
25 | */
26 | Cursor.prototype.limit = function(limit) {
27 | this._limit = limit;
28 | return this;
29 | };
30 |
31 |
32 | /**
33 | * Skip a the number of results
34 | */
35 | Cursor.prototype.skip = function(skip) {
36 | this._skip = skip;
37 | return this;
38 | };
39 |
40 |
41 | /**
42 | * Sort results of the query
43 | * @param {SortQuery} sortQuery - SortQuery is { field: order }, field can use the dot-notation, order is 1 for ascending and -1 for descending
44 | */
45 | Cursor.prototype.sort = function(sortQuery) {
46 | this._sort = sortQuery;
47 | return this;
48 | };
49 |
50 |
51 | /**
52 | * Add the use of a projection
53 | * @param {Object} projection - MongoDB-style projection. {} means take all fields. Then it's { key1: 1, key2: 1 } to take only key1 and key2
54 | * { key1: 0, key2: 0 } to omit only key1 and key2. Except _id, you can't mix takes and omits
55 | */
56 | Cursor.prototype.projection = function(projection) {
57 | this._projection = projection;
58 | return this;
59 | };
60 |
61 |
62 | /**
63 | * Apply the projection
64 | */
65 | Cursor.prototype.project = function (candidates) {
66 | var res = [], self = this
67 | , keepId, action, keys
68 | ;
69 |
70 | if (this._projection === undefined || Object.keys(this._projection).length === 0) {
71 | return candidates;
72 | }
73 |
74 | keepId = this._projection._id === 0 ? false : true;
75 | this._projection = _.omit(this._projection, '_id');
76 |
77 | // Check for consistency
78 | keys = Object.keys(this._projection);
79 | keys.forEach(function (k) {
80 | if (action !== undefined && self._projection[k] !== action) { throw new Error("Can't both keep and omit fields except for _id"); }
81 | action = self._projection[k];
82 | });
83 |
84 | // Do the actual projection
85 | candidates.forEach(function (candidate) {
86 | var toPush;
87 | if (action === 1) { // pick-type projection
88 | toPush = { $set: {} };
89 | keys.forEach(function (k) {
90 | toPush.$set[k] = model.getDotValue(candidate, k);
91 | if (toPush.$set[k] === undefined) { delete toPush.$set[k]; }
92 | });
93 | toPush = model.modify({}, toPush);
94 | } else { // omit-type projection
95 | toPush = { $unset: {} };
96 | keys.forEach(function (k) { toPush.$unset[k] = true });
97 | toPush = model.modify(candidate, toPush);
98 | }
99 | if (keepId) {
100 | toPush._id = candidate._id;
101 | } else {
102 | delete toPush._id;
103 | }
104 | res.push(toPush);
105 | });
106 |
107 | return res;
108 | };
109 |
110 |
111 | /**
112 | * Get all matching elements
113 | * Will return pointers to matched elements (shallow copies), returning full copies is the role of find or findOne
114 | * This is an internal function, use exec which uses the executor
115 | *
116 | * @param {Function} callback - Signature: err, results
117 | */
118 | Cursor.prototype._exec = function(_callback) {
119 | var res = [], added = 0, skipped = 0, self = this
120 | , error = null
121 | , i, keys, key
122 | ;
123 |
124 | function callback (error, res) {
125 | if (self.execFn) {
126 | return self.execFn(error, res, _callback);
127 | } else {
128 | return _callback(error, res);
129 | }
130 | }
131 |
132 | this.db.getCandidates(this.query, function (err, candidates) {
133 | if (err) { return callback(err); }
134 |
135 | try {
136 | for (i = 0; i < candidates.length; i += 1) {
137 | if (model.match(candidates[i], self.query)) {
138 | // If a sort is defined, wait for the results to be sorted before applying limit and skip
139 | if (!self._sort) {
140 | if (self._skip && self._skip > skipped) {
141 | skipped += 1;
142 | } else {
143 | res.push(candidates[i]);
144 | added += 1;
145 | if (self._limit && self._limit <= added) { break; }
146 | }
147 | } else {
148 | res.push(candidates[i]);
149 | }
150 | }
151 | }
152 | } catch (err) {
153 | return callback(err);
154 | }
155 |
156 | // Apply all sorts
157 | if (self._sort) {
158 | keys = Object.keys(self._sort);
159 |
160 | // Sorting
161 | var criteria = [];
162 | for (i = 0; i < keys.length; i++) {
163 | key = keys[i];
164 | criteria.push({ key: key, direction: self._sort[key] });
165 | }
166 | res.sort(function(a, b) {
167 | var criterion, compare, i;
168 | for (i = 0; i < criteria.length; i++) {
169 | criterion = criteria[i];
170 | compare = criterion.direction * model.compareThings(model.getDotValue(a, criterion.key), model.getDotValue(b, criterion.key), self.db.compareStrings);
171 | if (compare !== 0) {
172 | return compare;
173 | }
174 | }
175 | return 0;
176 | });
177 |
178 | // Applying limit and skip
179 | var limit = self._limit || res.length
180 | , skip = self._skip || 0;
181 |
182 | res = res.slice(skip, skip + limit);
183 | }
184 |
185 | // Apply projection
186 | try {
187 | res = self.project(res);
188 | } catch (e) {
189 | error = e;
190 | res = undefined;
191 | }
192 |
193 | return callback(error, res);
194 | });
195 | };
196 |
197 | Cursor.prototype.exec = function () {
198 | this.db.executor.push({ this: this, fn: this._exec, arguments: arguments });
199 | };
200 |
201 |
202 |
203 | // Interface
204 | module.exports = Cursor;
205 |
--------------------------------------------------------------------------------
/test/executor.test.js:
--------------------------------------------------------------------------------
1 | var should = require('chai').should()
2 | , assert = require('chai').assert
3 | , testDb = 'workspace/test.db'
4 | , fs = require('fs')
5 | , path = require('path')
6 | , _ = require('underscore')
7 | , async = require('async')
8 | , model = require('../lib/model')
9 | , Datastore = require('../lib/datastore')
10 | , Persistence = require('../lib/persistence')
11 | ;
12 |
13 |
14 | // Test that even if a callback throws an exception, the next DB operations will still be executed
15 | // We prevent Mocha from catching the exception we throw on purpose by remembering all current handlers, remove them and register them back after test ends
16 | function testThrowInCallback (d, done) {
17 | var currentUncaughtExceptionHandlers = process.listeners('uncaughtException');
18 |
19 | process.removeAllListeners('uncaughtException');
20 |
21 | process.on('uncaughtException', function (err) {
22 | // Do nothing with the error which is only there to test we stay on track
23 | });
24 |
25 | d.find({}, function (err) {
26 | process.nextTick(function () {
27 | d.insert({ bar: 1 }, function (err) {
28 | process.removeAllListeners('uncaughtException');
29 | for (var i = 0; i < currentUncaughtExceptionHandlers.length; i += 1) {
30 | process.on('uncaughtException', currentUncaughtExceptionHandlers[i]);
31 | }
32 |
33 | done();
34 | });
35 | });
36 |
37 | throw new Error('Some error');
38 | });
39 | }
40 |
41 | // Test that if the callback is falsy, the next DB operations will still be executed
42 | function testFalsyCallback (d, done) {
43 | d.insert({ a: 1 }, null);
44 | process.nextTick(function () {
45 | d.update({ a: 1 }, { a: 2 }, {}, null);
46 | process.nextTick(function () {
47 | d.update({ a: 2 }, { a: 1 }, null);
48 | process.nextTick(function () {
49 | d.remove({ a: 2 }, {}, null);
50 | process.nextTick(function () {
51 | d.remove({ a: 2 }, null);
52 | process.nextTick(function () {
53 | d.find({}, done);
54 | });
55 | });
56 | });
57 | });
58 | });
59 | }
60 |
61 | // Test that operations are executed in the right order
62 | // We prevent Mocha from catching the exception we throw on purpose by remembering all current handlers, remove them and register them back after test ends
63 | function testRightOrder (d, done) {
64 | var currentUncaughtExceptionHandlers = process.listeners('uncaughtException');
65 |
66 | process.removeAllListeners('uncaughtException');
67 |
68 | process.on('uncaughtException', function (err) {
69 | // Do nothing with the error which is only there to test we stay on track
70 | });
71 |
72 | d.find({}, function (err, docs) {
73 | docs.length.should.equal(0);
74 |
75 | d.insert({ a: 1 }, function () {
76 | d.update({ a: 1 }, { a: 2 }, {}, function () {
77 | d.find({}, function (err, docs) {
78 | docs[0].a.should.equal(2);
79 |
80 | process.nextTick(function () {
81 | d.update({ a: 2 }, { a: 3 }, {}, function () {
82 | d.find({}, function (err, docs) {
83 | docs[0].a.should.equal(3);
84 |
85 | process.removeAllListeners('uncaughtException');
86 | for (var i = 0; i < currentUncaughtExceptionHandlers.length; i += 1) {
87 | process.on('uncaughtException', currentUncaughtExceptionHandlers[i]);
88 | }
89 |
90 | done();
91 | });
92 | });
93 | });
94 |
95 | throw new Error('Some error');
96 | });
97 | });
98 | });
99 | });
100 | }
101 |
102 | // Note: The following test does not have any assertion because it
103 | // is meant to address the deprecation warning:
104 | // (node) warning: Recursive process.nextTick detected. This will break in the next version of node. Please use setImmediate for recursive deferral.
105 | // see
106 | var testEventLoopStarvation = function(d, done){
107 | var times = 1001;
108 | var i = 0;
109 | while ( i = 1; i -= 1) {
77 | j = Math.floor((i + 1) * Math.random());
78 | temp = res[i];
79 | res[i] = res[j];
80 | res[j] = temp;
81 | }
82 |
83 | return res;
84 | };
85 | module.exports.getRandomArray = getRandomArray;
86 |
87 |
88 | /**
89 | * Insert a certain number of documents for testing
90 | */
91 | module.exports.insertDocs = function (d, n, profiler, cb) {
92 | var beg = new Date()
93 | , order = getRandomArray(n)
94 | ;
95 |
96 | profiler.step('Begin inserting ' + n + ' docs');
97 |
98 | function runFrom(i) {
99 | if (i === n) { // Finished
100 | var opsPerSecond = Math.floor(1000* n / profiler.elapsedSinceLastStep());
101 | console.log("===== RESULT (insert) ===== " + opsPerSecond + " ops/s");
102 | profiler.step('Finished inserting ' + n + ' docs');
103 | profiler.insertOpsPerSecond = opsPerSecond;
104 | return cb();
105 | }
106 |
107 | d.insert({ docNumber: order[i] }, function (err) {
108 | executeAsap(function () {
109 | runFrom(i + 1);
110 | });
111 | });
112 | }
113 | runFrom(0);
114 | };
115 |
116 |
117 | /**
118 | * Find documents with find
119 | */
120 | module.exports.findDocs = function (d, n, profiler, cb) {
121 | var beg = new Date()
122 | , order = getRandomArray(n)
123 | ;
124 |
125 | profiler.step("Finding " + n + " documents");
126 |
127 | function runFrom(i) {
128 | if (i === n) { // Finished
129 | console.log("===== RESULT (find) ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s");
130 | profiler.step('Finished finding ' + n + ' docs');
131 | return cb();
132 | }
133 |
134 | d.find({ docNumber: order[i] }, function (err, docs) {
135 | if (docs.length !== 1 || docs[0].docNumber !== order[i]) { return cb('One find didnt work'); }
136 | executeAsap(function () {
137 | runFrom(i + 1);
138 | });
139 | });
140 | }
141 | runFrom(0);
142 | };
143 |
144 |
145 | /**
146 | * Find documents with find and the $in operator
147 | */
148 | module.exports.findDocsWithIn = function (d, n, profiler, cb) {
149 | var beg = new Date()
150 | , order = getRandomArray(n)
151 | , ins = [], i, j
152 | , arraySize = Math.min(10, n) // The array for $in needs to be smaller than n (inclusive)
153 | ;
154 |
155 | // Preparing all the $in arrays, will take some time
156 | for (i = 0; i < n; i += 1) {
157 | ins[i] = [];
158 |
159 | for (j = 0; j < arraySize; j += 1) {
160 | ins[i].push((i + j) % n);
161 | }
162 | }
163 |
164 | profiler.step("Finding " + n + " documents WITH $IN OPERATOR");
165 |
166 | function runFrom(i) {
167 | if (i === n) { // Finished
168 | console.log("===== RESULT (find with in selector) ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s");
169 | profiler.step('Finished finding ' + n + ' docs');
170 | return cb();
171 | }
172 |
173 | d.find({ docNumber: { $in: ins[i] } }, function (err, docs) {
174 | if (docs.length !== arraySize) { return cb('One find didnt work'); }
175 | executeAsap(function () {
176 | runFrom(i + 1);
177 | });
178 | });
179 | }
180 | runFrom(0);
181 | };
182 |
183 |
184 | /**
185 | * Find documents with findOne
186 | */
187 | module.exports.findOneDocs = function (d, n, profiler, cb) {
188 | var beg = new Date()
189 | , order = getRandomArray(n)
190 | ;
191 |
192 | profiler.step("FindingOne " + n + " documents");
193 |
194 | function runFrom(i) {
195 | if (i === n) { // Finished
196 | console.log("===== RESULT (findOne) ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s");
197 | profiler.step('Finished finding ' + n + ' docs');
198 | return cb();
199 | }
200 |
201 | d.findOne({ docNumber: order[i] }, function (err, doc) {
202 | if (!doc || doc.docNumber !== order[i]) { return cb('One find didnt work'); }
203 | executeAsap(function () {
204 | runFrom(i + 1);
205 | });
206 | });
207 | }
208 | runFrom(0);
209 | };
210 |
211 |
212 | /**
213 | * Update documents
214 | * options is the same as the options object for update
215 | */
216 | module.exports.updateDocs = function (options, d, n, profiler, cb) {
217 | var beg = new Date()
218 | , order = getRandomArray(n)
219 | ;
220 |
221 | profiler.step("Updating " + n + " documents");
222 |
223 | function runFrom(i) {
224 | if (i === n) { // Finished
225 | console.log("===== RESULT (update) ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s");
226 | profiler.step('Finished updating ' + n + ' docs');
227 | return cb();
228 | }
229 |
230 | // Will not actually modify the document but will take the same time
231 | d.update({ docNumber: order[i] }, { docNumber: order[i] }, options, function (err, nr) {
232 | if (err) { return cb(err); }
233 | if (nr !== 1) { return cb('One update didnt work'); }
234 | executeAsap(function () {
235 | runFrom(i + 1);
236 | });
237 | });
238 | }
239 | runFrom(0);
240 | };
241 |
242 |
243 | /**
244 | * Remove documents
245 | * options is the same as the options object for update
246 | */
247 | module.exports.removeDocs = function (options, d, n, profiler, cb) {
248 | var beg = new Date()
249 | , order = getRandomArray(n)
250 | ;
251 |
252 | profiler.step("Removing " + n + " documents");
253 |
254 | function runFrom(i) {
255 | if (i === n) { // Finished
256 | // opsPerSecond corresponds to 1 insert + 1 remove, needed to keep collection size at 10,000
257 | // We need to subtract the time taken by one insert to get the time actually taken by one remove
258 | var opsPerSecond = Math.floor(1000 * n / profiler.elapsedSinceLastStep());
259 | var removeOpsPerSecond = Math.floor(1 / ((1 / opsPerSecond) - (1 / profiler.insertOpsPerSecond)))
260 | console.log("===== RESULT (remove) ===== " + removeOpsPerSecond + " ops/s");
261 | profiler.step('Finished removing ' + n + ' docs');
262 | return cb();
263 | }
264 |
265 | d.remove({ docNumber: order[i] }, options, function (err, nr) {
266 | if (err) { return cb(err); }
267 | if (nr !== 1) { return cb('One remove didnt work'); }
268 | d.insert({ docNumber: order[i] }, function (err) { // We need to reinsert the doc so that we keep the collection's size at n
269 | // So actually we're calculating the average time taken by one insert + one remove
270 | executeAsap(function () {
271 | runFrom(i + 1);
272 | });
273 | });
274 | });
275 | }
276 | runFrom(0);
277 | };
278 |
279 |
280 | /**
281 | * Load database
282 | */
283 | module.exports.loadDatabase = function (d, n, profiler, cb) {
284 | var beg = new Date()
285 | , order = getRandomArray(n)
286 | ;
287 |
288 | profiler.step("Loading the database " + n + " times");
289 |
290 | function runFrom(i) {
291 | if (i === n) { // Finished
292 | console.log("===== RESULT ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s");
293 | profiler.step('Finished loading a database' + n + ' times');
294 | return cb();
295 | }
296 |
297 | d.loadDatabase(function (err) {
298 | executeAsap(function () {
299 | runFrom(i + 1);
300 | });
301 | });
302 | }
303 | runFrom(0);
304 | };
305 |
306 |
307 |
308 |
309 |
--------------------------------------------------------------------------------
/browser-version/test/nedb-browser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Testing the browser version of NeDB
3 | * The goal of these tests is not to be exhaustive, we have the server-side NeDB tests for that
4 | * This is more of a sanity check which executes most of the code at least once and checks
5 | * it behaves as the server version does
6 | */
7 |
8 | var assert = chai.assert;
9 |
10 | /**
11 | * Given a docs array and an id, return the document whose id matches, or null if none is found
12 | */
13 | function findById (docs, id) {
14 | return _.find(docs, function (doc) { return doc._id === id; }) || null;
15 | }
16 |
17 |
18 | describe('Basic CRUD functionality', function () {
19 |
20 | it('Able to create a database object in the browser', function () {
21 | var db = new Nedb();
22 |
23 | assert.equal(db.inMemoryOnly, true);
24 | assert.equal(db.persistence.inMemoryOnly, true);
25 | });
26 |
27 | it('Insertion and querying', function (done) {
28 | var db = new Nedb();
29 |
30 | db.insert({ a: 4 }, function (err, newDoc1) {
31 | assert.isNull(err);
32 | db.insert({ a: 40 }, function (err, newDoc2) {
33 | assert.isNull(err);
34 | db.insert({ a: 400 }, function (err, newDoc3) {
35 | assert.isNull(err);
36 |
37 | db.find({ a: { $gt: 36 } }, function (err, docs) {
38 | var doc2 = _.find(docs, function (doc) { return doc._id === newDoc2._id; })
39 | , doc3 = _.find(docs, function (doc) { return doc._id === newDoc3._id; })
40 | ;
41 |
42 | assert.isNull(err);
43 | assert.equal(docs.length, 2);
44 | assert.equal(doc2.a, 40);
45 | assert.equal(doc3.a, 400);
46 |
47 | db.find({ a: { $lt: 36 } }, function (err, docs) {
48 | assert.isNull(err);
49 | assert.equal(docs.length, 1);
50 | assert.equal(docs[0].a, 4);
51 | done();
52 | });
53 | });
54 | });
55 | });
56 | });
57 | });
58 |
59 | it('Querying with regular expressions', function (done) {
60 | var db = new Nedb();
61 |
62 | db.insert({ planet: 'Earth' }, function (err, newDoc1) {
63 | assert.isNull(err);
64 | db.insert({ planet: 'Mars' }, function (err, newDoc2) {
65 | assert.isNull(err);
66 | db.insert({ planet: 'Jupiter' }, function (err, newDoc3) {
67 | assert.isNull(err);
68 | db.insert({ planet: 'Eaaaaaarth' }, function (err, newDoc4) {
69 | assert.isNull(err);
70 | db.insert({ planet: 'Maaaars' }, function (err, newDoc5) {
71 | assert.isNull(err);
72 |
73 | db.find({ planet: /ar/ }, function (err, docs) {
74 | assert.isNull(err);
75 | assert.equal(docs.length, 4);
76 | assert.equal(_.find(docs, function (doc) { return doc._id === newDoc1._id; }).planet, 'Earth');
77 | assert.equal(_.find(docs, function (doc) { return doc._id === newDoc2._id; }).planet, 'Mars');
78 | assert.equal(_.find(docs, function (doc) { return doc._id === newDoc4._id; }).planet, 'Eaaaaaarth');
79 | assert.equal(_.find(docs, function (doc) { return doc._id === newDoc5._id; }).planet, 'Maaaars');
80 |
81 | db.find({ planet: /aa+r/ }, function (err, docs) {
82 | assert.isNull(err);
83 | assert.equal(docs.length, 2);
84 | assert.equal(_.find(docs, function (doc) { return doc._id === newDoc4._id; }).planet, 'Eaaaaaarth');
85 | assert.equal(_.find(docs, function (doc) { return doc._id === newDoc5._id; }).planet, 'Maaaars');
86 |
87 | done();
88 | });
89 | });
90 | });
91 | });
92 | });
93 | });
94 | });
95 | });
96 |
97 | it('Updating documents', function (done) {
98 | var db = new Nedb();
99 |
100 | db.insert({ planet: 'Eaaaaarth' }, function (err, newDoc1) {
101 | db.insert({ planet: 'Maaaaars' }, function (err, newDoc2) {
102 | // Simple update
103 | db.update({ _id: newDoc2._id }, { $set: { planet: 'Saturn' } }, {}, function (err, nr) {
104 | assert.isNull(err);
105 | assert.equal(nr, 1);
106 |
107 | db.find({}, function (err, docs) {
108 | assert.equal(docs.length, 2);
109 | assert.equal(findById(docs, newDoc1._id).planet, 'Eaaaaarth');
110 | assert.equal(findById(docs, newDoc2._id).planet, 'Saturn');
111 |
112 | // Failing update
113 | db.update({ _id: 'unknown' }, { $inc: { count: 1 } }, {}, function (err, nr) {
114 | assert.isNull(err);
115 | assert.equal(nr, 0);
116 |
117 | db.find({}, function (err, docs) {
118 | assert.equal(docs.length, 2);
119 | assert.equal(findById(docs, newDoc1._id).planet, 'Eaaaaarth');
120 | assert.equal(findById(docs, newDoc2._id).planet, 'Saturn');
121 |
122 | // Document replacement
123 | db.update({ planet: 'Eaaaaarth' }, { planet: 'Uranus' }, { multi: false }, function (err, nr) {
124 | assert.isNull(err);
125 | assert.equal(nr, 1);
126 |
127 | db.find({}, function (err, docs) {
128 | assert.equal(docs.length, 2);
129 | assert.equal(findById(docs, newDoc1._id).planet, 'Uranus');
130 | assert.equal(findById(docs, newDoc2._id).planet, 'Saturn');
131 |
132 | // Multi update
133 | db.update({}, { $inc: { count: 3 } }, { multi: true }, function (err, nr) {
134 | assert.isNull(err);
135 | assert.equal(nr, 2);
136 |
137 | db.find({}, function (err, docs) {
138 | assert.equal(docs.length, 2);
139 | assert.equal(findById(docs, newDoc1._id).planet, 'Uranus');
140 | assert.equal(findById(docs, newDoc1._id).count, 3);
141 | assert.equal(findById(docs, newDoc2._id).planet, 'Saturn');
142 | assert.equal(findById(docs, newDoc2._id).count, 3);
143 |
144 | done();
145 | });
146 | });
147 | });
148 | });
149 | });
150 | });
151 | });
152 | });
153 | });
154 | });
155 | });
156 |
157 | it('Updating documents: special modifiers', function (done) {
158 | var db = new Nedb();
159 |
160 | db.insert({ planet: 'Earth' }, function (err, newDoc1) {
161 | // Pushing to an array
162 | db.update({}, { $push: { satellites: 'Phobos' } }, {}, function (err, nr) {
163 | assert.isNull(err);
164 | assert.equal(nr, 1);
165 |
166 | db.findOne({}, function (err, doc) {
167 | assert.deepEqual(doc, { planet: 'Earth', _id: newDoc1._id, satellites: ['Phobos'] });
168 |
169 | db.update({}, { $push: { satellites: 'Deimos' } }, {}, function (err, nr) {
170 | assert.isNull(err);
171 | assert.equal(nr, 1);
172 |
173 | db.findOne({}, function (err, doc) {
174 | assert.deepEqual(doc, { planet: 'Earth', _id: newDoc1._id, satellites: ['Phobos', 'Deimos'] });
175 |
176 | done();
177 | });
178 | });
179 | });
180 | });
181 | });
182 | });
183 |
184 | it('Upserts', function (done) {
185 | var db = new Nedb();
186 |
187 | db.update({ a: 4 }, { $inc: { b: 1 } }, { upsert: true }, function (err, nr, upsert) {
188 | assert.isNull(err);
189 | // Return upserted document
190 | assert.equal(upsert.a, 4);
191 | assert.equal(upsert.b, 1);
192 | assert.equal(nr, 1);
193 |
194 | db.find({}, function (err, docs) {
195 | assert.equal(docs.length, 1);
196 | assert.equal(docs[0].a, 4);
197 | assert.equal(docs[0].b, 1);
198 |
199 | done();
200 | });
201 | });
202 | });
203 |
204 | it('Removing documents', function (done) {
205 | var db = new Nedb();
206 |
207 | db.insert({ a: 2 });
208 | db.insert({ a: 5 });
209 | db.insert({ a: 7 });
210 |
211 | // Multi remove
212 | db.remove({ a: { $in: [ 5, 7 ] } }, { multi: true }, function (err, nr) {
213 | assert.isNull(err);
214 | assert.equal(nr, 2);
215 |
216 | db.find({}, function (err, docs) {
217 | assert.equal(docs.length, 1);
218 | assert.equal(docs[0].a, 2);
219 |
220 | // Remove with no match
221 | db.remove({ b: { $exists: true } }, { multi: true }, function (err, nr) {
222 | assert.isNull(err);
223 | assert.equal(nr, 0);
224 |
225 | db.find({}, function (err, docs) {
226 | assert.equal(docs.length, 1);
227 | assert.equal(docs[0].a, 2);
228 |
229 | // Simple remove
230 | db.remove({ a: { $exists: true } }, { multi: true }, function (err, nr) {
231 | assert.isNull(err);
232 | assert.equal(nr, 1);
233 |
234 | db.find({}, function (err, docs) {
235 | assert.equal(docs.length, 0);
236 |
237 | done();
238 | });
239 | });
240 | });
241 | });
242 | });
243 | });
244 | });
245 |
246 | }); // ==== End of 'Basic CRUD functionality' ==== //
247 |
248 |
249 | describe('Indexing', function () {
250 |
251 | it('getCandidates works as expected', function (done) {
252 | var db = new Nedb();
253 |
254 | db.insert({ a: 4 }, function () {
255 | db.insert({ a: 6 }, function () {
256 | db.insert({ a: 7 }, function () {
257 | db.getCandidates({ a: 6 }, function (err, candidates) {
258 | console.log(candidates);
259 | assert.equal(candidates.length, 3);
260 | assert.isDefined(_.find(candidates, function (doc) { return doc.a === 4; }));
261 | assert.isDefined(_.find(candidates, function (doc) { return doc.a === 6; }));
262 | assert.isDefined(_.find(candidates, function (doc) { return doc.a === 7; }));
263 |
264 | db.ensureIndex({ fieldName: 'a' });
265 |
266 | db.getCandidates({ a: 6 }, function (err, candidates) {
267 | assert.equal(candidates.length, 1);
268 | assert.isDefined(_.find(candidates, function (doc) { return doc.a === 6; }));
269 |
270 | done();
271 | });
272 | });
273 | });
274 | });
275 | });
276 | });
277 |
278 | it('Can use indexes to enforce a unique constraint', function (done) {
279 | var db = new Nedb();
280 |
281 | db.ensureIndex({ fieldName: 'u', unique: true });
282 |
283 | db.insert({ u : 5 }, function (err) {
284 | assert.isNull(err);
285 |
286 | db.insert({ u : 98 }, function (err) {
287 | assert.isNull(err);
288 |
289 | db.insert({ u : 5 }, function (err) {
290 | assert.equal(err.errorType, 'uniqueViolated');
291 |
292 | done();
293 | });
294 | });
295 | });
296 | });
297 |
298 | }); // ==== End of 'Indexing' ==== //
299 |
300 |
301 | describe("Don't forget to launch persistence tests!", function () {
302 |
303 | it("See file testPersistence.html", function (done) {
304 | done();
305 | });
306 |
307 | }); // ===== End of 'persistent in-browser database' =====
308 |
309 |
310 |
--------------------------------------------------------------------------------
/browser-version/test/async.js:
--------------------------------------------------------------------------------
1 | !function(){function n(){}function t(n){return n}function e(n){return!!n}function r(n){return!n}function u(n){return function(){if(null===n)throw new Error("Callback was already called.");n.apply(this,arguments),n=null}}function i(n){return function(){null!==n&&(n.apply(this,arguments),n=null)}}function o(n){return M(n)||"number"==typeof n.length&&n.length>=0&&n.length%1===0}function c(n,t){for(var e=-1,r=n.length;++er?r:null}):(e=W(n),t=e.length,function(){return r++,t>r?e[r]:null})}function m(n,t){return t=null==t?n.length-1:+t,function(){for(var e=Math.max(arguments.length-t,0),r=Array(e),u=0;e>u;u++)r[u]=arguments[u+t];switch(t){case 0:return n.call(this,r);case 1:return n.call(this,arguments[0],r)}}}function y(n){return function(t,e,r){return n(t,r)}}function v(t){return function(e,r,o){o=i(o||n),e=e||[];var c=h(e);if(0>=t)return o(null);var a=!1,f=0,l=!1;!function s(){if(a&&0>=f)return o(null);for(;t>f&&!l;){var n=c();if(null===n)return a=!0,void(0>=f&&o(null));f+=1,r(e[n],n,u(function(n){f-=1,n?(o(n),l=!0):s()}))}}()}}function d(n){return function(t,e,r){return n(C.eachOf,t,e,r)}}function g(n){return function(t,e,r,u){return n(v(e),t,r,u)}}function k(n){return function(t,e,r){return n(C.eachOfSeries,t,e,r)}}function b(t,e,r,u){u=i(u||n),e=e||[];var c=o(e)?[]:{};t(e,function(n,t,e){r(n,function(n,r){c[t]=r,e(n)})},function(n){u(n,c)})}function w(n,t,e,r){var u=[];n(t,function(n,t,r){e(n,function(e){e&&u.push({index:t,value:n}),r()})},function(){r(a(u.sort(function(n,t){return n.index-t.index}),function(n){return n.value}))})}function O(n,t,e,r){w(n,t,function(n,t){e(n,function(n){t(!n)})},r)}function S(n,t,e){return function(r,u,i,o){function c(){o&&o(e(!1,void 0))}function a(n,r,u){return o?void i(n,function(r){o&&t(r)&&(o(e(!0,n)),o=i=!1),u()}):u()}arguments.length>3?n(r,u,a,c):(o=i,i=u,n(r,a,c))}}function E(n,t){return t}function L(t,e,r){r=r||n;var u=o(e)?[]:{};t(e,function(n,t,e){n(m(function(n,r){r.length<=1&&(r=r[0]),u[t]=r,e(n)}))},function(n){r(n,u)})}function I(n,t,e,r){var u=[];n(t,function(n,t,r){e(n,function(n,t){u=u.concat(t||[]),r(n)})},function(n){r(n,u)})}function x(t,e,r){function i(t,e,r,u){if(null!=u&&"function"!=typeof u)throw new Error("task callback must be a function");return t.started=!0,M(e)||(e=[e]),0===e.length&&t.idle()?C.setImmediate(function(){t.drain()}):(c(e,function(e){var i={data:e,callback:u||n};r?t.tasks.unshift(i):t.tasks.push(i),t.tasks.length===t.concurrency&&t.saturated()}),void C.setImmediate(t.process))}function o(n,t){return function(){f-=1;var e=!1,r=arguments;c(t,function(n){c(l,function(t,r){t!==n||e||(l.splice(r,1),e=!0)}),n.callback.apply(n,r)}),n.tasks.length+f===0&&n.drain(),n.process()}}if(null==e)e=1;else if(0===e)throw new Error("Concurrency must not be zero");var f=0,l=[],s={tasks:[],concurrency:e,payload:r,saturated:n,empty:n,drain:n,started:!1,paused:!1,push:function(n,t){i(s,n,!1,t)},kill:function(){s.drain=n,s.tasks=[]},unshift:function(n,t){i(s,n,!0,t)},process:function(){if(!s.paused&&f=t;t++)C.setImmediate(s.process)}}};return s}function j(n){return m(function(t,e){t.apply(null,e.concat([m(function(t,e){"object"==typeof console&&(t?console.error&&console.error(t):console[n]&&c(e,function(t){console[n](t)}))})]))})}function A(n){return function(t,e,r){n(f(t),e,r)}}function T(n){return m(function(t,e){var r=m(function(e){var r=this,u=e.pop();return n(t,function(n,t,u){n.apply(r,e.concat([u]))},u)});return e.length?r.apply(this,e):r})}function z(n){return m(function(t){var e=t.pop();t.push(function(){var n=arguments;r?C.setImmediate(function(){e.apply(null,n)}):e.apply(null,n)});var r=!0;n.apply(this,t),r=!1})}var q,C={},P="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||this;null!=P&&(q=P.async),C.noConflict=function(){return P.async=q,C};var H=Object.prototype.toString,M=Array.isArray||function(n){return"[object Array]"===H.call(n)},U=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},W=Object.keys||function(n){var t=[];for(var e in n)n.hasOwnProperty(e)&&t.push(e);return t},B="function"==typeof setImmediate&&setImmediate,D=B?function(n){B(n)}:function(n){setTimeout(n,0)};"object"==typeof process&&"function"==typeof process.nextTick?C.nextTick=process.nextTick:C.nextTick=D,C.setImmediate=B?D:C.nextTick,C.forEach=C.each=function(n,t,e){return C.eachOf(n,y(t),e)},C.forEachSeries=C.eachSeries=function(n,t,e){return C.eachOfSeries(n,y(t),e)},C.forEachLimit=C.eachLimit=function(n,t,e,r){return v(t)(n,y(e),r)},C.forEachOf=C.eachOf=function(t,e,r){function o(n){f--,n?r(n):null===c&&0>=f&&r(null)}r=i(r||n),t=t||[];for(var c,a=h(t),f=0;null!=(c=a());)f+=1,e(t[c],c,u(o));0===f&&r(null)},C.forEachOfSeries=C.eachOfSeries=function(t,e,r){function o(){var n=!0;return null===a?r(null):(e(t[a],a,u(function(t){if(t)r(t);else{if(a=c(),null===a)return r(null);n?C.setImmediate(o):o()}})),void(n=!1))}r=i(r||n),t=t||[];var c=h(t),a=c();o()},C.forEachOfLimit=C.eachOfLimit=function(n,t,e,r){v(t)(n,e,r)},C.map=d(b),C.mapSeries=k(b),C.mapLimit=g(b),C.inject=C.foldl=C.reduce=function(n,t,e,r){C.eachOfSeries(n,function(n,r,u){e(t,n,function(n,e){t=e,u(n)})},function(n){r(n,t)})},C.foldr=C.reduceRight=function(n,e,r,u){var i=a(n,t).reverse();C.reduce(i,e,r,u)},C.transform=function(n,t,e,r){3===arguments.length&&(r=e,e=t,t=M(n)?[]:{}),C.eachOf(n,function(n,r,u){e(t,n,r,u)},function(n){r(n,t)})},C.select=C.filter=d(w),C.selectLimit=C.filterLimit=g(w),C.selectSeries=C.filterSeries=k(w),C.reject=d(O),C.rejectLimit=g(O),C.rejectSeries=k(O),C.any=C.some=S(C.eachOf,e,t),C.someLimit=S(C.eachOfLimit,e,t),C.all=C.every=S(C.eachOf,r,r),C.everyLimit=S(C.eachOfLimit,r,r),C.detect=S(C.eachOf,t,E),C.detectSeries=S(C.eachOfSeries,t,E),C.detectLimit=S(C.eachOfLimit,t,E),C.sortBy=function(n,t,e){function r(n,t){var e=n.criteria,r=t.criteria;return r>e?-1:e>r?1:0}C.map(n,function(n,e){t(n,function(t,r){t?e(t):e(null,{value:n,criteria:r})})},function(n,t){return n?e(n):void e(null,a(t.sort(r),function(n){return n.value}))})},C.auto=function(t,e,r){function u(n){d.unshift(n)}function o(n){var t=p(d,n);t>=0&&d.splice(t,1)}function a(){h--,c(d.slice(0),function(n){n()})}r||(r=e,e=null),r=i(r||n);var f=W(t),h=f.length;if(!h)return r(null);e||(e=h);var y={},v=0,d=[];u(function(){h||r(null,y)}),c(f,function(n){function i(){return e>v&&l(g,function(n,t){return n&&y.hasOwnProperty(t)},!0)&&!y.hasOwnProperty(n)}function c(){i()&&(v++,o(c),h[h.length-1](d,y))}for(var f,h=M(t[n])?t[n]:[t[n]],d=m(function(t,e){if(v--,e.length<=1&&(e=e[0]),t){var u={};s(y,function(n,t){u[t]=n}),u[n]=e,r(t,u)}else y[n]=e,C.setImmediate(a)}),g=h.slice(0,h.length-1),k=g.length;k--;){if(!(f=t[g[k]]))throw new Error("Has inexistant dependency");if(M(f)&&p(f,n)>=0)throw new Error("Has cyclic dependencies")}i()?(v++,h[h.length-1](d,y)):u(c)})},C.retry=function(n,t,e){function r(n,t){if("number"==typeof t)n.times=parseInt(t,10)||i;else{if("object"!=typeof t)throw new Error("Unsupported argument type for 'times': "+typeof t);n.times=parseInt(t.times,10)||i,n.interval=parseInt(t.interval,10)||o}}function u(n,t){function e(n,e){return function(r){n(function(n,t){r(!n||e,{err:n,result:t})},t)}}function r(n){return function(t){setTimeout(function(){t(null)},n)}}for(;a.times;){var u=!(a.times-=1);c.push(e(a.task,u)),!u&&a.interval>0&&c.push(r(a.interval))}C.series(c,function(t,e){e=e[e.length-1],(n||a.callback)(e.err,e.result)})}var i=5,o=0,c=[],a={times:i,interval:o},f=arguments.length;if(1>f||f>3)throw new Error("Invalid arguments - must be either (task), (task, callback), (times, task) or (times, task, callback)");return 2>=f&&"function"==typeof n&&(e=t,t=n),"function"!=typeof n&&r(a,n),a.callback=e,a.task=t,a.callback?u():u},C.waterfall=function(t,e){function r(n){return m(function(t,u){if(t)e.apply(null,[t].concat(u));else{var i=n.next();i?u.push(r(i)):u.push(e),z(n).apply(null,u)}})}if(e=i(e||n),!M(t)){var u=new Error("First argument to waterfall must be an array of functions");return e(u)}return t.length?void r(C.iterator(t))():e()},C.parallel=function(n,t){L(C.eachOf,n,t)},C.parallelLimit=function(n,t,e){L(v(t),n,e)},C.series=function(n,t){L(C.eachOfSeries,n,t)},C.iterator=function(n){function t(e){function r(){return n.length&&n[e].apply(null,arguments),r.next()}return r.next=function(){return er;){var i=r+(u-r+1>>>1);e(t,n[i])>=0?r=i:u=i-1}return r}function i(t,e,i,o){if(null!=o&&"function"!=typeof o)throw new Error("task callback must be a function");return t.started=!0,M(e)||(e=[e]),0===e.length?C.setImmediate(function(){t.drain()}):void c(e,function(e){var c={data:e,priority:i,callback:"function"==typeof o?o:n};t.tasks.splice(u(t.tasks,c,r)+1,0,c),t.tasks.length===t.concurrency&&t.saturated(),C.setImmediate(t.process)})}var o=C.queue(t,e);return o.push=function(n,t,e){i(o,n,t,e)},delete o.unshift,o},C.cargo=function(n,t){return x(n,1,t)},C.log=j("log"),C.dir=j("dir"),C.memoize=function(n,e){var r={},u={};e=e||t;var i=m(function(t){var i=t.pop(),o=e.apply(null,t);o in r?C.setImmediate(function(){i.apply(null,r[o])}):o in u?u[o].push(i):(u[o]=[i],n.apply(null,t.concat([m(function(n){r[o]=n;var t=u[o];delete u[o];for(var e=0,i=t.length;i>e;e++)t[e].apply(null,n)})])))});return i.memo=r,i.unmemoized=n,i},C.unmemoize=function(n){return function(){return(n.unmemoized||n).apply(null,arguments)}},C.times=A(C.map),C.timesSeries=A(C.mapSeries),C.timesLimit=function(n,t,e,r){return C.mapLimit(f(n),t,e,r)},C.seq=function(){var t=arguments;return m(function(e){var r=this,u=e[e.length-1];"function"==typeof u?e.pop():u=n,C.reduce(t,e,function(n,t,e){t.apply(r,n.concat([m(function(n,t){e(n,t)})]))},function(n,t){u.apply(r,[n].concat(t))})})},C.compose=function(){return C.seq.apply(null,Array.prototype.reverse.call(arguments))},C.applyEach=T(C.eachOf),C.applyEachSeries=T(C.eachOfSeries),C.forever=function(t,e){function r(n){return n?i(n):void o(r)}var i=u(e||n),o=z(t);r()},C.ensureAsync=z,C.constant=m(function(n){var t=[null].concat(n);return function(n){return n.apply(this,t)}}),C.wrapSync=C.asyncify=function(n){return m(function(t){var e,r=t.pop();try{e=n.apply(this,t)}catch(u){return r(u)}U(e)&&"function"==typeof e.then?e.then(function(n){r(null,n)})["catch"](function(n){r(n.message?n:new Error(n))}):r(null,e)})},"object"==typeof module&&module.exports?module.exports=C:"function"==typeof define&&define.amd?define([],function(){return C}):P.async=C}();
2 |
3 |
--------------------------------------------------------------------------------
/lib/persistence.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Handle every persistence-related task
3 | * The interface Datastore expects to be implemented is
4 | * * Persistence.loadDatabase(callback) and callback has signature err
5 | * * Persistence.persistNewState(newDocs, callback) where newDocs is an array of documents and callback has signature err
6 | */
7 |
8 | var storage = require('./storage')
9 | , path = require('path')
10 | , model = require('./model')
11 | , async = require('async')
12 | , customUtils = require('./customUtils')
13 | , Index = require('./indexes')
14 | ;
15 |
16 |
17 | /**
18 | * Create a new Persistence object for database options.db
19 | * @param {Datastore} options.db
20 | * @param {Boolean} options.nodeWebkitAppName Optional, specify the name of your NW app if you want options.filename to be relative to the directory where
21 | * Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion)
22 | */
23 | function Persistence (options) {
24 | var i, j, randomString;
25 |
26 | this.db = options.db;
27 | this.inMemoryOnly = this.db.inMemoryOnly;
28 | this.filename = this.db.filename;
29 | this.corruptAlertThreshold = options.corruptAlertThreshold !== undefined ? options.corruptAlertThreshold : 0.1;
30 |
31 | if (!this.inMemoryOnly && this.filename && this.filename.charAt(this.filename.length - 1) === '~') {
32 | throw new Error("The datafile name can't end with a ~, which is reserved for crash safe backup files");
33 | }
34 |
35 | // After serialization and before deserialization hooks with some basic sanity checks
36 | if (options.afterSerialization && !options.beforeDeserialization) {
37 | throw new Error("Serialization hook defined but deserialization hook undefined, cautiously refusing to start NeDB to prevent dataloss");
38 | }
39 | if (!options.afterSerialization && options.beforeDeserialization) {
40 | throw new Error("Serialization hook undefined but deserialization hook defined, cautiously refusing to start NeDB to prevent dataloss");
41 | }
42 | this.afterSerialization = options.afterSerialization || function (s) { return s; };
43 | this.beforeDeserialization = options.beforeDeserialization || function (s) { return s; };
44 | for (i = 1; i < 30; i += 1) {
45 | for (j = 0; j < 10; j += 1) {
46 | randomString = customUtils.uid(i);
47 | if (this.beforeDeserialization(this.afterSerialization(randomString)) !== randomString) {
48 | throw new Error("beforeDeserialization is not the reverse of afterSerialization, cautiously refusing to start NeDB to prevent dataloss");
49 | }
50 | }
51 | }
52 |
53 | // For NW apps, store data in the same directory where NW stores application data
54 | if (this.filename && options.nodeWebkitAppName) {
55 | console.log("==================================================================");
56 | console.log("WARNING: The nodeWebkitAppName option is deprecated");
57 | console.log("To get the path to the directory where Node Webkit stores the data");
58 | console.log("for your app, use the internal nw.gui module like this");
59 | console.log("require('nw.gui').App.dataPath");
60 | console.log("See https://github.com/rogerwang/node-webkit/issues/500");
61 | console.log("==================================================================");
62 | this.filename = Persistence.getNWAppFilename(options.nodeWebkitAppName, this.filename);
63 | }
64 | };
65 |
66 |
67 | /**
68 | * Check if a directory exists and create it on the fly if it is not the case
69 | * cb is optional, signature: err
70 | */
71 | Persistence.ensureDirectoryExists = function (dir, cb) {
72 | var callback = cb || function () {}
73 | ;
74 |
75 | storage.mkdirp(dir, function (err) { return callback(err); });
76 | };
77 |
78 |
79 |
80 |
81 | /**
82 | * Return the path the datafile if the given filename is relative to the directory where Node Webkit stores
83 | * data for this application. Probably the best place to store data
84 | */
85 | Persistence.getNWAppFilename = function (appName, relativeFilename) {
86 | var home;
87 |
88 | switch (process.platform) {
89 | case 'win32':
90 | case 'win64':
91 | home = process.env.LOCALAPPDATA || process.env.APPDATA;
92 | if (!home) { throw new Error("Couldn't find the base application data folder"); }
93 | home = path.join(home, appName);
94 | break;
95 | case 'darwin':
96 | home = process.env.HOME;
97 | if (!home) { throw new Error("Couldn't find the base application data directory"); }
98 | home = path.join(home, 'Library', 'Application Support', appName);
99 | break;
100 | case 'linux':
101 | home = process.env.HOME;
102 | if (!home) { throw new Error("Couldn't find the base application data directory"); }
103 | home = path.join(home, '.config', appName);
104 | break;
105 | default:
106 | throw new Error("Can't use the Node Webkit relative path for platform " + process.platform);
107 | break;
108 | }
109 |
110 | return path.join(home, 'nedb-data', relativeFilename);
111 | }
112 |
113 |
114 | /**
115 | * Persist cached database
116 | * This serves as a compaction function since the cache always contains only the number of documents in the collection
117 | * while the data file is append-only so it may grow larger
118 | * @param {Function} cb Optional callback, signature: err
119 | */
120 | Persistence.prototype.persistCachedDatabase = function (cb) {
121 | var callback = cb || function () {}
122 | , toPersist = ''
123 | , self = this
124 | ;
125 |
126 | if (this.inMemoryOnly) { return callback(null); }
127 |
128 | this.db.getAllData().forEach(function (doc) {
129 | toPersist += self.afterSerialization(model.serialize(doc)) + '\n';
130 | });
131 | Object.keys(this.db.indexes).forEach(function (fieldName) {
132 | if (fieldName != "_id") { // The special _id index is managed by datastore.js, the others need to be persisted
133 | toPersist += self.afterSerialization(model.serialize({ $$indexCreated: { fieldName: fieldName, unique: self.db.indexes[fieldName].unique, sparse: self.db.indexes[fieldName].sparse }})) + '\n';
134 | }
135 | });
136 |
137 | storage.crashSafeWriteFile(this.filename, toPersist, function (err) {
138 | if (err) { return callback(err); }
139 | self.db.emit('compaction.done');
140 | return callback(null);
141 | });
142 | };
143 |
144 |
145 | /**
146 | * Queue a rewrite of the datafile
147 | */
148 | Persistence.prototype.compactDatafile = function () {
149 | this.db.executor.push({ this: this, fn: this.persistCachedDatabase, arguments: [] });
150 | };
151 |
152 |
153 | /**
154 | * Set automatic compaction every interval ms
155 | * @param {Number} interval in milliseconds, with an enforced minimum of 5 seconds
156 | */
157 | Persistence.prototype.setAutocompactionInterval = function (interval) {
158 | var self = this
159 | , minInterval = 5000
160 | , realInterval = Math.max(interval || 0, minInterval)
161 | ;
162 |
163 | this.stopAutocompaction();
164 |
165 | this.autocompactionIntervalId = setInterval(function () {
166 | self.compactDatafile();
167 | }, realInterval);
168 | };
169 |
170 |
171 | /**
172 | * Stop autocompaction (do nothing if autocompaction was not running)
173 | */
174 | Persistence.prototype.stopAutocompaction = function () {
175 | if (this.autocompactionIntervalId) { clearInterval(this.autocompactionIntervalId); }
176 | };
177 |
178 |
179 | /**
180 | * Persist new state for the given newDocs (can be insertion, update or removal)
181 | * Use an append-only format
182 | * @param {Array} newDocs Can be empty if no doc was updated/removed
183 | * @param {Function} cb Optional, signature: err
184 | */
185 | Persistence.prototype.persistNewState = function (newDocs, cb) {
186 | var self = this
187 | , toPersist = ''
188 | , callback = cb || function () {}
189 | ;
190 |
191 | // In-memory only datastore
192 | if (self.inMemoryOnly) { return callback(null); }
193 |
194 | newDocs.forEach(function (doc) {
195 | toPersist += self.afterSerialization(model.serialize(doc)) + '\n';
196 | });
197 |
198 | if (toPersist.length === 0) { return callback(null); }
199 |
200 | storage.appendFile(self.filename, toPersist, 'utf8', function (err) {
201 | return callback(err);
202 | });
203 | };
204 |
205 |
206 | /**
207 | * From a database's raw data, return the corresponding
208 | * machine understandable collection
209 | */
210 | Persistence.prototype.treatRawData = function (rawData) {
211 | var data = rawData.split('\n')
212 | , dataById = {}
213 | , tdata = []
214 | , i
215 | , indexes = {}
216 | , corruptItems = -1 // Last line of every data file is usually blank so not really corrupt
217 | ;
218 |
219 | for (i = 0; i < data.length; i += 1) {
220 | var doc;
221 |
222 | try {
223 | doc = model.deserialize(this.beforeDeserialization(data[i]));
224 | if (doc._id) {
225 | if (doc.$$deleted === true) {
226 | delete dataById[doc._id];
227 | } else {
228 | dataById[doc._id] = doc;
229 | }
230 | } else if (doc.$$indexCreated && doc.$$indexCreated.fieldName != undefined) {
231 | indexes[doc.$$indexCreated.fieldName] = doc.$$indexCreated;
232 | } else if (typeof doc.$$indexRemoved === "string") {
233 | delete indexes[doc.$$indexRemoved];
234 | }
235 | } catch (e) {
236 | corruptItems += 1;
237 | }
238 | }
239 |
240 | // A bit lenient on corruption
241 | if (data.length > 0 && corruptItems / data.length > this.corruptAlertThreshold) {
242 | throw new Error("More than " + Math.floor(100 * this.corruptAlertThreshold) + "% of the data file is corrupt, the wrong beforeDeserialization hook may be used. Cautiously refusing to start NeDB to prevent dataloss");
243 | }
244 |
245 | Object.keys(dataById).forEach(function (k) {
246 | tdata.push(dataById[k]);
247 | });
248 |
249 | return { data: tdata, indexes: indexes };
250 | };
251 |
252 |
253 | /**
254 | * Load the database
255 | * 1) Create all indexes
256 | * 2) Insert all data
257 | * 3) Compact the database
258 | * This means pulling data out of the data file or creating it if it doesn't exist
259 | * Also, all data is persisted right away, which has the effect of compacting the database file
260 | * This operation is very quick at startup for a big collection (60ms for ~10k docs)
261 | * @param {Function} cb Optional callback, signature: err
262 | */
263 | Persistence.prototype.loadDatabase = function (cb) {
264 | var callback = cb || function () {}
265 | , self = this
266 | ;
267 |
268 | self.db.resetIndexes();
269 |
270 | // In-memory only datastore
271 | if (self.inMemoryOnly) { return callback(null); }
272 |
273 | async.waterfall([
274 | function (cb) {
275 | Persistence.ensureDirectoryExists(path.dirname(self.filename), function (err) {
276 | storage.ensureDatafileIntegrity(self.filename, function (err) {
277 | storage.readFile(self.filename, 'utf8', function (err, rawData) {
278 | if (err) { return cb(err); }
279 |
280 | try {
281 | var treatedData = self.treatRawData(rawData);
282 | } catch (e) {
283 | return cb(e);
284 | }
285 |
286 | // Recreate all indexes in the datafile
287 | Object.keys(treatedData.indexes).forEach(function (key) {
288 | self.db.indexes[key] = new Index(treatedData.indexes[key]);
289 | });
290 |
291 | // Fill cached database (i.e. all indexes) with data
292 | try {
293 | self.db.resetIndexes(treatedData.data);
294 | } catch (e) {
295 | self.db.resetIndexes(); // Rollback any index which didn't fail
296 | return cb(e);
297 | }
298 |
299 | self.db.persistence.persistCachedDatabase(cb);
300 | });
301 | });
302 | });
303 | }
304 | ], function (err) {
305 | if (err) { return callback(err); }
306 |
307 | self.db.executor.processBuffer();
308 | return callback(null);
309 | });
310 | };
311 |
312 |
313 | // Interface
314 | module.exports = Persistence;
315 |
--------------------------------------------------------------------------------
/browser-version/test/underscore.min.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.8.3
2 | // http://underscorejs.org
3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4 | // Underscore may be freely distributed under the MIT license.
5 | (function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this);
6 | //# sourceMappingURL=underscore-min.map
7 |
--------------------------------------------------------------------------------
/lib/datastore.js:
--------------------------------------------------------------------------------
1 | var customUtils = require('./customUtils')
2 | , model = require('./model')
3 | , async = require('async')
4 | , Executor = require('./executor')
5 | , Index = require('./indexes')
6 | , util = require('util')
7 | , _ = require('underscore')
8 | , Persistence = require('./persistence')
9 | , Cursor = require('./cursor')
10 | ;
11 |
12 |
13 | /**
14 | * Create a new collection
15 | * @param {String} options.filename Optional, datastore will be in-memory only if not provided
16 | * @param {Boolean} options.timestampData Optional, defaults to false. If set to true, createdAt and updatedAt will be created and populated automatically (if not specified by user)
17 | * @param {Boolean} options.inMemoryOnly Optional, defaults to false
18 | * @param {String} options.nodeWebkitAppName Optional, specify the name of your NW app if you want options.filename to be relative to the directory where
19 | * Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion)
20 | * @param {Boolean} options.autoload Optional, defaults to false
21 | * @param {Function} options.onload Optional, if autoload is used this will be called after the load database with the error object as parameter. If you don't pass it the error will be thrown
22 | * @param {Function} options.afterSerialization/options.beforeDeserialization Optional, serialization hooks
23 | * @param {Number} options.corruptAlertThreshold Optional, threshold after which an alert is thrown if too much data is corrupt
24 | * @param {Function} options.compareStrings Optional, string comparison function that overrides default for sorting
25 | *
26 | * Event Emitter - Events
27 | * * compaction.done - Fired whenever a compaction operation was finished
28 | */
29 | function Datastore (options) {
30 | var filename;
31 |
32 | // Retrocompatibility with v0.6 and before
33 | if (typeof options === 'string') {
34 | filename = options;
35 | this.inMemoryOnly = false; // Default
36 | } else {
37 | options = options || {};
38 | filename = options.filename;
39 | this.inMemoryOnly = options.inMemoryOnly || false;
40 | this.autoload = options.autoload || false;
41 | this.timestampData = options.timestampData || false;
42 | }
43 |
44 | // Determine whether in memory or persistent
45 | if (!filename || typeof filename !== 'string' || filename.length === 0) {
46 | this.filename = null;
47 | this.inMemoryOnly = true;
48 | } else {
49 | this.filename = filename;
50 | }
51 |
52 | // String comparison function
53 | this.compareStrings = options.compareStrings;
54 |
55 | // Persistence handling
56 | this.persistence = new Persistence({ db: this, nodeWebkitAppName: options.nodeWebkitAppName
57 | , afterSerialization: options.afterSerialization
58 | , beforeDeserialization: options.beforeDeserialization
59 | , corruptAlertThreshold: options.corruptAlertThreshold
60 | });
61 |
62 | // This new executor is ready if we don't use persistence
63 | // If we do, it will only be ready once loadDatabase is called
64 | this.executor = new Executor();
65 | if (this.inMemoryOnly) { this.executor.ready = true; }
66 |
67 | // Indexed by field name, dot notation can be used
68 | // _id is always indexed and since _ids are generated randomly the underlying
69 | // binary is always well-balanced
70 | this.indexes = {};
71 | this.indexes._id = new Index({ fieldName: '_id', unique: true });
72 | this.ttlIndexes = {};
73 |
74 | // Queue a load of the database right away and call the onload handler
75 | // By default (no onload handler), if there is an error there, no operation will be possible so warn the user by throwing an exception
76 | if (this.autoload) { this.loadDatabase(options.onload || function (err) {
77 | if (err) { throw err; }
78 | }); }
79 | }
80 |
81 | util.inherits(Datastore, require('events').EventEmitter);
82 |
83 |
84 | /**
85 | * Load the database from the datafile, and trigger the execution of buffered commands if any
86 | */
87 | Datastore.prototype.loadDatabase = function () {
88 | this.executor.push({ this: this.persistence, fn: this.persistence.loadDatabase, arguments: arguments }, true);
89 | };
90 |
91 |
92 | /**
93 | * Get an array of all the data in the database
94 | */
95 | Datastore.prototype.getAllData = function () {
96 | return this.indexes._id.getAll();
97 | };
98 |
99 |
100 | /**
101 | * Reset all currently defined indexes
102 | */
103 | Datastore.prototype.resetIndexes = function (newData) {
104 | var self = this;
105 |
106 | Object.keys(this.indexes).forEach(function (i) {
107 | self.indexes[i].reset(newData);
108 | });
109 | };
110 |
111 |
112 | /**
113 | * Ensure an index is kept for this field. Same parameters as lib/indexes
114 | * For now this function is synchronous, we need to test how much time it takes
115 | * We use an async API for consistency with the rest of the code
116 | * @param {String} options.fieldName
117 | * @param {Boolean} options.unique
118 | * @param {Boolean} options.sparse
119 | * @param {Number} options.expireAfterSeconds - Optional, if set this index becomes a TTL index (only works on Date fields, not arrays of Date)
120 | * @param {Function} cb Optional callback, signature: err
121 | */
122 | Datastore.prototype.ensureIndex = function (options, cb) {
123 | var err
124 | , callback = cb || function () {};
125 |
126 | options = options || {};
127 |
128 | if (!options.fieldName) {
129 | err = new Error("Cannot create an index without a fieldName");
130 | err.missingFieldName = true;
131 | return callback(err);
132 | }
133 | if (this.indexes[options.fieldName]) { return callback(null); }
134 |
135 | this.indexes[options.fieldName] = new Index(options);
136 | if (options.expireAfterSeconds !== undefined) { this.ttlIndexes[options.fieldName] = options.expireAfterSeconds; } // With this implementation index creation is not necessary to ensure TTL but we stick with MongoDB's API here
137 |
138 | try {
139 | this.indexes[options.fieldName].insert(this.getAllData());
140 | } catch (e) {
141 | delete this.indexes[options.fieldName];
142 | return callback(e);
143 | }
144 |
145 | // We may want to force all options to be persisted including defaults, not just the ones passed the index creation function
146 | this.persistence.persistNewState([{ $$indexCreated: options }], function (err) {
147 | if (err) { return callback(err); }
148 | return callback(null);
149 | });
150 | };
151 |
152 |
153 | /**
154 | * Remove an index
155 | * @param {String} fieldName
156 | * @param {Function} cb Optional callback, signature: err
157 | */
158 | Datastore.prototype.removeIndex = function (fieldName, cb) {
159 | var callback = cb || function () {};
160 |
161 | delete this.indexes[fieldName];
162 |
163 | this.persistence.persistNewState([{ $$indexRemoved: fieldName }], function (err) {
164 | if (err) { return callback(err); }
165 | return callback(null);
166 | });
167 | };
168 |
169 |
170 | /**
171 | * Add one or several document(s) to all indexes
172 | */
173 | Datastore.prototype.addToIndexes = function (doc) {
174 | var i, failingIndex, error
175 | , keys = Object.keys(this.indexes)
176 | ;
177 |
178 | for (i = 0; i < keys.length; i += 1) {
179 | try {
180 | this.indexes[keys[i]].insert(doc);
181 | } catch (e) {
182 | failingIndex = i;
183 | error = e;
184 | break;
185 | }
186 | }
187 |
188 | // If an error happened, we need to rollback the insert on all other indexes
189 | if (error) {
190 | for (i = 0; i < failingIndex; i += 1) {
191 | this.indexes[keys[i]].remove(doc);
192 | }
193 |
194 | throw error;
195 | }
196 | };
197 |
198 |
199 | /**
200 | * Remove one or several document(s) from all indexes
201 | */
202 | Datastore.prototype.removeFromIndexes = function (doc) {
203 | var self = this;
204 |
205 | Object.keys(this.indexes).forEach(function (i) {
206 | self.indexes[i].remove(doc);
207 | });
208 | };
209 |
210 |
211 | /**
212 | * Update one or several documents in all indexes
213 | * To update multiple documents, oldDoc must be an array of { oldDoc, newDoc } pairs
214 | * If one update violates a constraint, all changes are rolled back
215 | */
216 | Datastore.prototype.updateIndexes = function (oldDoc, newDoc) {
217 | var i, failingIndex, error
218 | , keys = Object.keys(this.indexes)
219 | ;
220 |
221 | for (i = 0; i < keys.length; i += 1) {
222 | try {
223 | this.indexes[keys[i]].update(oldDoc, newDoc);
224 | } catch (e) {
225 | failingIndex = i;
226 | error = e;
227 | break;
228 | }
229 | }
230 |
231 | // If an error happened, we need to rollback the update on all other indexes
232 | if (error) {
233 | for (i = 0; i < failingIndex; i += 1) {
234 | this.indexes[keys[i]].revertUpdate(oldDoc, newDoc);
235 | }
236 |
237 | throw error;
238 | }
239 | };
240 |
241 |
242 | /**
243 | * Return the list of candidates for a given query
244 | * Crude implementation for now, we return the candidates given by the first usable index if any
245 | * We try the following query types, in this order: basic match, $in match, comparison match
246 | * One way to make it better would be to enable the use of multiple indexes if the first usable index
247 | * returns too much data. I may do it in the future.
248 | *
249 | * Returned candidates will be scanned to find and remove all expired documents
250 | *
251 | * @param {Query} query
252 | * @param {Boolean} dontExpireStaleDocs Optional, defaults to false, if true don't remove stale docs. Useful for the remove function which shouldn't be impacted by expirations
253 | * @param {Function} callback Signature err, candidates
254 | */
255 | Datastore.prototype.getCandidates = function (query, dontExpireStaleDocs, callback) {
256 | var indexNames = Object.keys(this.indexes)
257 | , self = this
258 | , usableQueryKeys;
259 |
260 | if (typeof dontExpireStaleDocs === 'function') {
261 | callback = dontExpireStaleDocs;
262 | dontExpireStaleDocs = false;
263 | }
264 |
265 |
266 | async.waterfall([
267 | // STEP 1: get candidates list by checking indexes from most to least frequent usecase
268 | function (cb) {
269 | // For a basic match
270 | usableQueryKeys = [];
271 | Object.keys(query).forEach(function (k) {
272 | if (typeof query[k] === 'string' || typeof query[k] === 'number' || typeof query[k] === 'boolean' || util.isDate(query[k]) || query[k] === null) {
273 | usableQueryKeys.push(k);
274 | }
275 | });
276 | usableQueryKeys = _.intersection(usableQueryKeys, indexNames);
277 | if (usableQueryKeys.length > 0) {
278 | return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]]));
279 | }
280 |
281 | // For a $in match
282 | usableQueryKeys = [];
283 | Object.keys(query).forEach(function (k) {
284 | if (query[k] && query[k].hasOwnProperty('$in')) {
285 | usableQueryKeys.push(k);
286 | }
287 | });
288 | usableQueryKeys = _.intersection(usableQueryKeys, indexNames);
289 | if (usableQueryKeys.length > 0) {
290 | return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]].$in));
291 | }
292 |
293 | // For a comparison match
294 | usableQueryKeys = [];
295 | Object.keys(query).forEach(function (k) {
296 | if (query[k] && (query[k].hasOwnProperty('$lt') || query[k].hasOwnProperty('$lte') || query[k].hasOwnProperty('$gt') || query[k].hasOwnProperty('$gte'))) {
297 | usableQueryKeys.push(k);
298 | }
299 | });
300 | usableQueryKeys = _.intersection(usableQueryKeys, indexNames);
301 | if (usableQueryKeys.length > 0) {
302 | return cb(null, self.indexes[usableQueryKeys[0]].getBetweenBounds(query[usableQueryKeys[0]]));
303 | }
304 |
305 | // By default, return all the DB data
306 | return cb(null, self.getAllData());
307 | }
308 | // STEP 2: remove all expired documents
309 | , function (docs) {
310 | if (dontExpireStaleDocs) { return callback(null, docs); }
311 |
312 | var expiredDocsIds = [], validDocs = [], ttlIndexesFieldNames = Object.keys(self.ttlIndexes);
313 |
314 | docs.forEach(function (doc) {
315 | var valid = true;
316 | ttlIndexesFieldNames.forEach(function (i) {
317 | if (doc[i] !== undefined && util.isDate(doc[i]) && Date.now() > doc[i].getTime() + self.ttlIndexes[i] * 1000) {
318 | valid = false;
319 | }
320 | });
321 | if (valid) { validDocs.push(doc); } else { expiredDocsIds.push(doc._id); }
322 | });
323 |
324 | async.eachSeries(expiredDocsIds, function (_id, cb) {
325 | self._remove({ _id: _id }, {}, function (err) {
326 | if (err) { return callback(err); }
327 | return cb();
328 | });
329 | }, function (err) {
330 | return callback(null, validDocs);
331 | });
332 | }]);
333 | };
334 |
335 |
336 | /**
337 | * Insert a new document
338 | * @param {Function} cb Optional callback, signature: err, insertedDoc
339 | *
340 | * @api private Use Datastore.insert which has the same signature
341 | */
342 | Datastore.prototype._insert = function (newDoc, cb) {
343 | var callback = cb || function () {}
344 | , preparedDoc
345 | ;
346 |
347 | try {
348 | preparedDoc = this.prepareDocumentForInsertion(newDoc)
349 | this._insertInCache(preparedDoc);
350 | } catch (e) {
351 | return callback(e);
352 | }
353 |
354 | this.persistence.persistNewState(util.isArray(preparedDoc) ? preparedDoc : [preparedDoc], function (err) {
355 | if (err) { return callback(err); }
356 | return callback(null, model.deepCopy(preparedDoc));
357 | });
358 | };
359 |
360 | /**
361 | * Create a new _id that's not already in use
362 | */
363 | Datastore.prototype.createNewId = function () {
364 | var tentativeId = customUtils.uid(16);
365 | // Try as many times as needed to get an unused _id. As explained in customUtils, the probability of this ever happening is extremely small, so this is O(1)
366 | if (this.indexes._id.getMatching(tentativeId).length > 0) {
367 | tentativeId = this.createNewId();
368 | }
369 | return tentativeId;
370 | };
371 |
372 | /**
373 | * Prepare a document (or array of documents) to be inserted in a database
374 | * Meaning adds _id and timestamps if necessary on a copy of newDoc to avoid any side effect on user input
375 | * @api private
376 | */
377 | Datastore.prototype.prepareDocumentForInsertion = function (newDoc) {
378 | var preparedDoc, self = this;
379 |
380 | if (util.isArray(newDoc)) {
381 | preparedDoc = [];
382 | newDoc.forEach(function (doc) { preparedDoc.push(self.prepareDocumentForInsertion(doc)); });
383 | } else {
384 | preparedDoc = model.deepCopy(newDoc);
385 | if (preparedDoc._id === undefined) { preparedDoc._id = this.createNewId(); }
386 | var now = new Date();
387 | if (this.timestampData && preparedDoc.createdAt === undefined) { preparedDoc.createdAt = now; }
388 | if (this.timestampData && preparedDoc.updatedAt === undefined) { preparedDoc.updatedAt = now; }
389 | model.checkObject(preparedDoc);
390 | }
391 |
392 | return preparedDoc;
393 | };
394 |
395 | /**
396 | * If newDoc is an array of documents, this will insert all documents in the cache
397 | * @api private
398 | */
399 | Datastore.prototype._insertInCache = function (preparedDoc) {
400 | if (util.isArray(preparedDoc)) {
401 | this._insertMultipleDocsInCache(preparedDoc);
402 | } else {
403 | this.addToIndexes(preparedDoc);
404 | }
405 | };
406 |
407 | /**
408 | * If one insertion fails (e.g. because of a unique constraint), roll back all previous
409 | * inserts and throws the error
410 | * @api private
411 | */
412 | Datastore.prototype._insertMultipleDocsInCache = function (preparedDocs) {
413 | var i, failingI, error;
414 |
415 | for (i = 0; i < preparedDocs.length; i += 1) {
416 | try {
417 | this.addToIndexes(preparedDocs[i]);
418 | } catch (e) {
419 | error = e;
420 | failingI = i;
421 | break;
422 | }
423 | }
424 |
425 | if (error) {
426 | for (i = 0; i < failingI; i += 1) {
427 | this.removeFromIndexes(preparedDocs[i]);
428 | }
429 |
430 | throw error;
431 | }
432 | };
433 |
434 | Datastore.prototype.insert = function () {
435 | this.executor.push({ this: this, fn: this._insert, arguments: arguments });
436 | };
437 |
438 |
439 | /**
440 | * Count all documents matching the query
441 | * @param {Object} query MongoDB-style query
442 | */
443 | Datastore.prototype.count = function(query, callback) {
444 | var cursor = new Cursor(this, query, function(err, docs, callback) {
445 | if (err) { return callback(err); }
446 | return callback(null, docs.length);
447 | });
448 |
449 | if (typeof callback === 'function') {
450 | cursor.exec(callback);
451 | } else {
452 | return cursor;
453 | }
454 | };
455 |
456 |
457 | /**
458 | * Find all documents matching the query
459 | * If no callback is passed, we return the cursor so that user can limit, skip and finally exec
460 | * @param {Object} query MongoDB-style query
461 | * @param {Object} projection MongoDB-style projection
462 | */
463 | Datastore.prototype.find = function (query, projection, callback) {
464 | switch (arguments.length) {
465 | case 1:
466 | projection = {};
467 | // callback is undefined, will return a cursor
468 | break;
469 | case 2:
470 | if (typeof projection === 'function') {
471 | callback = projection;
472 | projection = {};
473 | } // If not assume projection is an object and callback undefined
474 | break;
475 | }
476 |
477 | var cursor = new Cursor(this, query, function(err, docs, callback) {
478 | var res = [], i;
479 |
480 | if (err) { return callback(err); }
481 |
482 | for (i = 0; i < docs.length; i += 1) {
483 | res.push(model.deepCopy(docs[i]));
484 | }
485 | return callback(null, res);
486 | });
487 |
488 | cursor.projection(projection);
489 | if (typeof callback === 'function') {
490 | cursor.exec(callback);
491 | } else {
492 | return cursor;
493 | }
494 | };
495 |
496 |
497 | /**
498 | * Find one document matching the query
499 | * @param {Object} query MongoDB-style query
500 | * @param {Object} projection MongoDB-style projection
501 | */
502 | Datastore.prototype.findOne = function (query, projection, callback) {
503 | switch (arguments.length) {
504 | case 1:
505 | projection = {};
506 | // callback is undefined, will return a cursor
507 | break;
508 | case 2:
509 | if (typeof projection === 'function') {
510 | callback = projection;
511 | projection = {};
512 | } // If not assume projection is an object and callback undefined
513 | break;
514 | }
515 |
516 | var cursor = new Cursor(this, query, function(err, docs, callback) {
517 | if (err) { return callback(err); }
518 | if (docs.length === 1) {
519 | return callback(null, model.deepCopy(docs[0]));
520 | } else {
521 | return callback(null, null);
522 | }
523 | });
524 |
525 | cursor.projection(projection).limit(1);
526 | if (typeof callback === 'function') {
527 | cursor.exec(callback);
528 | } else {
529 | return cursor;
530 | }
531 | };
532 |
533 |
534 | /**
535 | * Update all docs matching query
536 | * @param {Object} query
537 | * @param {Object} updateQuery
538 | * @param {Object} options Optional options
539 | * options.multi If true, can update multiple documents (defaults to false)
540 | * options.upsert If true, document is inserted if the query doesn't match anything
541 | * options.returnUpdatedDocs Defaults to false, if true return as third argument the array of updated matched documents (even if no change actually took place)
542 | * @param {Function} cb Optional callback, signature: (err, numAffected, affectedDocuments, upsert)
543 | * If update was an upsert, upsert flag is set to true
544 | * affectedDocuments can be one of the following:
545 | * * For an upsert, the upserted document
546 | * * For an update with returnUpdatedDocs option false, null
547 | * * For an update with returnUpdatedDocs true and multi false, the updated document
548 | * * For an update with returnUpdatedDocs true and multi true, the array of updated documents
549 | *
550 | * WARNING: The API was changed between v1.7.4 and v1.8, for consistency and readability reasons. Prior and including to v1.7.4,
551 | * the callback signature was (err, numAffected, updated) where updated was the updated document in case of an upsert
552 | * or the array of updated documents for an update if the returnUpdatedDocs option was true. That meant that the type of
553 | * affectedDocuments in a non multi update depended on whether there was an upsert or not, leaving only two ways for the
554 | * user to check whether an upsert had occured: checking the type of affectedDocuments or running another find query on
555 | * the whole dataset to check its size. Both options being ugly, the breaking change was necessary.
556 | *
557 | * @api private Use Datastore.update which has the same signature
558 | */
559 | Datastore.prototype._update = function (query, updateQuery, options, cb) {
560 | var callback
561 | , self = this
562 | , numReplaced = 0
563 | , multi, upsert
564 | , i
565 | ;
566 |
567 | if (typeof options === 'function') { cb = options; options = {}; }
568 | callback = cb || function () {};
569 | multi = options.multi !== undefined ? options.multi : false;
570 | upsert = options.upsert !== undefined ? options.upsert : false;
571 |
572 | async.waterfall([
573 | function (cb) { // If upsert option is set, check whether we need to insert the doc
574 | if (!upsert) { return cb(); }
575 |
576 | // Need to use an internal function not tied to the executor to avoid deadlock
577 | var cursor = new Cursor(self, query);
578 | cursor.limit(1)._exec(function (err, docs) {
579 | if (err) { return callback(err); }
580 | if (docs.length === 1) {
581 | return cb();
582 | } else {
583 | var toBeInserted;
584 |
585 | try {
586 | model.checkObject(updateQuery);
587 | // updateQuery is a simple object with no modifier, use it as the document to insert
588 | toBeInserted = updateQuery;
589 | } catch (e) {
590 | // updateQuery contains modifiers, use the find query as the base,
591 | // strip it from all operators and update it according to updateQuery
592 | try {
593 | toBeInserted = model.modify(model.deepCopy(query, true), updateQuery);
594 | } catch (err) {
595 | return callback(err);
596 | }
597 | }
598 |
599 | return self._insert(toBeInserted, function (err, newDoc) {
600 | if (err) { return callback(err); }
601 | return callback(null, 1, newDoc, true);
602 | });
603 | }
604 | });
605 | }
606 | , function () { // Perform the update
607 | var modifiedDoc , modifications = [], createdAt;
608 |
609 | self.getCandidates(query, function (err, candidates) {
610 | if (err) { return callback(err); }
611 |
612 | // Preparing update (if an error is thrown here neither the datafile nor
613 | // the in-memory indexes are affected)
614 | try {
615 | for (i = 0; i < candidates.length; i += 1) {
616 | if (model.match(candidates[i], query) && (multi || numReplaced === 0)) {
617 | numReplaced += 1;
618 | if (self.timestampData) { createdAt = candidates[i].createdAt; }
619 | modifiedDoc = model.modify(candidates[i], updateQuery);
620 | if (self.timestampData) {
621 | modifiedDoc.createdAt = createdAt;
622 | modifiedDoc.updatedAt = new Date();
623 | }
624 | modifications.push({ oldDoc: candidates[i], newDoc: modifiedDoc });
625 | }
626 | }
627 | } catch (err) {
628 | return callback(err);
629 | }
630 |
631 | // Change the docs in memory
632 | try {
633 | self.updateIndexes(modifications);
634 | } catch (err) {
635 | return callback(err);
636 | }
637 |
638 | // Update the datafile
639 | var updatedDocs = _.pluck(modifications, 'newDoc');
640 | self.persistence.persistNewState(updatedDocs, function (err) {
641 | if (err) { return callback(err); }
642 | if (!options.returnUpdatedDocs) {
643 | return callback(null, numReplaced);
644 | } else {
645 | var updatedDocsDC = [];
646 | updatedDocs.forEach(function (doc) { updatedDocsDC.push(model.deepCopy(doc)); });
647 | if (! multi) { updatedDocsDC = updatedDocsDC[0]; }
648 | return callback(null, numReplaced, updatedDocsDC);
649 | }
650 | });
651 | });
652 | }]);
653 | };
654 |
655 | Datastore.prototype.update = function () {
656 | this.executor.push({ this: this, fn: this._update, arguments: arguments });
657 | };
658 |
659 |
660 | /**
661 | * Remove all docs matching the query
662 | * For now very naive implementation (similar to update)
663 | * @param {Object} query
664 | * @param {Object} options Optional options
665 | * options.multi If true, can update multiple documents (defaults to false)
666 | * @param {Function} cb Optional callback, signature: err, numRemoved
667 | *
668 | * @api private Use Datastore.remove which has the same signature
669 | */
670 | Datastore.prototype._remove = function (query, options, cb) {
671 | var callback
672 | , self = this, numRemoved = 0, removedDocs = [], multi
673 | ;
674 |
675 | if (typeof options === 'function') { cb = options; options = {}; }
676 | callback = cb || function () {};
677 | multi = options.multi !== undefined ? options.multi : false;
678 |
679 | this.getCandidates(query, true, function (err, candidates) {
680 | if (err) { return callback(err); }
681 |
682 | try {
683 | candidates.forEach(function (d) {
684 | if (model.match(d, query) && (multi || numRemoved === 0)) {
685 | numRemoved += 1;
686 | removedDocs.push({ $$deleted: true, _id: d._id });
687 | self.removeFromIndexes(d);
688 | }
689 | });
690 | } catch (err) { return callback(err); }
691 |
692 | self.persistence.persistNewState(removedDocs, function (err) {
693 | if (err) { return callback(err); }
694 | return callback(null, numRemoved);
695 | });
696 | });
697 | };
698 |
699 | Datastore.prototype.remove = function () {
700 | this.executor.push({ this: this, fn: this._remove, arguments: arguments });
701 | };
702 |
703 |
704 |
705 | module.exports = Datastore;
706 |
--------------------------------------------------------------------------------
/lib/model.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Handle models (i.e. docs)
3 | * Serialization/deserialization
4 | * Copying
5 | * Querying, update
6 | */
7 |
8 | var util = require('util')
9 | , _ = require('underscore')
10 | , modifierFunctions = {}
11 | , lastStepModifierFunctions = {}
12 | , comparisonFunctions = {}
13 | , logicalOperators = {}
14 | , arrayComparisonFunctions = {}
15 | ;
16 |
17 |
18 | /**
19 | * Check a key, throw an error if the key is non valid
20 | * @param {String} k key
21 | * @param {Model} v value, needed to treat the Date edge case
22 | * Non-treatable edge cases here: if part of the object if of the form { $$date: number } or { $$deleted: true }
23 | * Its serialized-then-deserialized version it will transformed into a Date object
24 | * But you really need to want it to trigger such behaviour, even when warned not to use '$' at the beginning of the field names...
25 | */
26 | function checkKey (k, v) {
27 | if (typeof k === 'number') {
28 | k = k.toString();
29 | }
30 |
31 | if (k[0] === '$' && !(k === '$$date' && typeof v === 'number') && !(k === '$$deleted' && v === true) && !(k === '$$indexCreated') && !(k === '$$indexRemoved')) {
32 | throw new Error('Field names cannot begin with the $ character');
33 | }
34 |
35 | if (k.indexOf('.') !== -1) {
36 | throw new Error('Field names cannot contain a .');
37 | }
38 | }
39 |
40 |
41 | /**
42 | * Check a DB object and throw an error if it's not valid
43 | * Works by applying the above checkKey function to all fields recursively
44 | */
45 | function checkObject (obj) {
46 | if (util.isArray(obj)) {
47 | obj.forEach(function (o) {
48 | checkObject(o);
49 | });
50 | }
51 |
52 | if (typeof obj === 'object' && obj !== null) {
53 | Object.keys(obj).forEach(function (k) {
54 | checkKey(k, obj[k]);
55 | checkObject(obj[k]);
56 | });
57 | }
58 | }
59 |
60 |
61 | /**
62 | * Serialize an object to be persisted to a one-line string
63 | * For serialization/deserialization, we use the native JSON parser and not eval or Function
64 | * That gives us less freedom but data entered in the database may come from users
65 | * so eval and the like are not safe
66 | * Accepted primitive types: Number, String, Boolean, Date, null
67 | * Accepted secondary types: Objects, Arrays
68 | */
69 | function serialize (obj) {
70 | var res;
71 |
72 | res = JSON.stringify(obj, function (k, v) {
73 | checkKey(k, v);
74 |
75 | if (v === undefined) { return undefined; }
76 | if (v === null) { return null; }
77 |
78 | // Hackish way of checking if object is Date (this way it works between execution contexts in node-webkit).
79 | // We can't use value directly because for dates it is already string in this function (date.toJSON was already called), so we use this
80 | if (typeof this[k].getTime === 'function') { return { $$date: this[k].getTime() }; }
81 |
82 | return v;
83 | });
84 |
85 | return res;
86 | }
87 |
88 |
89 | /**
90 | * From a one-line representation of an object generate by the serialize function
91 | * Return the object itself
92 | */
93 | function deserialize (rawData) {
94 | return JSON.parse(rawData, function (k, v) {
95 | if (k === '$$date') { return new Date(v); }
96 | if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null) { return v; }
97 | if (v && v.$$date) { return v.$$date; }
98 |
99 | return v;
100 | });
101 | }
102 |
103 |
104 | /**
105 | * Deep copy a DB object
106 | * The optional strictKeys flag (defaulting to false) indicates whether to copy everything or only fields
107 | * where the keys are valid, i.e. don't begin with $ and don't contain a .
108 | */
109 | function deepCopy (obj, strictKeys) {
110 | var res;
111 |
112 | if ( typeof obj === 'boolean' ||
113 | typeof obj === 'number' ||
114 | typeof obj === 'string' ||
115 | obj === null ||
116 | (util.isDate(obj)) ) {
117 | return obj;
118 | }
119 |
120 | if (util.isArray(obj)) {
121 | res = [];
122 | obj.forEach(function (o) { res.push(deepCopy(o, strictKeys)); });
123 | return res;
124 | }
125 |
126 | if (typeof obj === 'object') {
127 | res = {};
128 | Object.keys(obj).forEach(function (k) {
129 | if (!strictKeys || (k[0] !== '$' && k.indexOf('.') === -1)) {
130 | res[k] = deepCopy(obj[k], strictKeys);
131 | }
132 | });
133 | return res;
134 | }
135 |
136 | return undefined; // For now everything else is undefined. We should probably throw an error instead
137 | }
138 |
139 |
140 | /**
141 | * Tells if an object is a primitive type or a "real" object
142 | * Arrays are considered primitive
143 | */
144 | function isPrimitiveType (obj) {
145 | return ( typeof obj === 'boolean' ||
146 | typeof obj === 'number' ||
147 | typeof obj === 'string' ||
148 | obj === null ||
149 | util.isDate(obj) ||
150 | util.isArray(obj));
151 | }
152 |
153 |
154 | /**
155 | * Utility functions for comparing things
156 | * Assumes type checking was already done (a and b already have the same type)
157 | * compareNSB works for numbers, strings and booleans
158 | */
159 | function compareNSB (a, b) {
160 | if (a < b) { return -1; }
161 | if (a > b) { return 1; }
162 | return 0;
163 | }
164 |
165 | function compareArrays (a, b) {
166 | var i, comp;
167 |
168 | for (i = 0; i < Math.min(a.length, b.length); i += 1) {
169 | comp = compareThings(a[i], b[i]);
170 |
171 | if (comp !== 0) { return comp; }
172 | }
173 |
174 | // Common section was identical, longest one wins
175 | return compareNSB(a.length, b.length);
176 | }
177 |
178 |
179 | /**
180 | * Compare { things U undefined }
181 | * Things are defined as any native types (string, number, boolean, null, date) and objects
182 | * We need to compare with undefined as it will be used in indexes
183 | * In the case of objects and arrays, we deep-compare
184 | * If two objects dont have the same type, the (arbitrary) type hierarchy is: undefined, null, number, strings, boolean, dates, arrays, objects
185 | * Return -1 if a < b, 1 if a > b and 0 if a = b (note that equality here is NOT the same as defined in areThingsEqual!)
186 | *
187 | * @param {Function} _compareStrings String comparing function, returning -1, 0 or 1, overriding default string comparison (useful for languages with accented letters)
188 | */
189 | function compareThings (a, b, _compareStrings) {
190 | var aKeys, bKeys, comp, i
191 | , compareStrings = _compareStrings || compareNSB;
192 |
193 | // undefined
194 | if (a === undefined) { return b === undefined ? 0 : -1; }
195 | if (b === undefined) { return a === undefined ? 0 : 1; }
196 |
197 | // null
198 | if (a === null) { return b === null ? 0 : -1; }
199 | if (b === null) { return a === null ? 0 : 1; }
200 |
201 | // Numbers
202 | if (typeof a === 'number') { return typeof b === 'number' ? compareNSB(a, b) : -1; }
203 | if (typeof b === 'number') { return typeof a === 'number' ? compareNSB(a, b) : 1; }
204 |
205 | // Strings
206 | if (typeof a === 'string') { return typeof b === 'string' ? compareStrings(a, b) : -1; }
207 | if (typeof b === 'string') { return typeof a === 'string' ? compareStrings(a, b) : 1; }
208 |
209 | // Booleans
210 | if (typeof a === 'boolean') { return typeof b === 'boolean' ? compareNSB(a, b) : -1; }
211 | if (typeof b === 'boolean') { return typeof a === 'boolean' ? compareNSB(a, b) : 1; }
212 |
213 | // Dates
214 | if (util.isDate(a)) { return util.isDate(b) ? compareNSB(a.getTime(), b.getTime()) : -1; }
215 | if (util.isDate(b)) { return util.isDate(a) ? compareNSB(a.getTime(), b.getTime()) : 1; }
216 |
217 | // Arrays (first element is most significant and so on)
218 | if (util.isArray(a)) { return util.isArray(b) ? compareArrays(a, b) : -1; }
219 | if (util.isArray(b)) { return util.isArray(a) ? compareArrays(a, b) : 1; }
220 |
221 | // Objects
222 | aKeys = Object.keys(a).sort();
223 | bKeys = Object.keys(b).sort();
224 |
225 | for (i = 0; i < Math.min(aKeys.length, bKeys.length); i += 1) {
226 | comp = compareThings(a[aKeys[i]], b[bKeys[i]]);
227 |
228 | if (comp !== 0) { return comp; }
229 | }
230 |
231 | return compareNSB(aKeys.length, bKeys.length);
232 | }
233 |
234 |
235 |
236 | // ==============================================================
237 | // Updating documents
238 | // ==============================================================
239 |
240 | /**
241 | * The signature of modifier functions is as follows
242 | * Their structure is always the same: recursively follow the dot notation while creating
243 | * the nested documents if needed, then apply the "last step modifier"
244 | * @param {Object} obj The model to modify
245 | * @param {String} field Can contain dots, in that case that means we will set a subfield recursively
246 | * @param {Model} value
247 | */
248 |
249 | /**
250 | * Set a field to a new value
251 | */
252 | lastStepModifierFunctions.$set = function (obj, field, value) {
253 | obj[field] = value;
254 | };
255 |
256 |
257 | /**
258 | * Unset a field
259 | */
260 | lastStepModifierFunctions.$unset = function (obj, field, value) {
261 | delete obj[field];
262 | };
263 |
264 |
265 | /**
266 | * Push an element to the end of an array field
267 | * Optional modifier $each instead of value to push several values
268 | * Optional modifier $slice to slice the resulting array, see https://docs.mongodb.org/manual/reference/operator/update/slice/
269 | * Différeence with MongoDB: if $slice is specified and not $each, we act as if value is an empty array
270 | */
271 | lastStepModifierFunctions.$push = function (obj, field, value) {
272 | // Create the array if it doesn't exist
273 | if (!obj.hasOwnProperty(field)) { obj[field] = []; }
274 |
275 | if (!util.isArray(obj[field])) { throw new Error("Can't $push an element on non-array values"); }
276 |
277 | if (value !== null && typeof value === 'object' && value.$slice && value.$each === undefined) {
278 | value.$each = [];
279 | }
280 |
281 | if (value !== null && typeof value === 'object' && value.$each) {
282 | if (Object.keys(value).length >= 3 || (Object.keys(value).length === 2 && value.$slice === undefined)) { throw new Error("Can only use $slice in cunjunction with $each when $push to array"); }
283 | if (!util.isArray(value.$each)) { throw new Error("$each requires an array value"); }
284 |
285 | value.$each.forEach(function (v) {
286 | obj[field].push(v);
287 | });
288 |
289 | if (value.$slice === undefined || typeof value.$slice !== 'number') { return; }
290 |
291 | if (value.$slice === 0) {
292 | obj[field] = [];
293 | } else {
294 | var start, end, n = obj[field].length;
295 | if (value.$slice < 0) {
296 | start = Math.max(0, n + value.$slice);
297 | end = n;
298 | } else if (value.$slice > 0) {
299 | start = 0;
300 | end = Math.min(n, value.$slice);
301 | }
302 | obj[field] = obj[field].slice(start, end);
303 | }
304 | } else {
305 | obj[field].push(value);
306 | }
307 | };
308 |
309 |
310 | /**
311 | * Add an element to an array field only if it is not already in it
312 | * No modification if the element is already in the array
313 | * Note that it doesn't check whether the original array contains duplicates
314 | */
315 | lastStepModifierFunctions.$addToSet = function (obj, field, value) {
316 | var addToSet = true;
317 |
318 | // Create the array if it doesn't exist
319 | if (!obj.hasOwnProperty(field)) { obj[field] = []; }
320 |
321 | if (!util.isArray(obj[field])) { throw new Error("Can't $addToSet an element on non-array values"); }
322 |
323 | if (value !== null && typeof value === 'object' && value.$each) {
324 | if (Object.keys(value).length > 1) { throw new Error("Can't use another field in conjunction with $each"); }
325 | if (!util.isArray(value.$each)) { throw new Error("$each requires an array value"); }
326 |
327 | value.$each.forEach(function (v) {
328 | lastStepModifierFunctions.$addToSet(obj, field, v);
329 | });
330 | } else {
331 | obj[field].forEach(function (v) {
332 | if (compareThings(v, value) === 0) { addToSet = false; }
333 | });
334 | if (addToSet) { obj[field].push(value); }
335 | }
336 | };
337 |
338 |
339 | /**
340 | * Remove the first or last element of an array
341 | */
342 | lastStepModifierFunctions.$pop = function (obj, field, value) {
343 | if (!util.isArray(obj[field])) { throw new Error("Can't $pop an element from non-array values"); }
344 | if (typeof value !== 'number') { throw new Error(value + " isn't an integer, can't use it with $pop"); }
345 | if (value === 0) { return; }
346 |
347 | if (value > 0) {
348 | obj[field] = obj[field].slice(0, obj[field].length - 1);
349 | } else {
350 | obj[field] = obj[field].slice(1);
351 | }
352 | };
353 |
354 |
355 | /**
356 | * Removes all instances of a value from an existing array
357 | */
358 | lastStepModifierFunctions.$pull = function (obj, field, value) {
359 | var arr, i;
360 |
361 | if (!util.isArray(obj[field])) { throw new Error("Can't $pull an element from non-array values"); }
362 |
363 | arr = obj[field];
364 | for (i = arr.length - 1; i >= 0; i -= 1) {
365 | if (match(arr[i], value)) {
366 | arr.splice(i, 1);
367 | }
368 | }
369 | };
370 |
371 |
372 | /**
373 | * Increment a numeric field's value
374 | */
375 | lastStepModifierFunctions.$inc = function (obj, field, value) {
376 | if (typeof value !== 'number') { throw new Error(value + " must be a number"); }
377 |
378 | if (typeof obj[field] !== 'number') {
379 | if (!_.has(obj, field)) {
380 | obj[field] = value;
381 | } else {
382 | throw new Error("Don't use the $inc modifier on non-number fields");
383 | }
384 | } else {
385 | obj[field] += value;
386 | }
387 | };
388 |
389 | /**
390 | * Updates the value of the field, only if specified field is greater than the current value of the field
391 | */
392 | lastStepModifierFunctions.$max = function (obj, field, value) {
393 | if (typeof obj[field] === 'undefined') {
394 | obj[field] = value;
395 | } else if (value > obj[field]) {
396 | obj[field] = value;
397 | }
398 | };
399 |
400 | /**
401 | * Updates the value of the field, only if specified field is smaller than the current value of the field
402 | */
403 | lastStepModifierFunctions.$min = function (obj, field, value) {
404 | if (typeof obj[field] === 'undefined') {
405 | obj[field] = value;
406 | } else if (value < obj[field]) {
407 | obj[field] = value;
408 | }
409 | };
410 |
411 | // Given its name, create the complete modifier function
412 | function createModifierFunction (modifier) {
413 | return function (obj, field, value) {
414 | var fieldParts = typeof field === 'string' ? field.split('.') : field;
415 |
416 | if (fieldParts.length === 1) {
417 | lastStepModifierFunctions[modifier](obj, field, value);
418 | } else {
419 | if (obj[fieldParts[0]] === undefined) {
420 | if (modifier === '$unset') { return; } // Bad looking specific fix, needs to be generalized modifiers that behave like $unset are implemented
421 | obj[fieldParts[0]] = {};
422 | }
423 | modifierFunctions[modifier](obj[fieldParts[0]], fieldParts.slice(1), value);
424 | }
425 | };
426 | }
427 |
428 | // Actually create all modifier functions
429 | Object.keys(lastStepModifierFunctions).forEach(function (modifier) {
430 | modifierFunctions[modifier] = createModifierFunction(modifier);
431 | });
432 |
433 |
434 | /**
435 | * Modify a DB object according to an update query
436 | */
437 | function modify (obj, updateQuery) {
438 | var keys = Object.keys(updateQuery)
439 | , firstChars = _.map(keys, function (item) { return item[0]; })
440 | , dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; })
441 | , newDoc, modifiers
442 | ;
443 |
444 | if (keys.indexOf('_id') !== -1 && updateQuery._id !== obj._id) { throw new Error("You cannot change a document's _id"); }
445 |
446 | if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) {
447 | throw new Error("You cannot mix modifiers and normal fields");
448 | }
449 |
450 | if (dollarFirstChars.length === 0) {
451 | // Simply replace the object with the update query contents
452 | newDoc = deepCopy(updateQuery);
453 | newDoc._id = obj._id;
454 | } else {
455 | // Apply modifiers
456 | modifiers = _.uniq(keys);
457 | newDoc = deepCopy(obj);
458 | modifiers.forEach(function (m) {
459 | var keys;
460 |
461 | if (!modifierFunctions[m]) { throw new Error("Unknown modifier " + m); }
462 |
463 | // Can't rely on Object.keys throwing on non objects since ES6
464 | // Not 100% satisfying as non objects can be interpreted as objects but no false negatives so we can live with it
465 | if (typeof updateQuery[m] !== 'object') {
466 | throw new Error("Modifier " + m + "'s argument must be an object");
467 | }
468 |
469 | keys = Object.keys(updateQuery[m]);
470 | keys.forEach(function (k) {
471 | modifierFunctions[m](newDoc, k, updateQuery[m][k]);
472 | });
473 | });
474 | }
475 |
476 | // Check result is valid and return it
477 | checkObject(newDoc);
478 |
479 | if (obj._id !== newDoc._id) { throw new Error("You can't change a document's _id"); }
480 | return newDoc;
481 | };
482 |
483 |
484 | // ==============================================================
485 | // Finding documents
486 | // ==============================================================
487 |
488 | /**
489 | * Get a value from object with dot notation
490 | * @param {Object} obj
491 | * @param {String} field
492 | */
493 | function getDotValue (obj, field) {
494 | var fieldParts = typeof field === 'string' ? field.split('.') : field
495 | , i, objs;
496 |
497 | if (!obj) { return undefined; } // field cannot be empty so that means we should return undefined so that nothing can match
498 |
499 | if (fieldParts.length === 0) { return obj; }
500 |
501 | if (fieldParts.length === 1) { return obj[fieldParts[0]]; }
502 |
503 | if (util.isArray(obj[fieldParts[0]])) {
504 | // If the next field is an integer, return only this item of the array
505 | i = parseInt(fieldParts[1], 10);
506 | if (typeof i === 'number' && !isNaN(i)) {
507 | return getDotValue(obj[fieldParts[0]][i], fieldParts.slice(2))
508 | }
509 |
510 | // Return the array of values
511 | objs = new Array();
512 | for (i = 0; i < obj[fieldParts[0]].length; i += 1) {
513 | objs.push(getDotValue(obj[fieldParts[0]][i], fieldParts.slice(1)));
514 | }
515 | return objs;
516 | } else {
517 | return getDotValue(obj[fieldParts[0]], fieldParts.slice(1));
518 | }
519 | }
520 |
521 |
522 | /**
523 | * Check whether 'things' are equal
524 | * Things are defined as any native types (string, number, boolean, null, date) and objects
525 | * In the case of object, we check deep equality
526 | * Returns true if they are, false otherwise
527 | */
528 | function areThingsEqual (a, b) {
529 | var aKeys , bKeys , i;
530 |
531 | // Strings, booleans, numbers, null
532 | if (a === null || typeof a === 'string' || typeof a === 'boolean' || typeof a === 'number' ||
533 | b === null || typeof b === 'string' || typeof b === 'boolean' || typeof b === 'number') { return a === b; }
534 |
535 | // Dates
536 | if (util.isDate(a) || util.isDate(b)) { return util.isDate(a) && util.isDate(b) && a.getTime() === b.getTime(); }
537 |
538 | // Arrays (no match since arrays are used as a $in)
539 | // undefined (no match since they mean field doesn't exist and can't be serialized)
540 | if ((!(util.isArray(a) && util.isArray(b)) && (util.isArray(a) || util.isArray(b))) || a === undefined || b === undefined) { return false; }
541 |
542 | // General objects (check for deep equality)
543 | // a and b should be objects at this point
544 | try {
545 | aKeys = Object.keys(a);
546 | bKeys = Object.keys(b);
547 | } catch (e) {
548 | return false;
549 | }
550 |
551 | if (aKeys.length !== bKeys.length) { return false; }
552 | for (i = 0; i < aKeys.length; i += 1) {
553 | if (bKeys.indexOf(aKeys[i]) === -1) { return false; }
554 | if (!areThingsEqual(a[aKeys[i]], b[aKeys[i]])) { return false; }
555 | }
556 | return true;
557 | }
558 |
559 |
560 | /**
561 | * Check that two values are comparable
562 | */
563 | function areComparable (a, b) {
564 | if (typeof a !== 'string' && typeof a !== 'number' && !util.isDate(a) &&
565 | typeof b !== 'string' && typeof b !== 'number' && !util.isDate(b)) {
566 | return false;
567 | }
568 |
569 | if (typeof a !== typeof b) { return false; }
570 |
571 | return true;
572 | }
573 |
574 |
575 | /**
576 | * Arithmetic and comparison operators
577 | * @param {Native value} a Value in the object
578 | * @param {Native value} b Value in the query
579 | */
580 | comparisonFunctions.$lt = function (a, b) {
581 | return areComparable(a, b) && a < b;
582 | };
583 |
584 | comparisonFunctions.$lte = function (a, b) {
585 | return areComparable(a, b) && a <= b;
586 | };
587 |
588 | comparisonFunctions.$gt = function (a, b) {
589 | return areComparable(a, b) && a > b;
590 | };
591 |
592 | comparisonFunctions.$gte = function (a, b) {
593 | return areComparable(a, b) && a >= b;
594 | };
595 |
596 | comparisonFunctions.$ne = function (a, b) {
597 | if (a === undefined) { return true; }
598 | return !areThingsEqual(a, b);
599 | };
600 |
601 | comparisonFunctions.$in = function (a, b) {
602 | var i;
603 |
604 | if (!util.isArray(b)) { throw new Error("$in operator called with a non-array"); }
605 |
606 | for (i = 0; i < b.length; i += 1) {
607 | if (areThingsEqual(a, b[i])) { return true; }
608 | }
609 |
610 | return false;
611 | };
612 |
613 | comparisonFunctions.$nin = function (a, b) {
614 | if (!util.isArray(b)) { throw new Error("$nin operator called with a non-array"); }
615 |
616 | return !comparisonFunctions.$in(a, b);
617 | };
618 |
619 | comparisonFunctions.$regex = function (a, b) {
620 | if (!util.isRegExp(b)) { throw new Error("$regex operator called with non regular expression"); }
621 |
622 | if (typeof a !== 'string') {
623 | return false
624 | } else {
625 | return b.test(a);
626 | }
627 | };
628 |
629 | comparisonFunctions.$exists = function (value, exists) {
630 | if (exists || exists === '') { // This will be true for all values of exists except false, null, undefined and 0
631 | exists = true; // That's strange behaviour (we should only use true/false) but that's the way Mongo does it...
632 | } else {
633 | exists = false;
634 | }
635 |
636 | if (value === undefined) {
637 | return !exists
638 | } else {
639 | return exists;
640 | }
641 | };
642 |
643 | // Specific to arrays
644 | comparisonFunctions.$size = function (obj, value) {
645 | if (!util.isArray(obj)) { return false; }
646 | if (value % 1 !== 0) { throw new Error("$size operator called without an integer"); }
647 |
648 | return (obj.length == value);
649 | };
650 | comparisonFunctions.$elemMatch = function (obj, value) {
651 | if (!util.isArray(obj)) { return false; }
652 | var i = obj.length;
653 | var result = false; // Initialize result
654 | while (i--) {
655 | if (match(obj[i], value)) { // If match for array element, return true
656 | result = true;
657 | break;
658 | }
659 | }
660 | return result;
661 | };
662 | arrayComparisonFunctions.$size = true;
663 | arrayComparisonFunctions.$elemMatch = true;
664 |
665 |
666 | /**
667 | * Match any of the subqueries
668 | * @param {Model} obj
669 | * @param {Array of Queries} query
670 | */
671 | logicalOperators.$or = function (obj, query) {
672 | var i;
673 |
674 | if (!util.isArray(query)) { throw new Error("$or operator used without an array"); }
675 |
676 | for (i = 0; i < query.length; i += 1) {
677 | if (match(obj, query[i])) { return true; }
678 | }
679 |
680 | return false;
681 | };
682 |
683 |
684 | /**
685 | * Match all of the subqueries
686 | * @param {Model} obj
687 | * @param {Array of Queries} query
688 | */
689 | logicalOperators.$and = function (obj, query) {
690 | var i;
691 |
692 | if (!util.isArray(query)) { throw new Error("$and operator used without an array"); }
693 |
694 | for (i = 0; i < query.length; i += 1) {
695 | if (!match(obj, query[i])) { return false; }
696 | }
697 |
698 | return true;
699 | };
700 |
701 |
702 | /**
703 | * Inverted match of the query
704 | * @param {Model} obj
705 | * @param {Query} query
706 | */
707 | logicalOperators.$not = function (obj, query) {
708 | return !match(obj, query);
709 | };
710 |
711 |
712 | /**
713 | * Use a function to match
714 | * @param {Model} obj
715 | * @param {Query} query
716 | */
717 | logicalOperators.$where = function (obj, fn) {
718 | var result;
719 |
720 | if (!_.isFunction(fn)) { throw new Error("$where operator used without a function"); }
721 |
722 | result = fn.call(obj);
723 | if (!_.isBoolean(result)) { throw new Error("$where function must return boolean"); }
724 |
725 | return result;
726 | };
727 |
728 |
729 | /**
730 | * Tell if a given document matches a query
731 | * @param {Object} obj Document to check
732 | * @param {Object} query
733 | */
734 | function match (obj, query) {
735 | var queryKeys, queryKey, queryValue, i;
736 |
737 | // Primitive query against a primitive type
738 | // This is a bit of a hack since we construct an object with an arbitrary key only to dereference it later
739 | // But I don't have time for a cleaner implementation now
740 | if (isPrimitiveType(obj) || isPrimitiveType(query)) {
741 | return matchQueryPart({ needAKey: obj }, 'needAKey', query);
742 | }
743 |
744 | // Normal query
745 | queryKeys = Object.keys(query);
746 | for (i = 0; i < queryKeys.length; i += 1) {
747 | queryKey = queryKeys[i];
748 | queryValue = query[queryKey];
749 |
750 | if (queryKey[0] === '$') {
751 | if (!logicalOperators[queryKey]) { throw new Error("Unknown logical operator " + queryKey); }
752 | if (!logicalOperators[queryKey](obj, queryValue)) { return false; }
753 | } else {
754 | if (!matchQueryPart(obj, queryKey, queryValue)) { return false; }
755 | }
756 | }
757 |
758 | return true;
759 | };
760 |
761 |
762 | /**
763 | * Match an object against a specific { key: value } part of a query
764 | * if the treatObjAsValue flag is set, don't try to match every part separately, but the array as a whole
765 | */
766 | function matchQueryPart (obj, queryKey, queryValue, treatObjAsValue) {
767 | var objValue = getDotValue(obj, queryKey)
768 | , i, keys, firstChars, dollarFirstChars;
769 |
770 | // Check if the value is an array if we don't force a treatment as value
771 | if (util.isArray(objValue) && !treatObjAsValue) {
772 | // If the queryValue is an array, try to perform an exact match
773 | if (util.isArray(queryValue)) {
774 | return matchQueryPart(obj, queryKey, queryValue, true);
775 | }
776 |
777 | // Check if we are using an array-specific comparison function
778 | if (queryValue !== null && typeof queryValue === 'object' && !util.isRegExp(queryValue)) {
779 | keys = Object.keys(queryValue);
780 | for (i = 0; i < keys.length; i += 1) {
781 | if (arrayComparisonFunctions[keys[i]]) { return matchQueryPart(obj, queryKey, queryValue, true); }
782 | }
783 | }
784 |
785 | // If not, treat it as an array of { obj, query } where there needs to be at least one match
786 | for (i = 0; i < objValue.length; i += 1) {
787 | if (matchQueryPart({ k: objValue[i] }, 'k', queryValue)) { return true; } // k here could be any string
788 | }
789 | return false;
790 | }
791 |
792 | // queryValue is an actual object. Determine whether it contains comparison operators
793 | // or only normal fields. Mixed objects are not allowed
794 | if (queryValue !== null && typeof queryValue === 'object' && !util.isRegExp(queryValue) && !util.isArray(queryValue)) {
795 | keys = Object.keys(queryValue);
796 | firstChars = _.map(keys, function (item) { return item[0]; });
797 | dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; });
798 |
799 | if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) {
800 | throw new Error("You cannot mix operators and normal fields");
801 | }
802 |
803 | // queryValue is an object of this form: { $comparisonOperator1: value1, ... }
804 | if (dollarFirstChars.length > 0) {
805 | for (i = 0; i < keys.length; i += 1) {
806 | if (!comparisonFunctions[keys[i]]) { throw new Error("Unknown comparison function " + keys[i]); }
807 |
808 | if (!comparisonFunctions[keys[i]](objValue, queryValue[keys[i]])) { return false; }
809 | }
810 | return true;
811 | }
812 | }
813 |
814 | // Using regular expressions with basic querying
815 | if (util.isRegExp(queryValue)) { return comparisonFunctions.$regex(objValue, queryValue); }
816 |
817 | // queryValue is either a native value or a normal object
818 | // Basic matching is possible
819 | if (!areThingsEqual(objValue, queryValue)) { return false; }
820 |
821 | return true;
822 | }
823 |
824 |
825 | // Interface
826 | module.exports.serialize = serialize;
827 | module.exports.deserialize = deserialize;
828 | module.exports.deepCopy = deepCopy;
829 | module.exports.checkObject = checkObject;
830 | module.exports.isPrimitiveType = isPrimitiveType;
831 | module.exports.modify = modify;
832 | module.exports.getDotValue = getDotValue;
833 | module.exports.match = match;
834 | module.exports.areThingsEqual = areThingsEqual;
835 | module.exports.compareThings = compareThings;
836 |
--------------------------------------------------------------------------------
/test/indexes.test.js:
--------------------------------------------------------------------------------
1 | var Index = require('../lib/indexes')
2 | , customUtils = require('../lib/customUtils')
3 | , should = require('chai').should()
4 | , assert = require('chai').assert
5 | , _ = require('underscore')
6 | , async = require('async')
7 | , model = require('../lib/model')
8 | ;
9 |
10 | describe('Indexes', function () {
11 |
12 | describe('Insertion', function () {
13 |
14 | it('Can insert pointers to documents in the index correctly when they have the field', function () {
15 | var idx = new Index({ fieldName: 'tf' })
16 | , doc1 = { a: 5, tf: 'hello' }
17 | , doc2 = { a: 8, tf: 'world' }
18 | , doc3 = { a: 2, tf: 'bloup' }
19 | ;
20 |
21 | idx.insert(doc1);
22 | idx.insert(doc2);
23 | idx.insert(doc3);
24 |
25 | // The underlying BST now has 3 nodes which contain the docs where it's expected
26 | idx.tree.getNumberOfKeys().should.equal(3);
27 | assert.deepEqual(idx.tree.search('hello'), [{ a: 5, tf: 'hello' }]);
28 | assert.deepEqual(idx.tree.search('world'), [{ a: 8, tf: 'world' }]);
29 | assert.deepEqual(idx.tree.search('bloup'), [{ a: 2, tf: 'bloup' }]);
30 |
31 | // The nodes contain pointers to the actual documents
32 | idx.tree.search('world')[0].should.equal(doc2);
33 | idx.tree.search('bloup')[0].a = 42;
34 | doc3.a.should.equal(42);
35 | });
36 |
37 | it('Inserting twice for the same fieldName in a unique index will result in an error thrown', function () {
38 | var idx = new Index({ fieldName: 'tf', unique: true })
39 | , doc1 = { a: 5, tf: 'hello' }
40 | ;
41 |
42 | idx.insert(doc1);
43 | idx.tree.getNumberOfKeys().should.equal(1);
44 | (function () { idx.insert(doc1); }).should.throw();
45 | });
46 |
47 | it('Inserting twice for a fieldName the docs dont have with a unique index results in an error thrown', function () {
48 | var idx = new Index({ fieldName: 'nope', unique: true })
49 | , doc1 = { a: 5, tf: 'hello' }
50 | , doc2 = { a: 5, tf: 'world' }
51 | ;
52 |
53 | idx.insert(doc1);
54 | idx.tree.getNumberOfKeys().should.equal(1);
55 | (function () { idx.insert(doc2); }).should.throw();
56 | });
57 |
58 | it('Inserting twice for a fieldName the docs dont have with a unique and sparse index will not throw, since the docs will be non indexed', function () {
59 | var idx = new Index({ fieldName: 'nope', unique: true, sparse: true })
60 | , doc1 = { a: 5, tf: 'hello' }
61 | , doc2 = { a: 5, tf: 'world' }
62 | ;
63 |
64 | idx.insert(doc1);
65 | idx.insert(doc2);
66 | idx.tree.getNumberOfKeys().should.equal(0); // Docs are not indexed
67 | });
68 |
69 | it('Works with dot notation', function () {
70 | var idx = new Index({ fieldName: 'tf.nested' })
71 | , doc1 = { a: 5, tf: { nested: 'hello' } }
72 | , doc2 = { a: 8, tf: { nested: 'world', additional: true } }
73 | , doc3 = { a: 2, tf: { nested: 'bloup', age: 42 } }
74 | ;
75 |
76 | idx.insert(doc1);
77 | idx.insert(doc2);
78 | idx.insert(doc3);
79 |
80 | // The underlying BST now has 3 nodes which contain the docs where it's expected
81 | idx.tree.getNumberOfKeys().should.equal(3);
82 | assert.deepEqual(idx.tree.search('hello'), [doc1]);
83 | assert.deepEqual(idx.tree.search('world'), [doc2]);
84 | assert.deepEqual(idx.tree.search('bloup'), [doc3]);
85 |
86 | // The nodes contain pointers to the actual documents
87 | idx.tree.search('bloup')[0].a = 42;
88 | doc3.a.should.equal(42);
89 | });
90 |
91 | it('Can insert an array of documents', function () {
92 | var idx = new Index({ fieldName: 'tf' })
93 | , doc1 = { a: 5, tf: 'hello' }
94 | , doc2 = { a: 8, tf: 'world' }
95 | , doc3 = { a: 2, tf: 'bloup' }
96 | ;
97 |
98 | idx.insert([doc1, doc2, doc3]);
99 | idx.tree.getNumberOfKeys().should.equal(3);
100 | assert.deepEqual(idx.tree.search('hello'), [doc1]);
101 | assert.deepEqual(idx.tree.search('world'), [doc2]);
102 | assert.deepEqual(idx.tree.search('bloup'), [doc3]);
103 | });
104 |
105 | it('When inserting an array of elements, if an error is thrown all inserts need to be rolled back', function () {
106 | var idx = new Index({ fieldName: 'tf', unique: true })
107 | , doc1 = { a: 5, tf: 'hello' }
108 | , doc2 = { a: 8, tf: 'world' }
109 | , doc2b = { a: 84, tf: 'world' }
110 | , doc3 = { a: 2, tf: 'bloup' }
111 | ;
112 |
113 | try {
114 | idx.insert([doc1, doc2, doc2b, doc3]);
115 | } catch (e) {
116 | e.errorType.should.equal('uniqueViolated');
117 | }
118 | idx.tree.getNumberOfKeys().should.equal(0);
119 | assert.deepEqual(idx.tree.search('hello'), []);
120 | assert.deepEqual(idx.tree.search('world'), []);
121 | assert.deepEqual(idx.tree.search('bloup'), []);
122 | });
123 |
124 |
125 | describe('Array fields', function () {
126 |
127 | it('Inserts one entry per array element in the index', function () {
128 | var obj = { tf: ['aa', 'bb'], really: 'yeah' }
129 | , obj2 = { tf: 'normal', yes: 'indeed' }
130 | , idx = new Index({ fieldName: 'tf' })
131 | ;
132 |
133 | idx.insert(obj);
134 | idx.getAll().length.should.equal(2);
135 | idx.getAll()[0].should.equal(obj);
136 | idx.getAll()[1].should.equal(obj);
137 |
138 | idx.insert(obj2);
139 | idx.getAll().length.should.equal(3);
140 | });
141 |
142 | it('Inserts one entry per array element in the index, type-checked', function () {
143 | var obj = { tf: ['42', 42, new Date(42), 42], really: 'yeah' }
144 | , idx = new Index({ fieldName: 'tf' })
145 | ;
146 |
147 | idx.insert(obj);
148 | idx.getAll().length.should.equal(3);
149 | idx.getAll()[0].should.equal(obj);
150 | idx.getAll()[1].should.equal(obj);
151 | idx.getAll()[2].should.equal(obj);
152 | });
153 |
154 | it('Inserts one entry per unique array element in the index, the unique constraint only holds across documents', function () {
155 | var obj = { tf: ['aa', 'aa'], really: 'yeah' }
156 | , obj2 = { tf: ['cc', 'yy', 'cc'], yes: 'indeed' }
157 | , idx = new Index({ fieldName: 'tf', unique: true })
158 | ;
159 |
160 | idx.insert(obj);
161 | idx.getAll().length.should.equal(1);
162 | idx.getAll()[0].should.equal(obj);
163 |
164 | idx.insert(obj2);
165 | idx.getAll().length.should.equal(3);
166 | });
167 |
168 | it('The unique constraint holds across documents', function () {
169 | var obj = { tf: ['aa', 'aa'], really: 'yeah' }
170 | , obj2 = { tf: ['cc', 'aa', 'cc'], yes: 'indeed' }
171 | , idx = new Index({ fieldName: 'tf', unique: true })
172 | ;
173 |
174 | idx.insert(obj);
175 | idx.getAll().length.should.equal(1);
176 | idx.getAll()[0].should.equal(obj);
177 |
178 | (function () { idx.insert(obj2); }).should.throw();
179 | });
180 |
181 | it('When removing a document, remove it from the index at all unique array elements', function () {
182 | var obj = { tf: ['aa', 'aa'], really: 'yeah' }
183 | , obj2 = { tf: ['cc', 'aa', 'cc'], yes: 'indeed' }
184 | , idx = new Index({ fieldName: 'tf' })
185 | ;
186 |
187 | idx.insert(obj);
188 | idx.insert(obj2);
189 | idx.getMatching('aa').length.should.equal(2);
190 | idx.getMatching('aa').indexOf(obj).should.not.equal(-1);
191 | idx.getMatching('aa').indexOf(obj2).should.not.equal(-1);
192 | idx.getMatching('cc').length.should.equal(1);
193 |
194 | idx.remove(obj2);
195 | idx.getMatching('aa').length.should.equal(1);
196 | idx.getMatching('aa').indexOf(obj).should.not.equal(-1);
197 | idx.getMatching('aa').indexOf(obj2).should.equal(-1);
198 | idx.getMatching('cc').length.should.equal(0);
199 | });
200 |
201 | it('If a unique constraint is violated when inserting an array key, roll back all inserts before the key', function () {
202 | var obj = { tf: ['aa', 'bb'], really: 'yeah' }
203 | , obj2 = { tf: ['cc', 'dd', 'aa', 'ee'], yes: 'indeed' }
204 | , idx = new Index({ fieldName: 'tf', unique: true })
205 | ;
206 |
207 | idx.insert(obj);
208 | idx.getAll().length.should.equal(2);
209 | idx.getMatching('aa').length.should.equal(1);
210 | idx.getMatching('bb').length.should.equal(1);
211 | idx.getMatching('cc').length.should.equal(0);
212 | idx.getMatching('dd').length.should.equal(0);
213 | idx.getMatching('ee').length.should.equal(0);
214 |
215 | (function () { idx.insert(obj2); }).should.throw();
216 | idx.getAll().length.should.equal(2);
217 | idx.getMatching('aa').length.should.equal(1);
218 | idx.getMatching('bb').length.should.equal(1);
219 | idx.getMatching('cc').length.should.equal(0);
220 | idx.getMatching('dd').length.should.equal(0);
221 | idx.getMatching('ee').length.should.equal(0);
222 | });
223 |
224 | }); // ==== End of 'Array fields' ==== //
225 |
226 | }); // ==== End of 'Insertion' ==== //
227 |
228 |
229 | describe('Removal', function () {
230 |
231 | it('Can remove pointers from the index, even when multiple documents have the same key', function () {
232 | var idx = new Index({ fieldName: 'tf' })
233 | , doc1 = { a: 5, tf: 'hello' }
234 | , doc2 = { a: 8, tf: 'world' }
235 | , doc3 = { a: 2, tf: 'bloup' }
236 | , doc4 = { a: 23, tf: 'world' }
237 | ;
238 |
239 | idx.insert(doc1);
240 | idx.insert(doc2);
241 | idx.insert(doc3);
242 | idx.insert(doc4);
243 | idx.tree.getNumberOfKeys().should.equal(3);
244 |
245 | idx.remove(doc1);
246 | idx.tree.getNumberOfKeys().should.equal(2);
247 | idx.tree.search('hello').length.should.equal(0);
248 |
249 | idx.remove(doc2);
250 | idx.tree.getNumberOfKeys().should.equal(2);
251 | idx.tree.search('world').length.should.equal(1);
252 | idx.tree.search('world')[0].should.equal(doc4);
253 | });
254 |
255 | it('If we have a sparse index, removing a non indexed doc has no effect', function () {
256 | var idx = new Index({ fieldName: 'nope', sparse: true })
257 | , doc1 = { a: 5, tf: 'hello' }
258 | , doc2 = { a: 5, tf: 'world' }
259 | ;
260 |
261 | idx.insert(doc1);
262 | idx.insert(doc2);
263 | idx.tree.getNumberOfKeys().should.equal(0);
264 |
265 | idx.remove(doc1);
266 | idx.tree.getNumberOfKeys().should.equal(0);
267 | });
268 |
269 | it('Works with dot notation', function () {
270 | var idx = new Index({ fieldName: 'tf.nested' })
271 | , doc1 = { a: 5, tf: { nested: 'hello' } }
272 | , doc2 = { a: 8, tf: { nested: 'world', additional: true } }
273 | , doc3 = { a: 2, tf: { nested: 'bloup', age: 42 } }
274 | , doc4 = { a: 2, tf: { nested: 'world', fruits: ['apple', 'carrot'] } }
275 | ;
276 |
277 | idx.insert(doc1);
278 | idx.insert(doc2);
279 | idx.insert(doc3);
280 | idx.insert(doc4);
281 | idx.tree.getNumberOfKeys().should.equal(3);
282 |
283 | idx.remove(doc1);
284 | idx.tree.getNumberOfKeys().should.equal(2);
285 | idx.tree.search('hello').length.should.equal(0);
286 |
287 | idx.remove(doc2);
288 | idx.tree.getNumberOfKeys().should.equal(2);
289 | idx.tree.search('world').length.should.equal(1);
290 | idx.tree.search('world')[0].should.equal(doc4);
291 | });
292 |
293 | it('Can remove an array of documents', function () {
294 | var idx = new Index({ fieldName: 'tf' })
295 | , doc1 = { a: 5, tf: 'hello' }
296 | , doc2 = { a: 8, tf: 'world' }
297 | , doc3 = { a: 2, tf: 'bloup' }
298 | ;
299 |
300 | idx.insert([doc1, doc2, doc3]);
301 | idx.tree.getNumberOfKeys().should.equal(3);
302 | idx.remove([doc1, doc3]);
303 | idx.tree.getNumberOfKeys().should.equal(1);
304 | assert.deepEqual(idx.tree.search('hello'), []);
305 | assert.deepEqual(idx.tree.search('world'), [doc2]);
306 | assert.deepEqual(idx.tree.search('bloup'), []);
307 | });
308 |
309 | }); // ==== End of 'Removal' ==== //
310 |
311 |
312 | describe('Update', function () {
313 |
314 | it('Can update a document whose key did or didnt change', function () {
315 | var idx = new Index({ fieldName: 'tf' })
316 | , doc1 = { a: 5, tf: 'hello' }
317 | , doc2 = { a: 8, tf: 'world' }
318 | , doc3 = { a: 2, tf: 'bloup' }
319 | , doc4 = { a: 23, tf: 'world' }
320 | , doc5 = { a: 1, tf: 'changed' }
321 | ;
322 |
323 | idx.insert(doc1);
324 | idx.insert(doc2);
325 | idx.insert(doc3);
326 | idx.tree.getNumberOfKeys().should.equal(3);
327 | assert.deepEqual(idx.tree.search('world'), [doc2]);
328 |
329 | idx.update(doc2, doc4);
330 | idx.tree.getNumberOfKeys().should.equal(3);
331 | assert.deepEqual(idx.tree.search('world'), [doc4]);
332 |
333 | idx.update(doc1, doc5);
334 | idx.tree.getNumberOfKeys().should.equal(3);
335 | assert.deepEqual(idx.tree.search('hello'), []);
336 | assert.deepEqual(idx.tree.search('changed'), [doc5]);
337 | });
338 |
339 | it('If a simple update violates a unique constraint, changes are rolled back and an error thrown', function () {
340 | var idx = new Index({ fieldName: 'tf', unique: true })
341 | , doc1 = { a: 5, tf: 'hello' }
342 | , doc2 = { a: 8, tf: 'world' }
343 | , doc3 = { a: 2, tf: 'bloup' }
344 | , bad = { a: 23, tf: 'world' }
345 | ;
346 |
347 | idx.insert(doc1);
348 | idx.insert(doc2);
349 | idx.insert(doc3);
350 |
351 | idx.tree.getNumberOfKeys().should.equal(3);
352 | assert.deepEqual(idx.tree.search('hello'), [doc1]);
353 | assert.deepEqual(idx.tree.search('world'), [doc2]);
354 | assert.deepEqual(idx.tree.search('bloup'), [doc3]);
355 |
356 | try {
357 | idx.update(doc3, bad);
358 | } catch (e) {
359 | e.errorType.should.equal('uniqueViolated');
360 | }
361 |
362 | // No change
363 | idx.tree.getNumberOfKeys().should.equal(3);
364 | assert.deepEqual(idx.tree.search('hello'), [doc1]);
365 | assert.deepEqual(idx.tree.search('world'), [doc2]);
366 | assert.deepEqual(idx.tree.search('bloup'), [doc3]);
367 | });
368 |
369 | it('Can update an array of documents', function () {
370 | var idx = new Index({ fieldName: 'tf' })
371 | , doc1 = { a: 5, tf: 'hello' }
372 | , doc2 = { a: 8, tf: 'world' }
373 | , doc3 = { a: 2, tf: 'bloup' }
374 | , doc1b = { a: 23, tf: 'world' }
375 | , doc2b = { a: 1, tf: 'changed' }
376 | , doc3b = { a: 44, tf: 'bloup' }
377 | ;
378 |
379 | idx.insert(doc1);
380 | idx.insert(doc2);
381 | idx.insert(doc3);
382 | idx.tree.getNumberOfKeys().should.equal(3);
383 |
384 | idx.update([{ oldDoc: doc1, newDoc: doc1b }, { oldDoc: doc2, newDoc: doc2b }, { oldDoc: doc3, newDoc: doc3b }]);
385 |
386 | idx.tree.getNumberOfKeys().should.equal(3);
387 | idx.getMatching('world').length.should.equal(1);
388 | idx.getMatching('world')[0].should.equal(doc1b);
389 | idx.getMatching('changed').length.should.equal(1);
390 | idx.getMatching('changed')[0].should.equal(doc2b);
391 | idx.getMatching('bloup').length.should.equal(1);
392 | idx.getMatching('bloup')[0].should.equal(doc3b);
393 | });
394 |
395 | it('If a unique constraint is violated during an array-update, all changes are rolled back and an error thrown', function () {
396 | var idx = new Index({ fieldName: 'tf', unique: true })
397 | , doc0 = { a: 432, tf: 'notthistoo' }
398 | , doc1 = { a: 5, tf: 'hello' }
399 | , doc2 = { a: 8, tf: 'world' }
400 | , doc3 = { a: 2, tf: 'bloup' }
401 | , doc1b = { a: 23, tf: 'changed' }
402 | , doc2b = { a: 1, tf: 'changed' } // Will violate the constraint (first try)
403 | , doc2c = { a: 1, tf: 'notthistoo' } // Will violate the constraint (second try)
404 | , doc3b = { a: 44, tf: 'alsochanged' }
405 | ;
406 |
407 | idx.insert(doc1);
408 | idx.insert(doc2);
409 | idx.insert(doc3);
410 | idx.tree.getNumberOfKeys().should.equal(3);
411 |
412 | try {
413 | idx.update([{ oldDoc: doc1, newDoc: doc1b }, { oldDoc: doc2, newDoc: doc2b }, { oldDoc: doc3, newDoc: doc3b }]);
414 | } catch (e) {
415 | e.errorType.should.equal('uniqueViolated');
416 | }
417 |
418 | idx.tree.getNumberOfKeys().should.equal(3);
419 | idx.getMatching('hello').length.should.equal(1);
420 | idx.getMatching('hello')[0].should.equal(doc1);
421 | idx.getMatching('world').length.should.equal(1);
422 | idx.getMatching('world')[0].should.equal(doc2);
423 | idx.getMatching('bloup').length.should.equal(1);
424 | idx.getMatching('bloup')[0].should.equal(doc3);
425 |
426 | try {
427 | idx.update([{ oldDoc: doc1, newDoc: doc1b }, { oldDoc: doc2, newDoc: doc2b }, { oldDoc: doc3, newDoc: doc3b }]);
428 | } catch (e) {
429 | e.errorType.should.equal('uniqueViolated');
430 | }
431 |
432 | idx.tree.getNumberOfKeys().should.equal(3);
433 | idx.getMatching('hello').length.should.equal(1);
434 | idx.getMatching('hello')[0].should.equal(doc1);
435 | idx.getMatching('world').length.should.equal(1);
436 | idx.getMatching('world')[0].should.equal(doc2);
437 | idx.getMatching('bloup').length.should.equal(1);
438 | idx.getMatching('bloup')[0].should.equal(doc3);
439 | });
440 |
441 | it('If an update doesnt change a document, the unique constraint is not violated', function () {
442 | var idx = new Index({ fieldName: 'tf', unique: true })
443 | , doc1 = { a: 5, tf: 'hello' }
444 | , doc2 = { a: 8, tf: 'world' }
445 | , doc3 = { a: 2, tf: 'bloup' }
446 | , noChange = { a: 8, tf: 'world' }
447 | ;
448 |
449 | idx.insert(doc1);
450 | idx.insert(doc2);
451 | idx.insert(doc3);
452 | idx.tree.getNumberOfKeys().should.equal(3);
453 | assert.deepEqual(idx.tree.search('world'), [doc2]);
454 |
455 | idx.update(doc2, noChange); // No error thrown
456 | idx.tree.getNumberOfKeys().should.equal(3);
457 | assert.deepEqual(idx.tree.search('world'), [noChange]);
458 | });
459 |
460 | it('Can revert simple and batch updates', function () {
461 | var idx = new Index({ fieldName: 'tf' })
462 | , doc1 = { a: 5, tf: 'hello' }
463 | , doc2 = { a: 8, tf: 'world' }
464 | , doc3 = { a: 2, tf: 'bloup' }
465 | , doc1b = { a: 23, tf: 'world' }
466 | , doc2b = { a: 1, tf: 'changed' }
467 | , doc3b = { a: 44, tf: 'bloup' }
468 | , batchUpdate = [{ oldDoc: doc1, newDoc: doc1b }, { oldDoc: doc2, newDoc: doc2b }, { oldDoc: doc3, newDoc: doc3b }]
469 | ;
470 |
471 | idx.insert(doc1);
472 | idx.insert(doc2);
473 | idx.insert(doc3);
474 | idx.tree.getNumberOfKeys().should.equal(3);
475 |
476 | idx.update(batchUpdate);
477 |
478 | idx.tree.getNumberOfKeys().should.equal(3);
479 | idx.getMatching('world').length.should.equal(1);
480 | idx.getMatching('world')[0].should.equal(doc1b);
481 | idx.getMatching('changed').length.should.equal(1);
482 | idx.getMatching('changed')[0].should.equal(doc2b);
483 | idx.getMatching('bloup').length.should.equal(1);
484 | idx.getMatching('bloup')[0].should.equal(doc3b);
485 |
486 | idx.revertUpdate(batchUpdate);
487 |
488 | idx.tree.getNumberOfKeys().should.equal(3);
489 | idx.getMatching('hello').length.should.equal(1);
490 | idx.getMatching('hello')[0].should.equal(doc1);
491 | idx.getMatching('world').length.should.equal(1);
492 | idx.getMatching('world')[0].should.equal(doc2);
493 | idx.getMatching('bloup').length.should.equal(1);
494 | idx.getMatching('bloup')[0].should.equal(doc3);
495 |
496 | // Now a simple update
497 | idx.update(doc2, doc2b);
498 |
499 | idx.tree.getNumberOfKeys().should.equal(3);
500 | idx.getMatching('hello').length.should.equal(1);
501 | idx.getMatching('hello')[0].should.equal(doc1);
502 | idx.getMatching('changed').length.should.equal(1);
503 | idx.getMatching('changed')[0].should.equal(doc2b);
504 | idx.getMatching('bloup').length.should.equal(1);
505 | idx.getMatching('bloup')[0].should.equal(doc3);
506 |
507 | idx.revertUpdate(doc2, doc2b);
508 |
509 | idx.tree.getNumberOfKeys().should.equal(3);
510 | idx.getMatching('hello').length.should.equal(1);
511 | idx.getMatching('hello')[0].should.equal(doc1);
512 | idx.getMatching('world').length.should.equal(1);
513 | idx.getMatching('world')[0].should.equal(doc2);
514 | idx.getMatching('bloup').length.should.equal(1);
515 | idx.getMatching('bloup')[0].should.equal(doc3);
516 | });
517 |
518 | }); // ==== End of 'Update' ==== //
519 |
520 |
521 | describe('Get matching documents', function () {
522 |
523 | it('Get all documents where fieldName is equal to the given value, or an empty array if no match', function () {
524 | var idx = new Index({ fieldName: 'tf' })
525 | , doc1 = { a: 5, tf: 'hello' }
526 | , doc2 = { a: 8, tf: 'world' }
527 | , doc3 = { a: 2, tf: 'bloup' }
528 | , doc4 = { a: 23, tf: 'world' }
529 | ;
530 |
531 | idx.insert(doc1);
532 | idx.insert(doc2);
533 | idx.insert(doc3);
534 | idx.insert(doc4);
535 |
536 | assert.deepEqual(idx.getMatching('bloup'), [doc3]);
537 | assert.deepEqual(idx.getMatching('world'), [doc2, doc4]);
538 | assert.deepEqual(idx.getMatching('nope'), []);
539 | });
540 |
541 | it('Can get all documents for a given key in a unique index', function () {
542 | var idx = new Index({ fieldName: 'tf', unique: true })
543 | , doc1 = { a: 5, tf: 'hello' }
544 | , doc2 = { a: 8, tf: 'world' }
545 | , doc3 = { a: 2, tf: 'bloup' }
546 | ;
547 |
548 | idx.insert(doc1);
549 | idx.insert(doc2);
550 | idx.insert(doc3);
551 |
552 | assert.deepEqual(idx.getMatching('bloup'), [doc3]);
553 | assert.deepEqual(idx.getMatching('world'), [doc2]);
554 | assert.deepEqual(idx.getMatching('nope'), []);
555 | });
556 |
557 | it('Can get all documents for which a field is undefined', function () {
558 | var idx = new Index({ fieldName: 'tf' })
559 | , doc1 = { a: 5, tf: 'hello' }
560 | , doc2 = { a: 2, nottf: 'bloup' }
561 | , doc3 = { a: 8, tf: 'world' }
562 | , doc4 = { a: 7, nottf: 'yes' }
563 | ;
564 |
565 | idx.insert(doc1);
566 | idx.insert(doc2);
567 | idx.insert(doc3);
568 |
569 | assert.deepEqual(idx.getMatching('bloup'), []);
570 | assert.deepEqual(idx.getMatching('hello'), [doc1]);
571 | assert.deepEqual(idx.getMatching('world'), [doc3]);
572 | assert.deepEqual(idx.getMatching('yes'), []);
573 | assert.deepEqual(idx.getMatching(undefined), [doc2]);
574 |
575 | idx.insert(doc4);
576 |
577 | assert.deepEqual(idx.getMatching('bloup'), []);
578 | assert.deepEqual(idx.getMatching('hello'), [doc1]);
579 | assert.deepEqual(idx.getMatching('world'), [doc3]);
580 | assert.deepEqual(idx.getMatching('yes'), []);
581 | assert.deepEqual(idx.getMatching(undefined), [doc2, doc4]);
582 | });
583 |
584 | it('Can get all documents for which a field is null', function () {
585 | var idx = new Index({ fieldName: 'tf' })
586 | , doc1 = { a: 5, tf: 'hello' }
587 | , doc2 = { a: 2, tf: null }
588 | , doc3 = { a: 8, tf: 'world' }
589 | , doc4 = { a: 7, tf: null }
590 | ;
591 |
592 | idx.insert(doc1);
593 | idx.insert(doc2);
594 | idx.insert(doc3);
595 |
596 | assert.deepEqual(idx.getMatching('bloup'), []);
597 | assert.deepEqual(idx.getMatching('hello'), [doc1]);
598 | assert.deepEqual(idx.getMatching('world'), [doc3]);
599 | assert.deepEqual(idx.getMatching('yes'), []);
600 | assert.deepEqual(idx.getMatching(null), [doc2]);
601 |
602 | idx.insert(doc4);
603 |
604 | assert.deepEqual(idx.getMatching('bloup'), []);
605 | assert.deepEqual(idx.getMatching('hello'), [doc1]);
606 | assert.deepEqual(idx.getMatching('world'), [doc3]);
607 | assert.deepEqual(idx.getMatching('yes'), []);
608 | assert.deepEqual(idx.getMatching(null), [doc2, doc4]);
609 | });
610 |
611 | it('Can get all documents for a given key in a sparse index, but not unindexed docs (= field undefined)', function () {
612 | var idx = new Index({ fieldName: 'tf', sparse: true })
613 | , doc1 = { a: 5, tf: 'hello' }
614 | , doc2 = { a: 2, nottf: 'bloup' }
615 | , doc3 = { a: 8, tf: 'world' }
616 | , doc4 = { a: 7, nottf: 'yes' }
617 | ;
618 |
619 | idx.insert(doc1);
620 | idx.insert(doc2);
621 | idx.insert(doc3);
622 | idx.insert(doc4);
623 |
624 | assert.deepEqual(idx.getMatching('bloup'), []);
625 | assert.deepEqual(idx.getMatching('hello'), [doc1]);
626 | assert.deepEqual(idx.getMatching('world'), [doc3]);
627 | assert.deepEqual(idx.getMatching('yes'), []);
628 | assert.deepEqual(idx.getMatching(undefined), []);
629 | });
630 |
631 | it('Can get all documents whose key is in an array of keys', function () {
632 | // For this test only we have to use objects with _ids as the array version of getMatching
633 | // relies on the _id property being set, otherwise we have to use a quadratic algorithm
634 | // or a fingerprinting algorithm, both solutions too complicated and slow given that live nedb
635 | // indexes documents with _id always set
636 | var idx = new Index({ fieldName: 'tf' })
637 | , doc1 = { a: 5, tf: 'hello', _id: '1' }
638 | , doc2 = { a: 2, tf: 'bloup', _id: '2' }
639 | , doc3 = { a: 8, tf: 'world', _id: '3' }
640 | , doc4 = { a: 7, tf: 'yes', _id: '4' }
641 | , doc5 = { a: 7, tf: 'yes', _id: '5' }
642 | ;
643 |
644 | idx.insert(doc1);
645 | idx.insert(doc2);
646 | idx.insert(doc3);
647 | idx.insert(doc4);
648 | idx.insert(doc5);
649 |
650 | assert.deepEqual(idx.getMatching([]), []);
651 | assert.deepEqual(idx.getMatching(['bloup']), [doc2]);
652 | assert.deepEqual(idx.getMatching(['bloup', 'yes']), [doc2, doc4, doc5]);
653 | assert.deepEqual(idx.getMatching(['hello', 'no']), [doc1]);
654 | assert.deepEqual(idx.getMatching(['nope', 'no']), []);
655 | });
656 |
657 | it('Can get all documents whose key is between certain bounds', function () {
658 | var idx = new Index({ fieldName: 'a' })
659 | , doc1 = { a: 5, tf: 'hello' }
660 | , doc2 = { a: 2, tf: 'bloup' }
661 | , doc3 = { a: 8, tf: 'world' }
662 | , doc4 = { a: 7, tf: 'yes' }
663 | , doc5 = { a: 10, tf: 'yes' }
664 | ;
665 |
666 | idx.insert(doc1);
667 | idx.insert(doc2);
668 | idx.insert(doc3);
669 | idx.insert(doc4);
670 | idx.insert(doc5);
671 |
672 | assert.deepEqual(idx.getBetweenBounds({ $lt: 10, $gte: 5 }), [ doc1, doc4, doc3 ]);
673 | assert.deepEqual(idx.getBetweenBounds({ $lte: 8 }), [ doc2, doc1, doc4, doc3 ]);
674 | assert.deepEqual(idx.getBetweenBounds({ $gt: 7 }), [ doc3, doc5 ]);
675 | });
676 |
677 | }); // ==== End of 'Get matching documents' ==== //
678 |
679 |
680 | describe('Resetting', function () {
681 |
682 | it('Can reset an index without any new data, the index will be empty afterwards', function () {
683 | var idx = new Index({ fieldName: 'tf' })
684 | , doc1 = { a: 5, tf: 'hello' }
685 | , doc2 = { a: 8, tf: 'world' }
686 | , doc3 = { a: 2, tf: 'bloup' }
687 | ;
688 |
689 | idx.insert(doc1);
690 | idx.insert(doc2);
691 | idx.insert(doc3);
692 |
693 | idx.tree.getNumberOfKeys().should.equal(3);
694 | idx.getMatching('hello').length.should.equal(1);
695 | idx.getMatching('world').length.should.equal(1);
696 | idx.getMatching('bloup').length.should.equal(1);
697 |
698 | idx.reset();
699 | idx.tree.getNumberOfKeys().should.equal(0);
700 | idx.getMatching('hello').length.should.equal(0);
701 | idx.getMatching('world').length.should.equal(0);
702 | idx.getMatching('bloup').length.should.equal(0);
703 | });
704 |
705 | it('Can reset an index and initialize it with one document', function () {
706 | var idx = new Index({ fieldName: 'tf' })
707 | , doc1 = { a: 5, tf: 'hello' }
708 | , doc2 = { a: 8, tf: 'world' }
709 | , doc3 = { a: 2, tf: 'bloup' }
710 | , newDoc = { a: 555, tf: 'new' }
711 | ;
712 |
713 | idx.insert(doc1);
714 | idx.insert(doc2);
715 | idx.insert(doc3);
716 |
717 | idx.tree.getNumberOfKeys().should.equal(3);
718 | idx.getMatching('hello').length.should.equal(1);
719 | idx.getMatching('world').length.should.equal(1);
720 | idx.getMatching('bloup').length.should.equal(1);
721 |
722 | idx.reset(newDoc);
723 | idx.tree.getNumberOfKeys().should.equal(1);
724 | idx.getMatching('hello').length.should.equal(0);
725 | idx.getMatching('world').length.should.equal(0);
726 | idx.getMatching('bloup').length.should.equal(0);
727 | idx.getMatching('new')[0].a.should.equal(555);
728 | });
729 |
730 | it('Can reset an index and initialize it with an array of documents', function () {
731 | var idx = new Index({ fieldName: 'tf' })
732 | , doc1 = { a: 5, tf: 'hello' }
733 | , doc2 = { a: 8, tf: 'world' }
734 | , doc3 = { a: 2, tf: 'bloup' }
735 | , newDocs = [{ a: 555, tf: 'new' }, { a: 666, tf: 'again' }]
736 | ;
737 |
738 | idx.insert(doc1);
739 | idx.insert(doc2);
740 | idx.insert(doc3);
741 |
742 | idx.tree.getNumberOfKeys().should.equal(3);
743 | idx.getMatching('hello').length.should.equal(1);
744 | idx.getMatching('world').length.should.equal(1);
745 | idx.getMatching('bloup').length.should.equal(1);
746 |
747 | idx.reset(newDocs);
748 | idx.tree.getNumberOfKeys().should.equal(2);
749 | idx.getMatching('hello').length.should.equal(0);
750 | idx.getMatching('world').length.should.equal(0);
751 | idx.getMatching('bloup').length.should.equal(0);
752 | idx.getMatching('new')[0].a.should.equal(555);
753 | idx.getMatching('again')[0].a.should.equal(666);
754 | });
755 |
756 | }); // ==== End of 'Resetting' ==== //
757 |
758 | it('Get all elements in the index', function () {
759 | var idx = new Index({ fieldName: 'a' })
760 | , doc1 = { a: 5, tf: 'hello' }
761 | , doc2 = { a: 8, tf: 'world' }
762 | , doc3 = { a: 2, tf: 'bloup' }
763 | ;
764 |
765 | idx.insert(doc1);
766 | idx.insert(doc2);
767 | idx.insert(doc3);
768 |
769 | assert.deepEqual(idx.getAll(), [{ a: 2, tf: 'bloup' }, { a: 5, tf: 'hello' }, { a: 8, tf: 'world' }]);
770 | });
771 |
772 |
773 | });
774 |
--------------------------------------------------------------------------------