├── .gitignore ├── LICENSE ├── README.md ├── benchmarks ├── commonUtilities.js ├── ensureIndex.js ├── find.js ├── findOne.js ├── findWithIn.js ├── insert.js ├── loadDatabase.js ├── remove.js └── update.js ├── bower.json ├── browser-version ├── browser-specific │ └── lib │ │ ├── customUtils.js │ │ └── storage.js ├── build.js ├── out │ ├── nedb.js │ └── nedb.min.js ├── package.json └── test │ ├── async.js │ ├── chai.js │ ├── index.html │ ├── jquery.min.js │ ├── localforage.js │ ├── mocha.css │ ├── mocha.js │ ├── nedb-browser.js │ ├── playground.html │ ├── testLoad.html │ ├── testLoad.js │ ├── testPersistence.html │ ├── testPersistence.js │ ├── testPersistence2.html │ ├── testPersistence2.js │ └── underscore.min.js ├── index.js ├── lib ├── cursor.js ├── customUtils.js ├── datastore.js ├── executor.js ├── indexes.js ├── model.js ├── persistence.js └── storage.js ├── package.json ├── test ├── cursor.test.js ├── customUtil.test.js ├── db.test.js ├── executor.test.js ├── indexes.test.js ├── mocha.opts ├── model.test.js └── persistence.test.js └── test_lac ├── loadAndCrash.test.js ├── openFds.test.js ├── openFdsLaunch.sh ├── openFdsTestFile └── openFdsTestFile2 /.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 -------------------------------------------------------------------------------- /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/commonUtilities.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Functions that are used in several benchmark tests 3 | */ 4 | 5 | var customUtils = require('../lib/customUtils') 6 | , fs = require('fs') 7 | , path = require('path') 8 | , Datastore = require('../lib/datastore') 9 | , Persistence = require('../lib/persistence') 10 | , executeAsap // process.nextTick or setImmediate depending on your Node version 11 | ; 12 | 13 | try { 14 | executeAsap = setImmediate; 15 | } catch (e) { 16 | executeAsap = process.nextTick; 17 | } 18 | 19 | 20 | /** 21 | * Configure the benchmark 22 | */ 23 | module.exports.getConfiguration = function (benchDb) { 24 | var d, n 25 | , program = require('commander') 26 | ; 27 | 28 | program 29 | .option('-n --number [number]', 'Size of the collection to test on', parseInt) 30 | .option('-i --with-index', 'Use an index') 31 | .option('-m --in-memory', 'Test with an in-memory only store') 32 | .parse(process.argv); 33 | 34 | n = program.number || 10000; 35 | 36 | console.log("----------------------------"); 37 | console.log("Test with " + n + " documents"); 38 | console.log(program.withIndex ? "Use an index" : "Don't use an index"); 39 | console.log(program.inMemory ? "Use an in-memory datastore" : "Use a persistent datastore"); 40 | console.log("----------------------------"); 41 | 42 | d = new Datastore({ filename: benchDb 43 | , inMemoryOnly: program.inMemory 44 | }); 45 | 46 | return { n: n, d: d, program: program }; 47 | }; 48 | 49 | 50 | /** 51 | * Ensure the workspace exists and the db datafile is empty 52 | */ 53 | module.exports.prepareDb = function (filename, cb) { 54 | Persistence.ensureDirectoryExists(path.dirname(filename), function () { 55 | fs.exists(filename, function (exists) { 56 | if (exists) { 57 | fs.unlink(filename, cb); 58 | } else { return cb(); } 59 | }); 60 | }); 61 | }; 62 | 63 | 64 | /** 65 | * Return an array with the numbers from 0 to n-1, in a random order 66 | * Uses Fisher Yates algorithm 67 | * Useful to get fair tests 68 | */ 69 | function getRandomArray (n) { 70 | var res = [] 71 | , i, j, temp 72 | ; 73 | 74 | for (i = 0; i < n; i += 1) { res[i] = i; } 75 | 76 | for (i = n - 1; 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Playground for NeDB 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('./lib/datastore'); 2 | 3 | module.exports = Datastore; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /lib/indexes.js: -------------------------------------------------------------------------------- 1 | var BinarySearchTree = require('binary-search-tree').AVLTree 2 | , model = require('./model') 3 | , _ = require('underscore') 4 | , util = require('util') 5 | ; 6 | 7 | /** 8 | * Two indexed pointers are equal iif they point to the same place 9 | */ 10 | function checkValueEquality (a, b) { 11 | return a === b; 12 | } 13 | 14 | /** 15 | * Type-aware projection 16 | */ 17 | function projectForUnique (elt) { 18 | if (elt === null) { return '$null'; } 19 | if (typeof elt === 'string') { return '$string' + elt; } 20 | if (typeof elt === 'boolean') { return '$boolean' + elt; } 21 | if (typeof elt === 'number') { return '$number' + elt; } 22 | if (util.isArray(elt)) { return '$date' + elt.getTime(); } 23 | 24 | return elt; // Arrays and objects, will check for pointer equality 25 | } 26 | 27 | 28 | /** 29 | * Create a new index 30 | * All methods on an index guarantee that either the whole operation was successful and the index changed 31 | * or the operation was unsuccessful and an error is thrown while the index is unchanged 32 | * @param {String} options.fieldName On which field should the index apply (can use dot notation to index on sub fields) 33 | * @param {Boolean} options.unique Optional, enforce a unique constraint (default: false) 34 | * @param {Boolean} options.sparse Optional, allow a sparse index (we can have documents for which fieldName is undefined) (default: false) 35 | */ 36 | function Index (options) { 37 | this.fieldName = options.fieldName; 38 | this.unique = options.unique || false; 39 | this.sparse = options.sparse || false; 40 | 41 | this.treeOptions = { unique: this.unique, compareKeys: model.compareThings, checkValueEquality: checkValueEquality }; 42 | 43 | this.reset(); // No data in the beginning 44 | } 45 | 46 | 47 | /** 48 | * Reset an index 49 | * @param {Document or Array of documents} newData Optional, data to initialize the index with 50 | * If an error is thrown during insertion, the index is not modified 51 | */ 52 | Index.prototype.reset = function (newData) { 53 | this.tree = new BinarySearchTree(this.treeOptions); 54 | 55 | if (newData) { this.insert(newData); } 56 | }; 57 | 58 | 59 | /** 60 | * Insert a new document in the index 61 | * If an array is passed, we insert all its elements (if one insertion fails the index is not modified) 62 | * O(log(n)) 63 | */ 64 | Index.prototype.insert = function (doc) { 65 | var key, self = this 66 | , keys, i, failingI, error 67 | ; 68 | 69 | if (util.isArray(doc)) { this.insertMultipleDocs(doc); return; } 70 | 71 | key = model.getDotValue(doc, this.fieldName); 72 | 73 | // We don't index documents that don't contain the field if the index is sparse 74 | if (key === undefined && this.sparse) { return; } 75 | 76 | if (!util.isArray(key)) { 77 | this.tree.insert(key, doc); 78 | } else { 79 | // If an insert fails due to a unique constraint, roll back all inserts before it 80 | keys = _.uniq(key, projectForUnique); 81 | 82 | for (i = 0; i < keys.length; i += 1) { 83 | try { 84 | this.tree.insert(keys[i], doc); 85 | } catch (e) { 86 | error = e; 87 | failingI = i; 88 | break; 89 | } 90 | } 91 | 92 | if (error) { 93 | for (i = 0; i < failingI; i += 1) { 94 | this.tree.delete(keys[i], doc); 95 | } 96 | 97 | throw error; 98 | } 99 | } 100 | }; 101 | 102 | 103 | /** 104 | * Insert an array of documents in the index 105 | * If a constraint is violated, the changes should be rolled back and an error thrown 106 | * 107 | * @API private 108 | */ 109 | Index.prototype.insertMultipleDocs = function (docs) { 110 | var i, error, failingI; 111 | 112 | for (i = 0; i < docs.length; i += 1) { 113 | try { 114 | this.insert(docs[i]); 115 | } catch (e) { 116 | error = e; 117 | failingI = i; 118 | break; 119 | } 120 | } 121 | 122 | if (error) { 123 | for (i = 0; i < failingI; i += 1) { 124 | this.remove(docs[i]); 125 | } 126 | 127 | throw error; 128 | } 129 | }; 130 | 131 | 132 | /** 133 | * Remove a document from the index 134 | * If an array is passed, we remove all its elements 135 | * The remove operation is safe with regards to the 'unique' constraint 136 | * O(log(n)) 137 | */ 138 | Index.prototype.remove = function (doc) { 139 | var key, self = this; 140 | 141 | if (util.isArray(doc)) { doc.forEach(function (d) { self.remove(d); }); return; } 142 | 143 | key = model.getDotValue(doc, this.fieldName); 144 | 145 | if (key === undefined && this.sparse) { return; } 146 | 147 | if (!util.isArray(key)) { 148 | this.tree.delete(key, doc); 149 | } else { 150 | _.uniq(key, projectForUnique).forEach(function (_key) { 151 | self.tree.delete(_key, doc); 152 | }); 153 | } 154 | }; 155 | 156 | 157 | /** 158 | * Update a document in the index 159 | * If a constraint is violated, changes are rolled back and an error thrown 160 | * Naive implementation, still in O(log(n)) 161 | */ 162 | Index.prototype.update = function (oldDoc, newDoc) { 163 | if (util.isArray(oldDoc)) { this.updateMultipleDocs(oldDoc); return; } 164 | 165 | this.remove(oldDoc); 166 | 167 | try { 168 | this.insert(newDoc); 169 | } catch (e) { 170 | this.insert(oldDoc); 171 | throw e; 172 | } 173 | }; 174 | 175 | 176 | /** 177 | * Update multiple documents in the index 178 | * If a constraint is violated, the changes need to be rolled back 179 | * and an error thrown 180 | * @param {Array of oldDoc, newDoc pairs} pairs 181 | * 182 | * @API private 183 | */ 184 | Index.prototype.updateMultipleDocs = function (pairs) { 185 | var i, failingI, error; 186 | 187 | for (i = 0; i < pairs.length; i += 1) { 188 | this.remove(pairs[i].oldDoc); 189 | } 190 | 191 | for (i = 0; i < pairs.length; i += 1) { 192 | try { 193 | this.insert(pairs[i].newDoc); 194 | } catch (e) { 195 | error = e; 196 | failingI = i; 197 | break; 198 | } 199 | } 200 | 201 | // If an error was raised, roll back changes in the inverse order 202 | if (error) { 203 | for (i = 0; i < failingI; i += 1) { 204 | this.remove(pairs[i].newDoc); 205 | } 206 | 207 | for (i = 0; i < pairs.length; i += 1) { 208 | this.insert(pairs[i].oldDoc); 209 | } 210 | 211 | throw error; 212 | } 213 | }; 214 | 215 | 216 | /** 217 | * Revert an update 218 | */ 219 | Index.prototype.revertUpdate = function (oldDoc, newDoc) { 220 | var revert = []; 221 | 222 | if (!util.isArray(oldDoc)) { 223 | this.update(newDoc, oldDoc); 224 | } else { 225 | oldDoc.forEach(function (pair) { 226 | revert.push({ oldDoc: pair.newDoc, newDoc: pair.oldDoc }); 227 | }); 228 | this.update(revert); 229 | } 230 | }; 231 | 232 | 233 | /** 234 | * Get all documents in index whose key match value (if it is a Thing) or one of the elements of value (if it is an array of Things) 235 | * @param {Thing} value Value to match the key against 236 | * @return {Array of documents} 237 | */ 238 | Index.prototype.getMatching = function (value) { 239 | var self = this; 240 | 241 | if (!util.isArray(value)) { 242 | return self.tree.search(value); 243 | } else { 244 | var _res = {}, res = []; 245 | 246 | value.forEach(function (v) { 247 | self.getMatching(v).forEach(function (doc) { 248 | _res[doc._id] = doc; 249 | }); 250 | }); 251 | 252 | Object.keys(_res).forEach(function (_id) { 253 | res.push(_res[_id]); 254 | }); 255 | 256 | return res; 257 | } 258 | }; 259 | 260 | 261 | /** 262 | * Get all documents in index whose key is between bounds are they are defined by query 263 | * Documents are sorted by key 264 | * @param {Query} query 265 | * @return {Array of documents} 266 | */ 267 | Index.prototype.getBetweenBounds = function (query) { 268 | return this.tree.betweenBounds(query); 269 | }; 270 | 271 | 272 | /** 273 | * Get all elements in the index 274 | * @return {Array of documents} 275 | */ 276 | Index.prototype.getAll = function () { 277 | var res = []; 278 | 279 | this.tree.executeOnEveryNode(function (node) { 280 | var i; 281 | 282 | for (i = 0; i < node.data.length; i += 1) { 283 | res.push(node.data[i]); 284 | } 285 | }); 286 | 287 | return res; 288 | }; 289 | 290 | 291 | 292 | 293 | // Interface 294 | module.exports = Index; 295 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 >> 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test_lac/openFdsLaunch.sh: -------------------------------------------------------------------------------- 1 | ulimit -n 128 2 | node ./test_lac/openFds.test.js 3 | -------------------------------------------------------------------------------- /test_lac/openFdsTestFile: -------------------------------------------------------------------------------- 1 | Random stuff 2 | -------------------------------------------------------------------------------- /test_lac/openFdsTestFile2: -------------------------------------------------------------------------------- 1 | Some other random stuff 2 | --------------------------------------------------------------------------------