diff --git a/LICENSE b/LICENSE deleted file mode 100755 index 99c289b..0000000 --- a/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -(The MIT License) - -Copyright (c) 2013 Louis Chatriot <louis.chatriot@gmail.com> - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2ad6a72 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2013 Louis Chatriot <louis.chatriot@gmail.com> +Copyright (c) 2021 Seald [contact@seald.io](mailto:contact@seald.io); + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including without +limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index bc30688..00c323f 100755 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ It is a subset of MongoDB's API (the most used operations). * Inserting documents * Finding documents * Basic Querying - * Operators ($lt, $lte, $gt, $gte, $in, $nin, $ne, $exists, $regex) + * Operators ($lt, $lte, $gt, $gte, $in, $nin, $ne, $stat, $regex) * Array fields * Logical operators $or, $and, $not, $where * Sorting and paginating @@ -236,14 +236,14 @@ db.findOne({ _id: 'id1' }, function (err, doc) { }); ``` -#### Operators ($lt, $lte, $gt, $gte, $in, $nin, $ne, $exists, $regex) +#### Operators ($lt, $lte, $gt, $gte, $in, $nin, $ne, $stat, $regex) The syntax is `{ field: { $op: value } }` where `$op` is any comparison operator: * `$lt`, `$lte`: less than, less than or equal * `$gt`, `$gte`: greater than, greater than or equal * `$in`: member of. `value` must be an array of values * `$ne`, `$nin`: not equal, not a member of -* `$exists`: checks whether the document posses the property `field`. `value` should be true or false +* `$stat`: checks whether the document posses the property `field`. `value` should be true or false * `$regex`: checks whether a string is matched by the regular expression. Contrary to MongoDB, the use of `$options` with `$regex` is not supported, because it doesn't give you more power than regex flags. Basic queries are more readable so only use the `$regex` operator when you need to use another operator with it (see example below) ```javascript @@ -262,8 +262,8 @@ db.find({ planet: { $in: ['Earth', 'Jupiter'] }}, function (err, docs) { // docs contains Earth and Jupiter }); -// Using $exists -db.find({ satellites: { $exists: true } }, function (err, docs) { +// Using $stat +db.find({ satellites: { $stat: true } }, function (err, docs) { // docs contains only Mars }); @@ -724,4 +724,4 @@ You don't have time? You can support NeDB by sending bitcoins to this address: 1 ## License -See [License](LICENSE) +See [License](LICENSE.md) diff --git a/benchmarks/commonUtilities.js b/benchmarks/commonUtilities.js index 48b12d2..18424e1 100755 --- a/benchmarks/commonUtilities.js +++ b/benchmarks/commonUtilities.js @@ -1,65 +1,58 @@ /** * Functions that are used in several benchmark tests */ - -var customUtils = require('../lib/customUtils') - , fs = require('fs') - , path = require('path') - , Datastore = require('../lib/datastore') - , Persistence = require('../lib/persistence') - , executeAsap // process.nextTick or setImmediate depending on your Node version - ; +const fs = require('fs') +const path = require('path') +const Datastore = require('../lib/datastore') +const Persistence = require('../lib/persistence') +let executeAsap try { - executeAsap = setImmediate; + executeAsap = setImmediate } catch (e) { - executeAsap = process.nextTick; + executeAsap = process.nextTick } - /** * Configure the benchmark */ module.exports.getConfiguration = function (benchDb) { - var d, n - , program = require('commander') - ; + const program = require('commander') program .option('-n --number [number]', 'Size of the collection to test on', parseInt) .option('-i --with-index', 'Use an index') .option('-m --in-memory', 'Test with an in-memory only store') - .parse(process.argv); + .parse(process.argv) - n = program.number || 10000; + const n = program.number || 10000 - console.log("----------------------------"); - console.log("Test with " + n + " documents"); - console.log(program.withIndex ? "Use an index" : "Don't use an index"); - console.log(program.inMemory ? "Use an in-memory datastore" : "Use a persistent datastore"); - console.log("----------------------------"); + console.log('----------------------------') + console.log('Test with ' + n + ' documents') + console.log(program.withIndex ? 'Use an index' : "Don't use an index") + console.log(program.inMemory ? 'Use an in-memory datastore' : 'Use a persistent datastore') + console.log('----------------------------') - d = new Datastore({ filename: benchDb - , inMemoryOnly: program.inMemory - }); - - return { n: n, d: d, program: program }; -}; + const d = new Datastore({ + filename: benchDb, + inMemoryOnly: program.inMemory + }) + return { n: n, d: d, program: program } +} /** - * Ensure the workspace exists and the db datafile is empty + * Ensure the workspace stat and the db datafile is empty */ module.exports.prepareDb = function (filename, cb) { Persistence.ensureDirectoryExists(path.dirname(filename), function () { - fs.exists(filename, function (exists) { - if (exists) { - fs.unlink(filename, cb); - } else { return cb(); } - }); - }); -}; - + fs.access(filename, fs.constants.FS_OK, function (err) { + if (!err) { + fs.unlink(filename, cb) + } else { return cb() } + }) + }) +} /** * Return an array with the numbers from 0 to n-1, in a random order @@ -67,242 +60,221 @@ module.exports.prepareDb = function (filename, cb) { * Useful to get fair tests */ function getRandomArray (n) { - var res = [] - , i, j, temp - ; + const res = [] + let i + let j + let temp - for (i = 0; i < n; i += 1) { res[i] = i; } + for (i = 0; i < n; i += 1) { res[i] = i } for (i = n - 1; i >= 1; i -= 1) { - j = Math.floor((i + 1) * Math.random()); - temp = res[i]; - res[i] = res[j]; - res[j] = temp; + j = Math.floor((i + 1) * Math.random()) + temp = res[i] + res[i] = res[j] + res[j] = temp } - return res; + return res }; -module.exports.getRandomArray = getRandomArray; - +module.exports.getRandomArray = getRandomArray /** * Insert a certain number of documents for testing */ module.exports.insertDocs = function (d, n, profiler, cb) { - var beg = new Date() - , order = getRandomArray(n) - ; - - profiler.step('Begin inserting ' + n + ' docs'); - - function runFrom(i) { - if (i === n) { // Finished - var opsPerSecond = Math.floor(1000* n / profiler.elapsedSinceLastStep()); - console.log("===== RESULT (insert) ===== " + opsPerSecond + " ops/s"); - profiler.step('Finished inserting ' + n + ' docs'); - profiler.insertOpsPerSecond = opsPerSecond; - return cb(); + const order = getRandomArray(n) + + profiler.step('Begin inserting ' + n + ' docs') + + function runFrom (i) { + if (i === n) { // Finished + const opsPerSecond = Math.floor(1000 * n / profiler.elapsedSinceLastStep()) + console.log('===== RESULT (insert) ===== ' + opsPerSecond + ' ops/s') + profiler.step('Finished inserting ' + n + ' docs') + profiler.insertOpsPerSecond = opsPerSecond + return cb() } + // eslint-disable-next-line node/handle-callback-err d.insert({ docNumber: order[i] }, function (err) { executeAsap(function () { - runFrom(i + 1); - }); - }); + runFrom(i + 1) + }) + }) } - runFrom(0); -}; - + runFrom(0) +} /** * Find documents with find */ module.exports.findDocs = function (d, n, profiler, cb) { - var beg = new Date() - , order = getRandomArray(n) - ; + const order = getRandomArray(n) - profiler.step("Finding " + n + " documents"); + profiler.step('Finding ' + n + ' documents') - function runFrom(i) { - if (i === n) { // Finished - console.log("===== RESULT (find) ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s"); - profiler.step('Finished finding ' + n + ' docs'); - return cb(); + function runFrom (i) { + if (i === n) { // Finished + console.log('===== RESULT (find) ===== ' + Math.floor(1000 * n / profiler.elapsedSinceLastStep()) + ' ops/s') + profiler.step('Finished finding ' + n + ' docs') + return cb() } + // eslint-disable-next-line node/handle-callback-err d.find({ docNumber: order[i] }, function (err, docs) { - if (docs.length !== 1 || docs[0].docNumber !== order[i]) { return cb('One find didnt work'); } + if (docs.length !== 1 || docs[0].docNumber !== order[i]) { return cb(new Error('One find didnt work')) } executeAsap(function () { - runFrom(i + 1); - }); - }); + runFrom(i + 1) + }) + }) } - runFrom(0); -}; - + runFrom(0) +} /** * Find documents with find and the $in operator */ module.exports.findDocsWithIn = function (d, n, profiler, cb) { - var beg = new Date() - , order = getRandomArray(n) - , ins = [], i, j - , arraySize = Math.min(10, n) // The array for $in needs to be smaller than n (inclusive) - ; + const ins = [] + const arraySize = Math.min(10, n) // Preparing all the $in arrays, will take some time - for (i = 0; i < n; i += 1) { - ins[i] = []; + for (let i = 0; i < n; i += 1) { + ins[i] = [] - for (j = 0; j < arraySize; j += 1) { - ins[i].push((i + j) % n); + for (let j = 0; j < arraySize; j += 1) { + ins[i].push((i + j) % n) } } - profiler.step("Finding " + n + " documents WITH $IN OPERATOR"); + profiler.step('Finding ' + n + ' documents WITH $IN OPERATOR') - function runFrom(i) { - if (i === n) { // Finished - console.log("===== RESULT (find with in selector) ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s"); - profiler.step('Finished finding ' + n + ' docs'); - return cb(); + function runFrom (i) { + if (i === n) { // Finished + console.log('===== RESULT (find with in selector) ===== ' + Math.floor(1000 * n / profiler.elapsedSinceLastStep()) + ' ops/s') + profiler.step('Finished finding ' + n + ' docs') + return cb() } + // eslint-disable-next-line node/handle-callback-err d.find({ docNumber: { $in: ins[i] } }, function (err, docs) { - if (docs.length !== arraySize) { return cb('One find didnt work'); } + if (docs.length !== arraySize) { return cb(new Error('One find didnt work')) } executeAsap(function () { - runFrom(i + 1); - }); - }); + runFrom(i + 1) + }) + }) } - runFrom(0); -}; - + runFrom(0) +} /** * Find documents with findOne */ module.exports.findOneDocs = function (d, n, profiler, cb) { - var beg = new Date() - , order = getRandomArray(n) - ; + const order = getRandomArray(n) - profiler.step("FindingOne " + n + " documents"); + profiler.step('FindingOne ' + n + ' documents') - function runFrom(i) { - if (i === n) { // Finished - console.log("===== RESULT (findOne) ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s"); - profiler.step('Finished finding ' + n + ' docs'); - return cb(); + function runFrom (i) { + if (i === n) { // Finished + console.log('===== RESULT (findOne) ===== ' + Math.floor(1000 * n / profiler.elapsedSinceLastStep()) + ' ops/s') + profiler.step('Finished finding ' + n + ' docs') + return cb() } + // eslint-disable-next-line node/handle-callback-err d.findOne({ docNumber: order[i] }, function (err, doc) { - if (!doc || doc.docNumber !== order[i]) { return cb('One find didnt work'); } + if (!doc || doc.docNumber !== order[i]) { return cb(new Error('One find didnt work')) } executeAsap(function () { - runFrom(i + 1); - }); - }); + runFrom(i + 1) + }) + }) } - runFrom(0); -}; - + runFrom(0) +} /** * Update documents * options is the same as the options object for update */ module.exports.updateDocs = function (options, d, n, profiler, cb) { - var beg = new Date() - , order = getRandomArray(n) - ; + const order = getRandomArray(n) - profiler.step("Updating " + n + " documents"); + profiler.step('Updating ' + n + ' documents') - function runFrom(i) { - if (i === n) { // Finished - console.log("===== RESULT (update) ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s"); - profiler.step('Finished updating ' + n + ' docs'); - return cb(); + function runFrom (i) { + if (i === n) { // Finished + console.log('===== RESULT (update) ===== ' + Math.floor(1000 * n / profiler.elapsedSinceLastStep()) + ' ops/s') + profiler.step('Finished updating ' + n + ' docs') + return cb() } // Will not actually modify the document but will take the same time d.update({ docNumber: order[i] }, { docNumber: order[i] }, options, function (err, nr) { - if (err) { return cb(err); } - if (nr !== 1) { return cb('One update didnt work'); } + if (err) { return cb(err) } + if (nr !== 1) { return cb(new Error('One update didnt work')) } executeAsap(function () { - runFrom(i + 1); - }); - }); + runFrom(i + 1) + }) + }) } - runFrom(0); -}; - + runFrom(0) +} /** * Remove documents * options is the same as the options object for update */ module.exports.removeDocs = function (options, d, n, profiler, cb) { - var beg = new Date() - , order = getRandomArray(n) - ; + const order = getRandomArray(n) - profiler.step("Removing " + n + " documents"); + profiler.step('Removing ' + n + ' documents') - function runFrom(i) { - if (i === n) { // Finished + function runFrom (i) { + if (i === n) { // Finished // opsPerSecond corresponds to 1 insert + 1 remove, needed to keep collection size at 10,000 // We need to subtract the time taken by one insert to get the time actually taken by one remove - var opsPerSecond = Math.floor(1000 * n / profiler.elapsedSinceLastStep()); - var removeOpsPerSecond = Math.floor(1 / ((1 / opsPerSecond) - (1 / profiler.insertOpsPerSecond))) - console.log("===== RESULT (remove) ===== " + removeOpsPerSecond + " ops/s"); - profiler.step('Finished removing ' + n + ' docs'); - return cb(); + const opsPerSecond = Math.floor(1000 * n / profiler.elapsedSinceLastStep()) + const removeOpsPerSecond = Math.floor(1 / ((1 / opsPerSecond) - (1 / profiler.insertOpsPerSecond))) + console.log('===== RESULT (remove) ===== ' + removeOpsPerSecond + ' ops/s') + profiler.step('Finished removing ' + n + ' docs') + return cb() } d.remove({ docNumber: order[i] }, options, function (err, nr) { - if (err) { return cb(err); } - if (nr !== 1) { return cb('One remove didnt work'); } - d.insert({ docNumber: order[i] }, function (err) { // We need to reinsert the doc so that we keep the collection's size at n - // So actually we're calculating the average time taken by one insert + one remove + if (err) { return cb(err) } + if (nr !== 1) { return cb(new Error('One remove didnt work')) } + // eslint-disable-next-line node/handle-callback-err + d.insert({ docNumber: order[i] }, function (err) { // We need to reinsert the doc so that we keep the collection's size at n + // So actually we're calculating the average time taken by one insert + one remove executeAsap(function () { - runFrom(i + 1); - }); - }); - }); + runFrom(i + 1) + }) + }) + }) } - runFrom(0); -}; - + runFrom(0) +} /** * Load database */ module.exports.loadDatabase = function (d, n, profiler, cb) { - var beg = new Date() - , order = getRandomArray(n) - ; - - profiler.step("Loading the database " + n + " times"); + profiler.step('Loading the database ' + n + ' times') - function runFrom(i) { - if (i === n) { // Finished - console.log("===== RESULT ===== " + Math.floor(1000* n / profiler.elapsedSinceLastStep()) + " ops/s"); - profiler.step('Finished loading a database' + n + ' times'); - return cb(); + function runFrom (i) { + if (i === n) { // Finished + console.log('===== RESULT ===== ' + Math.floor(1000 * n / profiler.elapsedSinceLastStep()) + ' ops/s') + profiler.step('Finished loading a database' + n + ' times') + return cb() } + // eslint-disable-next-line node/handle-callback-err d.loadDatabase(function (err) { executeAsap(function () { - runFrom(i + 1); - }); - }); + runFrom(i + 1) + }) + }) } - runFrom(0); -}; - - - - + runFrom(0) +} diff --git a/benchmarks/ensureIndex.js b/benchmarks/ensureIndex.js index 1c3c43e..5cd14a5 100755 --- a/benchmarks/ensureIndex.js +++ b/benchmarks/ensureIndex.js @@ -1,51 +1,48 @@ -var Datastore = require('../lib/datastore') - , benchDb = 'workspace/insert.bench.db' - , async = require('async') - , commonUtilities = require('./commonUtilities') - , execTime = require('exec-time') - , profiler = new execTime('INSERT BENCH') - , d = new Datastore(benchDb) - , program = require('commander') - , n - ; +const Datastore = require('../lib/datastore') +const benchDb = 'workspace/insert.bench.db' +const async = require('async') +const commonUtilities = require('./commonUtilities') +const ExecTime = require('exec-time') +const profiler = new ExecTime('INSERT BENCH') +const d = new Datastore(benchDb) +const program = require('commander') program .option('-n --number [number]', 'Size of the collection to test on', parseInt) .option('-i --with-index', 'Test with an index') - .parse(process.argv); + .parse(process.argv) -n = program.number || 10000; +const n = program.number || 10000 -console.log("----------------------------"); -console.log("Test with " + n + " documents"); -console.log("----------------------------"); +console.log('----------------------------') +console.log('Test with ' + n + ' documents') +console.log('----------------------------') async.waterfall([ - async.apply(commonUtilities.prepareDb, benchDb) -, function (cb) { + async.apply(commonUtilities.prepareDb, benchDb), + function (cb) { d.loadDatabase(function (err) { - if (err) { return cb(err); } - cb(); - }); - } -, function (cb) { profiler.beginProfiling(); return cb(); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) -, function (cb) { - var i; + if (err) { return cb(err) } + cb() + }) + }, + function (cb) { profiler.beginProfiling(); return cb() }, + async.apply(commonUtilities.insertDocs, d, n, profiler), + function (cb) { + let i - profiler.step('Begin calling ensureIndex ' + n + ' times'); + profiler.step('Begin calling ensureIndex ' + n + ' times') for (i = 0; i < n; i += 1) { - d.ensureIndex({ fieldName: 'docNumber' }); - delete d.indexes.docNumber; + d.ensureIndex({ fieldName: 'docNumber' }) + delete d.indexes.docNumber } - console.log("Average time for one ensureIndex: " + (profiler.elapsedSinceLastStep() / n) + "ms"); - profiler.step('Finished calling ensureIndex ' + n + ' times'); + console.log('Average time for one ensureIndex: ' + (profiler.elapsedSinceLastStep() / n) + 'ms') + profiler.step('Finished calling ensureIndex ' + n + ' times') } ], function (err) { - profiler.step("Benchmark finished"); - - if (err) { return console.log("An error was encountered: ", err); } -}); + profiler.step('Benchmark finished') + if (err) { return console.log('An error was encountered: ', err) } +}) diff --git a/benchmarks/find.js b/benchmarks/find.js index 21a57bd..5b32098 100755 --- a/benchmarks/find.js +++ b/benchmarks/find.js @@ -1,30 +1,26 @@ -var Datastore = require('../lib/datastore') - , benchDb = 'workspace/find.bench.db' - , fs = require('fs') - , path = require('path') - , async = require('async') - , execTime = require('exec-time') - , profiler = new execTime('FIND BENCH') - , commonUtilities = require('./commonUtilities') - , config = commonUtilities.getConfiguration(benchDb) - , d = config.d - , n = config.n - ; +const benchDb = 'workspace/find.bench.db' +const async = require('async') +const ExecTime = require('exec-time') +const profiler = new ExecTime('FIND BENCH') +const commonUtilities = require('./commonUtilities') +const config = commonUtilities.getConfiguration(benchDb) +const d = config.d +const n = config.n async.waterfall([ - async.apply(commonUtilities.prepareDb, benchDb) -, function (cb) { + async.apply(commonUtilities.prepareDb, benchDb), + function (cb) { d.loadDatabase(function (err) { - if (err) { return cb(err); } - if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); } - cb(); - }); - } -, function (cb) { profiler.beginProfiling(); return cb(); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) -, async.apply(commonUtilities.findDocs, d, n, profiler) + if (err) { return cb(err) } + if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }) } + cb() + }) + }, + function (cb) { profiler.beginProfiling(); return cb() }, + async.apply(commonUtilities.insertDocs, d, n, profiler), + async.apply(commonUtilities.findDocs, d, n, profiler) ], function (err) { - profiler.step("Benchmark finished"); + profiler.step('Benchmark finished') - if (err) { return console.log("An error was encountered: ", err); } -}); + if (err) { return console.log('An error was encountered: ', err) } +}) diff --git a/benchmarks/findOne.js b/benchmarks/findOne.js index d70c636..97b8951 100755 --- a/benchmarks/findOne.js +++ b/benchmarks/findOne.js @@ -1,31 +1,27 @@ -var Datastore = require('../lib/datastore') - , benchDb = 'workspace/findOne.bench.db' - , fs = require('fs') - , path = require('path') - , async = require('async') - , execTime = require('exec-time') - , profiler = new execTime('FINDONE BENCH') - , commonUtilities = require('./commonUtilities') - , config = commonUtilities.getConfiguration(benchDb) - , d = config.d - , n = config.n - ; +const benchDb = 'workspace/findOne.bench.db' +const async = require('async') +const ExecTime = require('exec-time') +const profiler = new ExecTime('FINDONE BENCH') +const commonUtilities = require('./commonUtilities') +const config = commonUtilities.getConfiguration(benchDb) +const d = config.d +const n = config.n async.waterfall([ - async.apply(commonUtilities.prepareDb, benchDb) -, function (cb) { + async.apply(commonUtilities.prepareDb, benchDb), + function (cb) { d.loadDatabase(function (err) { - if (err) { return cb(err); } - if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); } - cb(); - }); - } -, function (cb) { profiler.beginProfiling(); return cb(); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) -, function (cb) { setTimeout(function () {cb();}, 500); } -, async.apply(commonUtilities.findOneDocs, d, n, profiler) + if (err) { return cb(err) } + if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }) } + cb() + }) + }, + function (cb) { profiler.beginProfiling(); return cb() }, + async.apply(commonUtilities.insertDocs, d, n, profiler), + function (cb) { setTimeout(function () { cb() }, 500) }, + async.apply(commonUtilities.findOneDocs, d, n, profiler) ], function (err) { - profiler.step("Benchmark finished"); + profiler.step('Benchmark finished') - if (err) { return console.log("An error was encountered: ", err); } -}); + if (err) { return console.log('An error was encountered: ', err) } +}) diff --git a/benchmarks/findWithIn.js b/benchmarks/findWithIn.js index 9777ab7..ab0b7de 100755 --- a/benchmarks/findWithIn.js +++ b/benchmarks/findWithIn.js @@ -1,30 +1,26 @@ -var Datastore = require('../lib/datastore') - , benchDb = 'workspace/find.bench.db' - , fs = require('fs') - , path = require('path') - , async = require('async') - , execTime = require('exec-time') - , profiler = new execTime('FIND BENCH') - , commonUtilities = require('./commonUtilities') - , config = commonUtilities.getConfiguration(benchDb) - , d = config.d - , n = config.n - ; +const benchDb = 'workspace/find.bench.db' +const async = require('async') +const ExecTime = require('exec-time') +const profiler = new ExecTime('FIND BENCH') +const commonUtilities = require('./commonUtilities') +const config = commonUtilities.getConfiguration(benchDb) +const d = config.d +const n = config.n async.waterfall([ - async.apply(commonUtilities.prepareDb, benchDb) -, function (cb) { + async.apply(commonUtilities.prepareDb, benchDb), + function (cb) { d.loadDatabase(function (err) { - if (err) { return cb(err); } - if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); } - cb(); - }); - } -, function (cb) { profiler.beginProfiling(); return cb(); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) -, async.apply(commonUtilities.findDocsWithIn, d, n, profiler) + if (err) { return cb(err) } + if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }) } + cb() + }) + }, + function (cb) { profiler.beginProfiling(); return cb() }, + async.apply(commonUtilities.insertDocs, d, n, profiler), + async.apply(commonUtilities.findDocsWithIn, d, n, profiler) ], function (err) { - profiler.step("Benchmark finished"); + profiler.step('Benchmark finished') - if (err) { return console.log("An error was encountered: ", err); } -}); + if (err) { return console.log('An error was encountered: ', err) } +}) diff --git a/benchmarks/insert.js b/benchmarks/insert.js index 44875ea..784844a 100755 --- a/benchmarks/insert.js +++ b/benchmarks/insert.js @@ -1,33 +1,31 @@ -var Datastore = require('../lib/datastore') - , benchDb = 'workspace/insert.bench.db' - , async = require('async') - , execTime = require('exec-time') - , profiler = new execTime('INSERT BENCH') - , commonUtilities = require('./commonUtilities') - , config = commonUtilities.getConfiguration(benchDb) - , d = config.d - , n = config.n - ; +const benchDb = 'workspace/insert.bench.db' +const async = require('async') +const ExecTime = require('exec-time') +const profiler = new ExecTime('INSERT BENCH') +const commonUtilities = require('./commonUtilities') +const config = commonUtilities.getConfiguration(benchDb) +const d = config.d +let n = config.n async.waterfall([ - async.apply(commonUtilities.prepareDb, benchDb) -, function (cb) { + async.apply(commonUtilities.prepareDb, benchDb), + function (cb) { d.loadDatabase(function (err) { - if (err) { return cb(err); } + if (err) { return cb(err) } if (config.program.withIndex) { - d.ensureIndex({ fieldName: 'docNumber' }); - n = 2 * n; // We will actually insert twice as many documents - // because the index is slower when the collection is already - // big. So the result given by the algorithm will be a bit worse than - // actual performance + d.ensureIndex({ fieldName: 'docNumber' }) + n = 2 * n // We will actually insert twice as many documents + // because the index is slower when the collection is already + // big. So the result given by the algorithm will be a bit worse than + // actual performance } - cb(); - }); - } -, function (cb) { profiler.beginProfiling(); return cb(); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) + cb() + }) + }, + function (cb) { profiler.beginProfiling(); return cb() }, + async.apply(commonUtilities.insertDocs, d, n, profiler) ], function (err) { - profiler.step("Benchmark finished"); + profiler.step('Benchmark finished') - if (err) { return console.log("An error was encountered: ", err); } -}); + if (err) { return console.log('An error was encountered: ', err) } +}) diff --git a/benchmarks/loadDatabase.js b/benchmarks/loadDatabase.js index 46290ff..fc8a157 100755 --- a/benchmarks/loadDatabase.js +++ b/benchmarks/loadDatabase.js @@ -1,38 +1,34 @@ -var Datastore = require('../lib/datastore') - , benchDb = 'workspace/loaddb.bench.db' - , fs = require('fs') - , path = require('path') - , async = require('async') - , commonUtilities = require('./commonUtilities') - , execTime = require('exec-time') - , profiler = new execTime('LOADDB BENCH') - , d = new Datastore(benchDb) - , program = require('commander') - , n - ; +const Datastore = require('../lib/datastore') +const benchDb = 'workspace/loaddb.bench.db' +const async = require('async') +const commonUtilities = require('./commonUtilities') +const ExecTime = require('exec-time') +const profiler = new ExecTime('LOADDB BENCH') +const d = new Datastore(benchDb) +const program = require('commander') program .option('-n --number [number]', 'Size of the collection to test on', parseInt) .option('-i --with-index', 'Test with an index') - .parse(process.argv); + .parse(process.argv) -n = program.number || 10000; +const n = program.number || 10000 -console.log("----------------------------"); -console.log("Test with " + n + " documents"); -console.log(program.withIndex ? "Use an index" : "Don't use an index"); -console.log("----------------------------"); +console.log('----------------------------') +console.log('Test with ' + n + ' documents') +console.log(program.withIndex ? 'Use an index' : "Don't use an index") +console.log('----------------------------') async.waterfall([ - async.apply(commonUtilities.prepareDb, benchDb) -, function (cb) { - d.loadDatabase(cb); - } -, function (cb) { profiler.beginProfiling(); return cb(); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) -, async.apply(commonUtilities.loadDatabase, d, n, profiler) + async.apply(commonUtilities.prepareDb, benchDb), + function (cb) { + d.loadDatabase(cb) + }, + function (cb) { profiler.beginProfiling(); return cb() }, + async.apply(commonUtilities.insertDocs, d, n, profiler), + async.apply(commonUtilities.loadDatabase, d, n, profiler) ], function (err) { - profiler.step("Benchmark finished"); + profiler.step('Benchmark finished') - if (err) { return console.log("An error was encountered: ", err); } -}); + if (err) { return console.log('An error was encountered: ', err) } +}) diff --git a/benchmarks/remove.js b/benchmarks/remove.js index 9c7f522..5bd580d 100755 --- a/benchmarks/remove.js +++ b/benchmarks/remove.js @@ -1,38 +1,34 @@ -var Datastore = require('../lib/datastore') - , benchDb = 'workspace/remove.bench.db' - , fs = require('fs') - , path = require('path') - , async = require('async') - , execTime = require('exec-time') - , profiler = new execTime('REMOVE BENCH') - , commonUtilities = require('./commonUtilities') - , config = commonUtilities.getConfiguration(benchDb) - , d = config.d - , n = config.n - ; +const benchDb = 'workspace/remove.bench.db' +const async = require('async') +const ExecTime = require('exec-time') +const profiler = new ExecTime('REMOVE BENCH') +const commonUtilities = require('./commonUtilities') +const config = commonUtilities.getConfiguration(benchDb) +const d = config.d +const n = config.n async.waterfall([ - async.apply(commonUtilities.prepareDb, benchDb) -, function (cb) { + async.apply(commonUtilities.prepareDb, benchDb), + function (cb) { d.loadDatabase(function (err) { - if (err) { return cb(err); } - if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); } - cb(); - }); - } -, function (cb) { profiler.beginProfiling(); return cb(); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) + if (err) { return cb(err) } + if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }) } + cb() + }) + }, + function (cb) { profiler.beginProfiling(); return cb() }, + async.apply(commonUtilities.insertDocs, d, n, profiler), -// Test with remove only one document -, function (cb) { profiler.step('MULTI: FALSE'); return cb(); } -, async.apply(commonUtilities.removeDocs, { multi: false }, d, n, profiler) -// Test with multiple documents -, function (cb) { d.remove({}, { multi: true }, function () { return cb(); }); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) -, function (cb) { profiler.step('MULTI: TRUE'); return cb(); } -, async.apply(commonUtilities.removeDocs, { multi: true }, d, n, profiler) + // Test with remove only one document + function (cb) { profiler.step('MULTI: FALSE'); return cb() }, + async.apply(commonUtilities.removeDocs, { multi: false }, d, n, profiler), + // Test with multiple documents + function (cb) { d.remove({}, { multi: true }, function () { return cb() }) }, + async.apply(commonUtilities.insertDocs, d, n, profiler), + function (cb) { profiler.step('MULTI: TRUE'); return cb() }, + async.apply(commonUtilities.removeDocs, { multi: true }, d, n, profiler) ], function (err) { - profiler.step("Benchmark finished"); + profiler.step('Benchmark finished') - if (err) { return console.log("An error was encountered: ", err); } -}); + if (err) { return console.log('An error was encountered: ', err) } +}) diff --git a/benchmarks/update.js b/benchmarks/update.js index 025a23e..91d27c7 100755 --- a/benchmarks/update.js +++ b/benchmarks/update.js @@ -1,39 +1,36 @@ -var Datastore = require('../lib/datastore') - , benchDb = 'workspace/update.bench.db' - , fs = require('fs') - , path = require('path') - , async = require('async') - , execTime = require('exec-time') - , profiler = new execTime('UPDATE BENCH') - , commonUtilities = require('./commonUtilities') - , config = commonUtilities.getConfiguration(benchDb) - , d = config.d - , n = config.n - ; +const benchDb = 'workspace/update.bench.db' +const async = require('async') +const ExecTime = require('exec-time') +const profiler = new ExecTime('UPDATE BENCH') +const commonUtilities = require('./commonUtilities') +const config = commonUtilities.getConfiguration(benchDb) +const d = config.d +const n = config.n async.waterfall([ - async.apply(commonUtilities.prepareDb, benchDb) -, function (cb) { + async.apply(commonUtilities.prepareDb, benchDb), + function (cb) { d.loadDatabase(function (err) { - if (err) { return cb(err); } - if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }); } - cb(); - }); - } -, function (cb) { profiler.beginProfiling(); return cb(); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) + if (err) { return cb(err) } + if (config.program.withIndex) { d.ensureIndex({ fieldName: 'docNumber' }) } + cb() + }) + }, + function (cb) { profiler.beginProfiling(); return cb() }, + async.apply(commonUtilities.insertDocs, d, n, profiler), -// Test with update only one document -, function (cb) { profiler.step('MULTI: FALSE'); return cb(); } -, async.apply(commonUtilities.updateDocs, { multi: false }, d, n, profiler) + // Test with update only one document + function (cb) { profiler.step('MULTI: FALSE'); return cb() }, + async.apply(commonUtilities.updateDocs, { multi: false }, d, n, profiler), -// Test with multiple documents -, function (cb) { d.remove({}, { multi: true }, function (err) { return cb(); }); } -, async.apply(commonUtilities.insertDocs, d, n, profiler) -, function (cb) { profiler.step('MULTI: TRUE'); return cb(); } -, async.apply(commonUtilities.updateDocs, { multi: true }, d, n, profiler) + // Test with multiple documents + // eslint-disable-next-line node/handle-callback-err + function (cb) { d.remove({}, { multi: true }, function (err) { return cb() }) }, + async.apply(commonUtilities.insertDocs, d, n, profiler), + function (cb) { profiler.step('MULTI: TRUE'); return cb() }, + async.apply(commonUtilities.updateDocs, { multi: true }, d, n, profiler) ], function (err) { - profiler.step("Benchmark finished"); + profiler.step('Benchmark finished') - if (err) { return console.log("An error was encountered: ", err); } -}); + if (err) { return console.log('An error was encountered: ', err) } +}) diff --git a/index.js b/index.js index 1c6f810..e6b9e6c 100755 --- a/index.js +++ b/index.js @@ -1,3 +1,3 @@ -var Datastore = require('./lib/datastore'); +const Datastore = require('./lib/datastore') -module.exports = Datastore; +module.exports = Datastore diff --git a/lib/cursor.js b/lib/cursor.js index 673d7a4..1dddd0a 100755 --- a/lib/cursor.js +++ b/lib/cursor.js @@ -1,204 +1,201 @@ /** * Manage access to data, be it to find, update or remove it */ -var model = require('./model') - , _ = require('underscore') - ; - - - -/** - * Create a new cursor for this collection - * @param {Datastore} db - The datastore this cursor is bound to - * @param {Query} query - The query this cursor will operate on - * @param {Function} execFn - Handler to be executed after cursor has found the results and before the callback passed to find/findOne/update/remove - */ -function Cursor (db, query, execFn) { - this.db = db; - this.query = query || {}; - if (execFn) { this.execFn = execFn; } -} - - -/** - * Set a limit to the number of results - */ -Cursor.prototype.limit = function(limit) { - this._limit = limit; - return this; -}; - - -/** - * Skip a the number of results - */ -Cursor.prototype.skip = function(skip) { - this._skip = skip; - return this; -}; - - -/** - * Sort results of the query - * @param {SortQuery} sortQuery - SortQuery is { field: order }, field can use the dot-notation, order is 1 for ascending and -1 for descending - */ -Cursor.prototype.sort = function(sortQuery) { - this._sort = sortQuery; - return this; -}; - +const model = require('./model') +const _ = require('underscore') + +class Cursor { + /** + * Create a new cursor for this collection + * @param {Datastore} db - The datastore this cursor is bound to + * @param {Query} query - The query this cursor will operate on + * @param {Function} execFn - Handler to be executed after cursor has found the results and before the callback passed to find/findOne/update/remove + */ + constructor (db, query, execFn) { + this.db = db + this.query = query || {} + if (execFn) { this.execFn = execFn } + } -/** - * Add the use of a projection - * @param {Object} projection - MongoDB-style projection. {} means take all fields. Then it's { key1: 1, key2: 1 } to take only key1 and key2 - * { key1: 0, key2: 0 } to omit only key1 and key2. Except _id, you can't mix takes and omits - */ -Cursor.prototype.projection = function(projection) { - this._projection = projection; - return this; -}; + /** + * Set a limit to the number of results + */ + limit (limit) { + this._limit = limit + return this + } + /** + * Skip a the number of results + */ + skip (skip) { + this._skip = skip + return this + } -/** - * Apply the projection - */ -Cursor.prototype.project = function (candidates) { - var res = [], self = this - , keepId, action, keys - ; + /** + * Sort results of the query + * @param {SortQuery} sortQuery - SortQuery is { field: order }, field can use the dot-notation, order is 1 for ascending and -1 for descending + */ + sort (sortQuery) { + this._sort = sortQuery + return this + } - if (this._projection === undefined || Object.keys(this._projection).length === 0) { - return candidates; + /** + * Add the use of a projection + * @param {Object} projection - MongoDB-style projection. {} means take all fields. Then it's { key1: 1, key2: 1 } to take only key1 and key2 + * { key1: 0, key2: 0 } to omit only key1 and key2. Except _id, you can't mix takes and omits + */ + projection (projection) { + this._projection = projection + return this } - keepId = this._projection._id === 0 ? false : true; - this._projection = _.omit(this._projection, '_id'); - - // Check for consistency - keys = Object.keys(this._projection); - keys.forEach(function (k) { - if (action !== undefined && self._projection[k] !== action) { throw new Error("Can't both keep and omit fields except for _id"); } - action = self._projection[k]; - }); - - // Do the actual projection - candidates.forEach(function (candidate) { - var toPush; - if (action === 1) { // pick-type projection - toPush = { $set: {} }; - keys.forEach(function (k) { - toPush.$set[k] = model.getDotValue(candidate, k); - if (toPush.$set[k] === undefined) { delete toPush.$set[k]; } - }); - toPush = model.modify({}, toPush); - } else { // omit-type projection - toPush = { $unset: {} }; - keys.forEach(function (k) { toPush.$unset[k] = true }); - toPush = model.modify(candidate, toPush); - } - if (keepId) { - toPush._id = candidate._id; - } else { - delete toPush._id; - } - res.push(toPush); - }); + /** + * Apply the projection + */ + project (candidates) { + const res = [] + const self = this + let action - return res; -}; + if (this._projection === undefined || Object.keys(this._projection).length === 0) { + return candidates + } + const keepId = this._projection._id !== 0 + this._projection = _.omit(this._projection, '_id') + + // Check for consistency + const keys = Object.keys(this._projection) + keys.forEach(function (k) { + if (action !== undefined && self._projection[k] !== action) { throw new Error('Can\'t both keep and omit fields except for _id') } + action = self._projection[k] + }) + + // Do the actual projection + candidates.forEach(function (candidate) { + let toPush + if (action === 1) { // pick-type projection + toPush = { $set: {} } + keys.forEach(function (k) { + toPush.$set[k] = model.getDotValue(candidate, k) + if (toPush.$set[k] === undefined) { delete toPush.$set[k] } + }) + toPush = model.modify({}, toPush) + } else { // omit-type projection + toPush = { $unset: {} } + keys.forEach(function (k) { toPush.$unset[k] = true }) + toPush = model.modify(candidate, toPush) + } + if (keepId) { + toPush._id = candidate._id + } else { + delete toPush._id + } + res.push(toPush) + }) -/** - * Get all matching elements - * Will return pointers to matched elements (shallow copies), returning full copies is the role of find or findOne - * This is an internal function, use exec which uses the executor - * - * @param {Function} callback - Signature: err, results - */ -Cursor.prototype._exec = function(_callback) { - var res = [], added = 0, skipped = 0, self = this - , error = null - , i, keys, key - ; - - function callback (error, res) { - if (self.execFn) { - return self.execFn(error, res, _callback); - } else { - return _callback(error, res); - } + return res } - this.db.getCandidates(this.query, function (err, candidates) { - if (err) { return callback(err); } + /** + * Get all matching elements + * Will return pointers to matched elements (shallow copies), returning full copies is the role of find or findOne + * This is an internal function, use exec which uses the executor + * + * @param {Function} callback - Signature: err, results + */ + _exec (_callback) { + let res = [] + let added = 0 + let skipped = 0 + const self = this + let error = null + let i + let keys + let key + + function callback (error, res) { + if (self.execFn) { + return self.execFn(error, res, _callback) + } else { + return _callback(error, res) + } + } - try { - for (i = 0; i < candidates.length; i += 1) { - if (model.match(candidates[i], self.query)) { - // If a sort is defined, wait for the results to be sorted before applying limit and skip - if (!self._sort) { - if (self._skip && self._skip > skipped) { - skipped += 1; + this.db.getCandidates(this.query, function (err, candidates) { + if (err) { return callback(err) } + + try { + for (i = 0; i < candidates.length; i += 1) { + if (model.match(candidates[i], self.query)) { + // If a sort is defined, wait for the results to be sorted before applying limit and skip + if (!self._sort) { + if (self._skip && self._skip > skipped) { + skipped += 1 + } else { + res.push(candidates[i]) + added += 1 + if (self._limit && self._limit <= added) { break } + } } else { - res.push(candidates[i]); - added += 1; - if (self._limit && self._limit <= added) { break; } + res.push(candidates[i]) } - } else { - res.push(candidates[i]); } } + } catch (err) { + return callback(err) } - } catch (err) { - return callback(err); - } - // Apply all sorts - if (self._sort) { - keys = Object.keys(self._sort); + // Apply all sorts + if (self._sort) { + keys = Object.keys(self._sort) - // Sorting - var criteria = []; - for (i = 0; i < keys.length; i++) { - key = keys[i]; - criteria.push({ key: key, direction: self._sort[key] }); - } - res.sort(function(a, b) { - var criterion, compare, i; - for (i = 0; i < criteria.length; i++) { - criterion = criteria[i]; - compare = criterion.direction * model.compareThings(model.getDotValue(a, criterion.key), model.getDotValue(b, criterion.key), self.db.compareStrings); - if (compare !== 0) { - return compare; - } + // Sorting + const criteria = [] + for (i = 0; i < keys.length; i++) { + key = keys[i] + criteria.push({ key: key, direction: self._sort[key] }) } - return 0; - }); - - // Applying limit and skip - var limit = self._limit || res.length - , skip = self._skip || 0; - - res = res.slice(skip, skip + limit); - } + res.sort(function (a, b) { + let criterion + let compare + let i + for (i = 0; i < criteria.length; i++) { + criterion = criteria[i] + compare = criterion.direction * model.compareThings(model.getDotValue(a, criterion.key), model.getDotValue(b, criterion.key), self.db.compareStrings) + if (compare !== 0) { + return compare + } + } + return 0 + }) - // Apply projection - try { - res = self.project(res); - } catch (e) { - error = e; - res = undefined; - } + // Applying limit and skip + const limit = self._limit || res.length + const skip = self._skip || 0 - return callback(error, res); - }); -}; + res = res.slice(skip, skip + limit) + } -Cursor.prototype.exec = function () { - this.db.executor.push({ this: this, fn: this._exec, arguments: arguments }); -}; + // Apply projection + try { + res = self.project(res) + } catch (e) { + error = e + res = undefined + } + return callback(error, res) + }) + } + exec () { + this.db.executor.push({ this: this, fn: this._exec, arguments: arguments }) + } +} // Interface -module.exports = Cursor; +module.exports = Cursor diff --git a/lib/customUtils.js b/lib/customUtils.js index 8b58560..da57d47 100755 --- a/lib/customUtils.js +++ b/lib/customUtils.js @@ -1,5 +1,4 @@ -var crypto = require('crypto') - ; +const crypto = require('crypto') /** * Return a random alphanumerical string of length len @@ -12,11 +11,9 @@ var crypto = require('crypto') function uid (len) { return crypto.randomBytes(Math.ceil(Math.max(8, len * 2))) .toString('base64') - .replace(/[+\/]/g, '') - .slice(0, len); + .replace(/[+/]/g, '') + .slice(0, len) } - // Interface -module.exports.uid = uid; - +module.exports.uid = uid diff --git a/lib/datastore.js b/lib/datastore.js index 4b978ad..28c9f3a 100755 --- a/lib/datastore.js +++ b/lib/datastore.js @@ -1,705 +1,705 @@ -var customUtils = require('./customUtils') - , model = require('./model') - , async = require('async') - , Executor = require('./executor') - , Index = require('./indexes') - , util = require('util') - , _ = require('underscore') - , Persistence = require('./persistence') - , Cursor = require('./cursor') - ; - - -/** - * Create a new collection - * @param {String} options.filename Optional, datastore will be in-memory only if not provided - * @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) - * @param {Boolean} options.inMemoryOnly Optional, defaults to false - * @param {String} options.nodeWebkitAppName Optional, specify the name of your NW app if you want options.filename to be relative to the directory where - * Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion) - * @param {Boolean} options.autoload Optional, defaults to false - * @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 - * @param {Function} options.afterSerialization/options.beforeDeserialization Optional, serialization hooks - * @param {Number} options.corruptAlertThreshold Optional, threshold after which an alert is thrown if too much data is corrupt - * @param {Function} options.compareStrings Optional, string comparison function that overrides default for sorting - * - * Event Emitter - Events - * * compaction.done - Fired whenever a compaction operation was finished - */ -function Datastore (options) { - var filename; - - // Retrocompatibility with v0.6 and before - if (typeof options === 'string') { - filename = options; - this.inMemoryOnly = false; // Default - } else { - options = options || {}; - filename = options.filename; - this.inMemoryOnly = options.inMemoryOnly || false; - this.autoload = options.autoload || false; - this.timestampData = options.timestampData || false; - } - - // Determine whether in memory or persistent - if (!filename || typeof filename !== 'string' || filename.length === 0) { - this.filename = null; - this.inMemoryOnly = true; - } else { - this.filename = filename; - } +const customUtils = require('./customUtils') +const model = require('./model') +const async = require('async') +const Executor = require('./executor') +const Index = require('./indexes') +const util = require('util') +const _ = require('underscore') +const Persistence = require('./persistence') +const Cursor = require('./cursor') + +class Datastore { + /** + * Create a new collection + * @param {String} options.filename Optional, datastore will be in-memory only if not provided + * @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) + * @param {Boolean} options.inMemoryOnly Optional, defaults to false + * @param {String} options.nodeWebkitAppName Optional, specify the name of your NW app if you want options.filename to be relative to the directory where + * Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion) + * @param {Boolean} options.autoload Optional, defaults to false + * @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 + * @param {Function} options.afterSerialization/options.beforeDeserialization Optional, serialization hooks + * @param {Number} options.corruptAlertThreshold Optional, threshold after which an alert is thrown if too much data is corrupt + * @param {Function} options.compareStrings Optional, string comparison function that overrides default for sorting + * + * Event Emitter - Events + * * compaction.done - Fired whenever a compaction operation was finished + */ + constructor (options) { + let filename + + // Retrocompatibility with v0.6 and before + if (typeof options === 'string') { + filename = options + this.inMemoryOnly = false // Default + } else { + options = options || {} + filename = options.filename + this.inMemoryOnly = options.inMemoryOnly || false + this.autoload = options.autoload || false + this.timestampData = options.timestampData || false + } - // String comparison function - this.compareStrings = options.compareStrings; - - // Persistence handling - this.persistence = new Persistence({ db: this, nodeWebkitAppName: options.nodeWebkitAppName - , afterSerialization: options.afterSerialization - , beforeDeserialization: options.beforeDeserialization - , corruptAlertThreshold: options.corruptAlertThreshold - }); - - // This new executor is ready if we don't use persistence - // If we do, it will only be ready once loadDatabase is called - this.executor = new Executor(); - if (this.inMemoryOnly) { this.executor.ready = true; } - - // Indexed by field name, dot notation can be used - // _id is always indexed and since _ids are generated randomly the underlying - // binary is always well-balanced - this.indexes = {}; - this.indexes._id = new Index({ fieldName: '_id', unique: true }); - this.ttlIndexes = {}; - - // Queue a load of the database right away and call the onload handler - // By default (no onload handler), if there is an error there, no operation will be possible so warn the user by throwing an exception - if (this.autoload) { this.loadDatabase(options.onload || function (err) { - if (err) { throw err; } - }); } -} + // Determine whether in memory or persistent + if (!filename || typeof filename !== 'string' || filename.length === 0) { + this.filename = null + this.inMemoryOnly = true + } else { + this.filename = filename + } -util.inherits(Datastore, require('events').EventEmitter); - - -/** - * Load the database from the datafile, and trigger the execution of buffered commands if any - */ -Datastore.prototype.loadDatabase = function () { - this.executor.push({ this: this.persistence, fn: this.persistence.loadDatabase, arguments: arguments }, true); -}; - - -/** - * Get an array of all the data in the database - */ -Datastore.prototype.getAllData = function () { - return this.indexes._id.getAll(); -}; - - -/** - * Reset all currently defined indexes - */ -Datastore.prototype.resetIndexes = function (newData) { - var self = this; - - Object.keys(this.indexes).forEach(function (i) { - self.indexes[i].reset(newData); - }); -}; - - -/** - * Ensure an index is kept for this field. Same parameters as lib/indexes - * For now this function is synchronous, we need to test how much time it takes - * We use an async API for consistency with the rest of the code - * @param {String} options.fieldName - * @param {Boolean} options.unique - * @param {Boolean} options.sparse - * @param {Number} options.expireAfterSeconds - Optional, if set this index becomes a TTL index (only works on Date fields, not arrays of Date) - * @param {Function} cb Optional callback, signature: err - */ -Datastore.prototype.ensureIndex = function (options, cb) { - var err - , callback = cb || function () {}; - - options = options || {}; - - if (!options.fieldName) { - err = new Error("Cannot create an index without a fieldName"); - err.missingFieldName = true; - return callback(err); + // String comparison function + this.compareStrings = options.compareStrings + + // Persistence handling + this.persistence = new Persistence({ + db: this, + nodeWebkitAppName: options.nodeWebkitAppName, + afterSerialization: options.afterSerialization, + beforeDeserialization: options.beforeDeserialization, + corruptAlertThreshold: options.corruptAlertThreshold + }) + + // This new executor is ready if we don't use persistence + // If we do, it will only be ready once loadDatabase is called + this.executor = new Executor() + if (this.inMemoryOnly) { this.executor.ready = true } + + // Indexed by field name, dot notation can be used + // _id is always indexed and since _ids are generated randomly the underlying + // binary is always well-balanced + this.indexes = {} + this.indexes._id = new Index({ fieldName: '_id', unique: true }) + this.ttlIndexes = {} + + // Queue a load of the database right away and call the onload handler + // By default (no onload handler), if there is an error there, no operation will be possible so warn the user by throwing an exception + if (this.autoload) { + this.loadDatabase(options.onload || function (err) { + if (err) { throw err } + }) + } } - if (this.indexes[options.fieldName]) { return callback(null); } - - this.indexes[options.fieldName] = new Index(options); - 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 - try { - this.indexes[options.fieldName].insert(this.getAllData()); - } catch (e) { - delete this.indexes[options.fieldName]; - return callback(e); + /** + * Load the database from the datafile, and trigger the execution of buffered commands if any + */ + loadDatabase () { + this.executor.push({ this: this.persistence, fn: this.persistence.loadDatabase, arguments: arguments }, true) } - // We may want to force all options to be persisted including defaults, not just the ones passed the index creation function - this.persistence.persistNewState([{ $$indexCreated: options }], function (err) { - if (err) { return callback(err); } - return callback(null); - }); -}; - - -/** - * Remove an index - * @param {String} fieldName - * @param {Function} cb Optional callback, signature: err - */ -Datastore.prototype.removeIndex = function (fieldName, cb) { - var callback = cb || function () {}; + /** + * Get an array of all the data in the database + */ + getAllData () { + return this.indexes._id.getAll() + } - delete this.indexes[fieldName]; + /** + * Reset all currently defined indexes + */ + resetIndexes (newData) { + const self = this - this.persistence.persistNewState([{ $$indexRemoved: fieldName }], function (err) { - if (err) { return callback(err); } - return callback(null); - }); -}; + Object.keys(this.indexes).forEach(function (i) { + self.indexes[i].reset(newData) + }) + } + /** + * Ensure an index is kept for this field. Same parameters as lib/indexes + * For now this function is synchronous, we need to test how much time it takes + * We use an async API for consistency with the rest of the code + * @param {String} options.fieldName + * @param {Boolean} options.unique + * @param {Boolean} options.sparse + * @param {Number} options.expireAfterSeconds - Optional, if set this index becomes a TTL index (only works on Date fields, not arrays of Date) + * @param {Function} cb Optional callback, signature: err + */ + ensureIndex (options, cb) { + let err + const callback = cb || function () {} + + options = options || {} + + if (!options.fieldName) { + err = new Error('Cannot create an index without a fieldName') + err.missingFieldName = true + return callback(err) + } + if (this.indexes[options.fieldName]) { return callback(null) } -/** - * Add one or several document(s) to all indexes - */ -Datastore.prototype.addToIndexes = function (doc) { - var i, failingIndex, error - , keys = Object.keys(this.indexes) - ; + this.indexes[options.fieldName] = new Index(options) + 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 - for (i = 0; i < keys.length; i += 1) { try { - this.indexes[keys[i]].insert(doc); + this.indexes[options.fieldName].insert(this.getAllData()) } catch (e) { - failingIndex = i; - error = e; - break; + delete this.indexes[options.fieldName] + return callback(e) } - } - // If an error happened, we need to rollback the insert on all other indexes - if (error) { - for (i = 0; i < failingIndex; i += 1) { - this.indexes[keys[i]].remove(doc); - } - - throw error; + // We may want to force all options to be persisted including defaults, not just the ones passed the index creation function + this.persistence.persistNewState([{ $$indexCreated: options }], function (err) { + if (err) { return callback(err) } + return callback(null) + }) } -}; + /** + * Remove an index + * @param {String} fieldName + * @param {Function} cb Optional callback, signature: err + */ + removeIndex (fieldName, cb) { + const callback = cb || function () {} -/** - * Remove one or several document(s) from all indexes - */ -Datastore.prototype.removeFromIndexes = function (doc) { - var self = this; + delete this.indexes[fieldName] - Object.keys(this.indexes).forEach(function (i) { - self.indexes[i].remove(doc); - }); -}; + this.persistence.persistNewState([{ $$indexRemoved: fieldName }], function (err) { + if (err) { return callback(err) } + return callback(null) + }) + } + /** + * Add one or several document(s) to all indexes + */ + addToIndexes (doc) { + let i + let failingIndex + let error + const keys = Object.keys(this.indexes) -/** - * Update one or several documents in all indexes - * To update multiple documents, oldDoc must be an array of { oldDoc, newDoc } pairs - * If one update violates a constraint, all changes are rolled back - */ -Datastore.prototype.updateIndexes = function (oldDoc, newDoc) { - var i, failingIndex, error - , keys = Object.keys(this.indexes) - ; + for (i = 0; i < keys.length; i += 1) { + try { + this.indexes[keys[i]].insert(doc) + } catch (e) { + failingIndex = i + error = e + break + } + } - for (i = 0; i < keys.length; i += 1) { - try { - this.indexes[keys[i]].update(oldDoc, newDoc); - } catch (e) { - failingIndex = i; - error = e; - break; + // If an error happened, we need to rollback the insert on all other indexes + if (error) { + for (i = 0; i < failingIndex; i += 1) { + this.indexes[keys[i]].remove(doc) + } + + throw error } } - // If an error happened, we need to rollback the update on all other indexes - if (error) { - for (i = 0; i < failingIndex; i += 1) { - this.indexes[keys[i]].revertUpdate(oldDoc, newDoc); - } + /** + * Remove one or several document(s) from all indexes + */ + removeFromIndexes (doc) { + const self = this - throw error; - } -}; - - -/** - * Return the list of candidates for a given query - * Crude implementation for now, we return the candidates given by the first usable index if any - * We try the following query types, in this order: basic match, $in match, comparison match - * One way to make it better would be to enable the use of multiple indexes if the first usable index - * returns too much data. I may do it in the future. - * - * Returned candidates will be scanned to find and remove all expired documents - * - * @param {Query} query - * @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 - * @param {Function} callback Signature err, candidates - */ -Datastore.prototype.getCandidates = function (query, dontExpireStaleDocs, callback) { - var indexNames = Object.keys(this.indexes) - , self = this - , usableQueryKeys; - - if (typeof dontExpireStaleDocs === 'function') { - callback = dontExpireStaleDocs; - dontExpireStaleDocs = false; + Object.keys(this.indexes).forEach(function (i) { + self.indexes[i].remove(doc) + }) } - - async.waterfall([ - // STEP 1: get candidates list by checking indexes from most to least frequent usecase - function (cb) { - // For a basic match - usableQueryKeys = []; - Object.keys(query).forEach(function (k) { - if (typeof query[k] === 'string' || typeof query[k] === 'number' || typeof query[k] === 'boolean' || util.isDate(query[k]) || query[k] === null) { - usableQueryKeys.push(k); + /** + * Update one or several documents in all indexes + * To update multiple documents, oldDoc must be an array of { oldDoc, newDoc } pairs + * If one update violates a constraint, all changes are rolled back + */ + updateIndexes (oldDoc, newDoc) { + let i + let failingIndex + let error + const keys = Object.keys(this.indexes) + + for (i = 0; i < keys.length; i += 1) { + try { + this.indexes[keys[i]].update(oldDoc, newDoc) + } catch (e) { + failingIndex = i + error = e + break } - }); - usableQueryKeys = _.intersection(usableQueryKeys, indexNames); - if (usableQueryKeys.length > 0) { - return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]])); } - // For a $in match - usableQueryKeys = []; - Object.keys(query).forEach(function (k) { - if (query[k] && query[k].hasOwnProperty('$in')) { - usableQueryKeys.push(k); + // If an error happened, we need to rollback the update on all other indexes + if (error) { + for (i = 0; i < failingIndex; i += 1) { + this.indexes[keys[i]].revertUpdate(oldDoc, newDoc) } - }); - usableQueryKeys = _.intersection(usableQueryKeys, indexNames); - if (usableQueryKeys.length > 0) { - return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]].$in)); - } - // For a comparison match - usableQueryKeys = []; - Object.keys(query).forEach(function (k) { - if (query[k] && (query[k].hasOwnProperty('$lt') || query[k].hasOwnProperty('$lte') || query[k].hasOwnProperty('$gt') || query[k].hasOwnProperty('$gte'))) { - usableQueryKeys.push(k); - } - }); - usableQueryKeys = _.intersection(usableQueryKeys, indexNames); - if (usableQueryKeys.length > 0) { - return cb(null, self.indexes[usableQueryKeys[0]].getBetweenBounds(query[usableQueryKeys[0]])); + throw error } - - // By default, return all the DB data - return cb(null, self.getAllData()); } - // STEP 2: remove all expired documents - , function (docs) { - if (dontExpireStaleDocs) { return callback(null, docs); } - var expiredDocsIds = [], validDocs = [], ttlIndexesFieldNames = Object.keys(self.ttlIndexes); + /** + * Return the list of candidates for a given query + * Crude implementation for now, we return the candidates given by the first usable index if any + * We try the following query types, in this order: basic match, $in match, comparison match + * One way to make it better would be to enable the use of multiple indexes if the first usable index + * returns too much data. I may do it in the future. + * + * Returned candidates will be scanned to find and remove all expired documents + * + * @param {Query} query + * @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 + * @param {Function} callback Signature err, candidates + */ + getCandidates (query, dontExpireStaleDocs, callback) { + const indexNames = Object.keys(this.indexes) + const self = this + let usableQueryKeys + + if (typeof dontExpireStaleDocs === 'function') { + callback = dontExpireStaleDocs + dontExpireStaleDocs = false + } - docs.forEach(function (doc) { - var valid = true; - ttlIndexesFieldNames.forEach(function (i) { - if (doc[i] !== undefined && util.isDate(doc[i]) && Date.now() > doc[i].getTime() + self.ttlIndexes[i] * 1000) { - valid = false; + async.waterfall([ + // STEP 1: get candidates list by checking indexes from most to least frequent usecase + function (cb) { + // For a basic match + usableQueryKeys = [] + Object.keys(query).forEach(function (k) { + if (typeof query[k] === 'string' || typeof query[k] === 'number' || typeof query[k] === 'boolean' || util.types.isDate(query[k]) || query[k] === null) { + usableQueryKeys.push(k) + } + }) + usableQueryKeys = _.intersection(usableQueryKeys, indexNames) + if (usableQueryKeys.length > 0) { + return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]])) } - }); - if (valid) { validDocs.push(doc); } else { expiredDocsIds.push(doc._id); } - }); - - async.eachSeries(expiredDocsIds, function (_id, cb) { - self._remove({ _id: _id }, {}, function (err) { - if (err) { return callback(err); } - return cb(); - }); - }, function (err) { - return callback(null, validDocs); - }); - }]); -}; - - -/** - * Insert a new document - * @param {Function} cb Optional callback, signature: err, insertedDoc - * - * @api private Use Datastore.insert which has the same signature - */ -Datastore.prototype._insert = function (newDoc, cb) { - var callback = cb || function () {} - , preparedDoc - ; - - try { - preparedDoc = this.prepareDocumentForInsertion(newDoc) - this._insertInCache(preparedDoc); - } catch (e) { - return callback(e); - } - this.persistence.persistNewState(util.isArray(preparedDoc) ? preparedDoc : [preparedDoc], function (err) { - if (err) { return callback(err); } - return callback(null, model.deepCopy(preparedDoc)); - }); -}; - -/** - * Create a new _id that's not already in use - */ -Datastore.prototype.createNewId = function () { - var tentativeId = customUtils.uid(16); - // 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) - if (this.indexes._id.getMatching(tentativeId).length > 0) { - tentativeId = this.createNewId(); - } - return tentativeId; -}; - -/** - * Prepare a document (or array of documents) to be inserted in a database - * Meaning adds _id and timestamps if necessary on a copy of newDoc to avoid any side effect on user input - * @api private - */ -Datastore.prototype.prepareDocumentForInsertion = function (newDoc) { - var preparedDoc, self = this; - - if (util.isArray(newDoc)) { - preparedDoc = []; - newDoc.forEach(function (doc) { preparedDoc.push(self.prepareDocumentForInsertion(doc)); }); - } else { - preparedDoc = model.deepCopy(newDoc); - if (preparedDoc._id === undefined) { preparedDoc._id = this.createNewId(); } - var now = new Date(); - if (this.timestampData && preparedDoc.createdAt === undefined) { preparedDoc.createdAt = now; } - if (this.timestampData && preparedDoc.updatedAt === undefined) { preparedDoc.updatedAt = now; } - model.checkObject(preparedDoc); - } + // For a $in match + usableQueryKeys = [] + Object.keys(query).forEach(function (k) { + if (query[k] && Object.prototype.hasOwnProperty.call(query[k], '$in')) { + usableQueryKeys.push(k) + } + }) + usableQueryKeys = _.intersection(usableQueryKeys, indexNames) + if (usableQueryKeys.length > 0) { + return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]].$in)) + } + + // For a comparison match + usableQueryKeys = [] + Object.keys(query).forEach(function (k) { + if (query[k] && (Object.prototype.hasOwnProperty.call(query[k], '$lt') || Object.prototype.hasOwnProperty.call(query[k], '$lte') || Object.prototype.hasOwnProperty.call(query[k], '$gt') || Object.prototype.hasOwnProperty.call(query[k], '$gte'))) { + usableQueryKeys.push(k) + } + }) + usableQueryKeys = _.intersection(usableQueryKeys, indexNames) + if (usableQueryKeys.length > 0) { + return cb(null, self.indexes[usableQueryKeys[0]].getBetweenBounds(query[usableQueryKeys[0]])) + } - return preparedDoc; -}; - -/** - * If newDoc is an array of documents, this will insert all documents in the cache - * @api private - */ -Datastore.prototype._insertInCache = function (preparedDoc) { - if (util.isArray(preparedDoc)) { - this._insertMultipleDocsInCache(preparedDoc); - } else { - this.addToIndexes(preparedDoc); + // By default, return all the DB data + return cb(null, self.getAllData()) + }, + // STEP 2: remove all expired documents + function (docs) { + if (dontExpireStaleDocs) { return callback(null, docs) } + + const expiredDocsIds = [] + const validDocs = [] + const ttlIndexesFieldNames = Object.keys(self.ttlIndexes) + + docs.forEach(function (doc) { + let valid = true + ttlIndexesFieldNames.forEach(function (i) { + if (doc[i] !== undefined && util.types.isDate(doc[i]) && Date.now() > doc[i].getTime() + self.ttlIndexes[i] * 1000) { + valid = false + } + }) + if (valid) { validDocs.push(doc) } else { expiredDocsIds.push(doc._id) } + }) + + async.eachSeries(expiredDocsIds, function (_id, cb) { + self._remove({ _id: _id }, {}, function (err) { + if (err) { return callback(err) } + return cb() + }) + // eslint-disable-next-line node/handle-callback-err + }, function (err) { + // TODO: handle error + return callback(null, validDocs) + }) + }]) } -}; -/** - * If one insertion fails (e.g. because of a unique constraint), roll back all previous - * inserts and throws the error - * @api private - */ -Datastore.prototype._insertMultipleDocsInCache = function (preparedDocs) { - var i, failingI, error; + /** + * Insert a new document + * @param {Function} cb Optional callback, signature: err, insertedDoc + * + * @api private Use Datastore.insert which has the same signature + */ + _insert (newDoc, cb) { + const callback = cb || function () {} + let preparedDoc - for (i = 0; i < preparedDocs.length; i += 1) { try { - this.addToIndexes(preparedDocs[i]); + preparedDoc = this.prepareDocumentForInsertion(newDoc) + this._insertInCache(preparedDoc) } catch (e) { - error = e; - failingI = i; - break; + return callback(e) } + + this.persistence.persistNewState(Array.isArray(preparedDoc) ? preparedDoc : [preparedDoc], function (err) { + if (err) { return callback(err) } + return callback(null, model.deepCopy(preparedDoc)) + }) } - if (error) { - for (i = 0; i < failingI; i += 1) { - this.removeFromIndexes(preparedDocs[i]); + /** + * Create a new _id that's not already in use + */ + createNewId () { + let tentativeId = customUtils.uid(16) + // 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) + if (this.indexes._id.getMatching(tentativeId).length > 0) { + tentativeId = this.createNewId() } - - throw error; + return tentativeId } -}; - -Datastore.prototype.insert = function () { - this.executor.push({ this: this, fn: this._insert, arguments: arguments }); -}; - - -/** - * Count all documents matching the query - * @param {Object} query MongoDB-style query - */ -Datastore.prototype.count = function(query, callback) { - var cursor = new Cursor(this, query, function(err, docs, callback) { - if (err) { return callback(err); } - return callback(null, docs.length); - }); - - if (typeof callback === 'function') { - cursor.exec(callback); - } else { - return cursor; + + /** + * Prepare a document (or array of documents) to be inserted in a database + * Meaning adds _id and timestamps if necessary on a copy of newDoc to avoid any side effect on user input + * @api private + */ + prepareDocumentForInsertion (newDoc) { + let preparedDoc + const self = this + + if (Array.isArray(newDoc)) { + preparedDoc = [] + newDoc.forEach(function (doc) { preparedDoc.push(self.prepareDocumentForInsertion(doc)) }) + } else { + preparedDoc = model.deepCopy(newDoc) + if (preparedDoc._id === undefined) { preparedDoc._id = this.createNewId() } + const now = new Date() + if (this.timestampData && preparedDoc.createdAt === undefined) { preparedDoc.createdAt = now } + if (this.timestampData && preparedDoc.updatedAt === undefined) { preparedDoc.updatedAt = now } + model.checkObject(preparedDoc) + } + + return preparedDoc } -}; - - -/** - * Find all documents matching the query - * If no callback is passed, we return the cursor so that user can limit, skip and finally exec - * @param {Object} query MongoDB-style query - * @param {Object} projection MongoDB-style projection - */ -Datastore.prototype.find = function (query, projection, callback) { - switch (arguments.length) { - case 1: - projection = {}; - // callback is undefined, will return a cursor - break; - case 2: - if (typeof projection === 'function') { - callback = projection; - projection = {}; - } // If not assume projection is an object and callback undefined - break; + + /** + * If newDoc is an array of documents, this will insert all documents in the cache + * @api private + */ + _insertInCache (preparedDoc) { + if (Array.isArray(preparedDoc)) { + this._insertMultipleDocsInCache(preparedDoc) + } else { + this.addToIndexes(preparedDoc) + } } - var cursor = new Cursor(this, query, function(err, docs, callback) { - var res = [], i; + /** + * If one insertion fails (e.g. because of a unique constraint), roll back all previous + * inserts and throws the error + * @api private + */ + _insertMultipleDocsInCache (preparedDocs) { + let i + let failingI + let error + + for (i = 0; i < preparedDocs.length; i += 1) { + try { + this.addToIndexes(preparedDocs[i]) + } catch (e) { + error = e + failingI = i + break + } + } - if (err) { return callback(err); } + if (error) { + for (i = 0; i < failingI; i += 1) { + this.removeFromIndexes(preparedDocs[i]) + } - for (i = 0; i < docs.length; i += 1) { - res.push(model.deepCopy(docs[i])); + throw error } - return callback(null, res); - }); - - cursor.projection(projection); - if (typeof callback === 'function') { - cursor.exec(callback); - } else { - return cursor; } -}; - - -/** - * Find one document matching the query - * @param {Object} query MongoDB-style query - * @param {Object} projection MongoDB-style projection - */ -Datastore.prototype.findOne = function (query, projection, callback) { - switch (arguments.length) { - case 1: - projection = {}; - // callback is undefined, will return a cursor - break; - case 2: - if (typeof projection === 'function') { - callback = projection; - projection = {}; - } // If not assume projection is an object and callback undefined - break; + + insert () { + this.executor.push({ this: this, fn: this._insert, arguments: arguments }) } - var cursor = new Cursor(this, query, function(err, docs, callback) { - if (err) { return callback(err); } - if (docs.length === 1) { - return callback(null, model.deepCopy(docs[0])); + /** + * Count all documents matching the query + * @param {Object} query MongoDB-style query + */ + count (query, callback) { + const cursor = new Cursor(this, query, function (err, docs, callback) { + if (err) { return callback(err) } + return callback(null, docs.length) + }) + + if (typeof callback === 'function') { + cursor.exec(callback) } else { - return callback(null, null); + return cursor } - }); + } + + /** + * Find all documents matching the query + * If no callback is passed, we return the cursor so that user can limit, skip and finally exec + * @param {Object} query MongoDB-style query + * @param {Object} projection MongoDB-style projection + */ + find (query, projection, callback) { + switch (arguments.length) { + case 1: + projection = {} + // callback is undefined, will return a cursor + break + case 2: + if (typeof projection === 'function') { + callback = projection + projection = {} + } // If not assume projection is an object and callback undefined + break + } + + const cursor = new Cursor(this, query, function (err, docs, callback) { + const res = [] + let i + + if (err) { return callback(err) } + + for (i = 0; i < docs.length; i += 1) { + res.push(model.deepCopy(docs[i])) + } + return callback(null, res) + }) - cursor.projection(projection).limit(1); - if (typeof callback === 'function') { - cursor.exec(callback); - } else { - return cursor; + cursor.projection(projection) + if (typeof callback === 'function') { + cursor.exec(callback) + } else { + return cursor + } } -}; - - -/** - * Update all docs matching query - * @param {Object} query - * @param {Object} updateQuery - * @param {Object} options Optional options - * options.multi If true, can update multiple documents (defaults to false) - * options.upsert If true, document is inserted if the query doesn't match anything - * options.returnUpdatedDocs Defaults to false, if true return as third argument the array of updated matched documents (even if no change actually took place) - * @param {Function} cb Optional callback, signature: (err, numAffected, affectedDocuments, upsert) - * If update was an upsert, upsert flag is set to true - * affectedDocuments can be one of the following: - * * For an upsert, the upserted document - * * For an update with returnUpdatedDocs option false, null - * * For an update with returnUpdatedDocs true and multi false, the updated document - * * For an update with returnUpdatedDocs true and multi true, the array of updated documents - * - * WARNING: The API was changed between v1.7.4 and v1.8, for consistency and readability reasons. Prior and including to v1.7.4, - * the callback signature was (err, numAffected, updated) where updated was the updated document in case of an upsert - * or the array of updated documents for an update if the returnUpdatedDocs option was true. That meant that the type of - * affectedDocuments in a non multi update depended on whether there was an upsert or not, leaving only two ways for the - * user to check whether an upsert had occured: checking the type of affectedDocuments or running another find query on - * the whole dataset to check its size. Both options being ugly, the breaking change was necessary. - * - * @api private Use Datastore.update which has the same signature - */ -Datastore.prototype._update = function (query, updateQuery, options, cb) { - var callback - , self = this - , numReplaced = 0 - , multi, upsert - , i - ; - - if (typeof options === 'function') { cb = options; options = {}; } - callback = cb || function () {}; - multi = options.multi !== undefined ? options.multi : false; - upsert = options.upsert !== undefined ? options.upsert : false; - - async.waterfall([ - function (cb) { // If upsert option is set, check whether we need to insert the doc - if (!upsert) { return cb(); } - - // Need to use an internal function not tied to the executor to avoid deadlock - var cursor = new Cursor(self, query); - cursor.limit(1)._exec(function (err, docs) { - if (err) { return callback(err); } + + /** + * Find one document matching the query + * @param {Object} query MongoDB-style query + * @param {Object} projection MongoDB-style projection + */ + findOne (query, projection, callback) { + switch (arguments.length) { + case 1: + projection = {} + // callback is undefined, will return a cursor + break + case 2: + if (typeof projection === 'function') { + callback = projection + projection = {} + } // If not assume projection is an object and callback undefined + break + } + + const cursor = new Cursor(this, query, function (err, docs, callback) { + if (err) { return callback(err) } if (docs.length === 1) { - return cb(); + return callback(null, model.deepCopy(docs[0])) } else { - var toBeInserted; - - try { - model.checkObject(updateQuery); - // updateQuery is a simple object with no modifier, use it as the document to insert - toBeInserted = updateQuery; - } catch (e) { - // updateQuery contains modifiers, use the find query as the base, - // strip it from all operators and update it according to updateQuery - try { - toBeInserted = model.modify(model.deepCopy(query, true), updateQuery); - } catch (err) { - return callback(err); - } - } - - return self._insert(toBeInserted, function (err, newDoc) { - if (err) { return callback(err); } - return callback(null, 1, newDoc, true); - }); + return callback(null, null) } - }); + }) + + cursor.projection(projection).limit(1) + if (typeof callback === 'function') { + cursor.exec(callback) + } else { + return cursor + } } - , function () { // Perform the update - var modifiedDoc , modifications = [], createdAt; - self.getCandidates(query, function (err, candidates) { - if (err) { return callback(err); } + /** + * Update all docs matching query + * @param {Object} query + * @param {Object} updateQuery + * @param {Object} options Optional options + * options.multi If true, can update multiple documents (defaults to false) + * options.upsert If true, document is inserted if the query doesn't match anything + * options.returnUpdatedDocs Defaults to false, if true return as third argument the array of updated matched documents (even if no change actually took place) + * @param {Function} cb Optional callback, signature: (err, numAffected, affectedDocuments, upsert) + * If update was an upsert, upsert flag is set to true + * affectedDocuments can be one of the following: + * * For an upsert, the upserted document + * * For an update with returnUpdatedDocs option false, null + * * For an update with returnUpdatedDocs true and multi false, the updated document + * * For an update with returnUpdatedDocs true and multi true, the array of updated documents + * + * WARNING: The API was changed between v1.7.4 and v1.8, for consistency and readability reasons. Prior and including to v1.7.4, + * the callback signature was (err, numAffected, updated) where updated was the updated document in case of an upsert + * or the array of updated documents for an update if the returnUpdatedDocs option was true. That meant that the type of + * affectedDocuments in a non multi update depended on whether there was an upsert or not, leaving only two ways for the + * user to check whether an upsert had occured: checking the type of affectedDocuments or running another find query on + * the whole dataset to check its size. Both options being ugly, the breaking change was necessary. + * + * @api private Use Datastore.update which has the same signature + */ + _update (query, updateQuery, options, cb) { + const self = this + let numReplaced = 0 + let i + + if (typeof options === 'function') { + cb = options + options = {} + } + const callback = cb || function () {} + const multi = options.multi !== undefined ? options.multi : false + const upsert = options.upsert !== undefined ? options.upsert : false + + async.waterfall([ + function (cb) { // If upsert option is set, check whether we need to insert the doc + if (!upsert) { return cb() } + + // Need to use an internal function not tied to the executor to avoid deadlock + const cursor = new Cursor(self, query) + cursor.limit(1)._exec(function (err, docs) { + if (err) { return callback(err) } + if (docs.length === 1) { + return cb() + } else { + let toBeInserted + + try { + model.checkObject(updateQuery) + // updateQuery is a simple object with no modifier, use it as the document to insert + toBeInserted = updateQuery + } catch (e) { + // updateQuery contains modifiers, use the find query as the base, + // strip it from all operators and update it according to updateQuery + try { + toBeInserted = model.modify(model.deepCopy(query, true), updateQuery) + } catch (err) { + return callback(err) + } + } - // Preparing update (if an error is thrown here neither the datafile nor - // the in-memory indexes are affected) - try { - for (i = 0; i < candidates.length; i += 1) { - if (model.match(candidates[i], query) && (multi || numReplaced === 0)) { - numReplaced += 1; - if (self.timestampData) { createdAt = candidates[i].createdAt; } - modifiedDoc = model.modify(candidates[i], updateQuery); - if (self.timestampData) { - modifiedDoc.createdAt = createdAt; - modifiedDoc.updatedAt = new Date(); + return self._insert(toBeInserted, function (err, newDoc) { + if (err) { return callback(err) } + return callback(null, 1, newDoc, true) + }) + } + }) + }, + function () { // Perform the update + let modifiedDoc + const modifications = [] + let createdAt + + self.getCandidates(query, function (err, candidates) { + if (err) { return callback(err) } + + // Preparing update (if an error is thrown here neither the datafile nor + // the in-memory indexes are affected) + try { + for (i = 0; i < candidates.length; i += 1) { + if (model.match(candidates[i], query) && (multi || numReplaced === 0)) { + numReplaced += 1 + if (self.timestampData) { createdAt = candidates[i].createdAt } + modifiedDoc = model.modify(candidates[i], updateQuery) + if (self.timestampData) { + modifiedDoc.createdAt = createdAt + modifiedDoc.updatedAt = new Date() + } + modifications.push({ oldDoc: candidates[i], newDoc: modifiedDoc }) + } } - modifications.push({ oldDoc: candidates[i], newDoc: modifiedDoc }); + } catch (err) { + return callback(err) } - } - } catch (err) { - return callback(err); - } - // Change the docs in memory - try { - self.updateIndexes(modifications); - } catch (err) { - return callback(err); - } + // Change the docs in memory + try { + self.updateIndexes(modifications) + } catch (err) { + return callback(err) + } - // Update the datafile - var updatedDocs = _.pluck(modifications, 'newDoc'); - self.persistence.persistNewState(updatedDocs, function (err) { - if (err) { return callback(err); } - if (!options.returnUpdatedDocs) { - return callback(null, numReplaced); - } else { - var updatedDocsDC = []; - updatedDocs.forEach(function (doc) { updatedDocsDC.push(model.deepCopy(doc)); }); - if (! multi) { updatedDocsDC = updatedDocsDC[0]; } - return callback(null, numReplaced, updatedDocsDC); - } - }); - }); - }]); -}; - -Datastore.prototype.update = function () { - this.executor.push({ this: this, fn: this._update, arguments: arguments }); -}; - - -/** - * Remove all docs matching the query - * For now very naive implementation (similar to update) - * @param {Object} query - * @param {Object} options Optional options - * options.multi If true, can update multiple documents (defaults to false) - * @param {Function} cb Optional callback, signature: err, numRemoved - * - * @api private Use Datastore.remove which has the same signature - */ -Datastore.prototype._remove = function (query, options, cb) { - var callback - , self = this, numRemoved = 0, removedDocs = [], multi - ; - - if (typeof options === 'function') { cb = options; options = {}; } - callback = cb || function () {}; - multi = options.multi !== undefined ? options.multi : false; - - this.getCandidates(query, true, function (err, candidates) { - if (err) { return callback(err); } + // Update the datafile + const updatedDocs = _.pluck(modifications, 'newDoc') + self.persistence.persistNewState(updatedDocs, function (err) { + if (err) { return callback(err) } + if (!options.returnUpdatedDocs) { + return callback(null, numReplaced) + } else { + let updatedDocsDC = [] + updatedDocs.forEach(function (doc) { updatedDocsDC.push(model.deepCopy(doc)) }) + if (!multi) { updatedDocsDC = updatedDocsDC[0] } + return callback(null, numReplaced, updatedDocsDC) + } + }) + }) + }]) + } - try { - candidates.forEach(function (d) { - if (model.match(d, query) && (multi || numRemoved === 0)) { - numRemoved += 1; - removedDocs.push({ $$deleted: true, _id: d._id }); - self.removeFromIndexes(d); - } - }); - } catch (err) { return callback(err); } + update () { + this.executor.push({ this: this, fn: this._update, arguments: arguments }) + } - self.persistence.persistNewState(removedDocs, function (err) { - if (err) { return callback(err); } - return callback(null, numRemoved); - }); - }); -}; + /** + * Remove all docs matching the query + * For now very naive implementation (similar to update) + * @param {Object} query + * @param {Object} options Optional options + * options.multi If true, can update multiple documents (defaults to false) + * @param {Function} cb Optional callback, signature: err, numRemoved + * + * @api private Use Datastore.remove which has the same signature + */ + _remove (query, options, cb) { + const self = this + let numRemoved = 0 + const removedDocs = [] + + if (typeof options === 'function') { + cb = options + options = {} + } + const callback = cb || function () {} + const multi = options.multi !== undefined ? options.multi : false -Datastore.prototype.remove = function () { - this.executor.push({ this: this, fn: this._remove, arguments: arguments }); -}; + this.getCandidates(query, true, function (err, candidates) { + if (err) { return callback(err) } + try { + candidates.forEach(function (d) { + if (model.match(d, query) && (multi || numRemoved === 0)) { + numRemoved += 1 + removedDocs.push({ $$deleted: true, _id: d._id }) + self.removeFromIndexes(d) + } + }) + } catch (err) { return callback(err) } + + self.persistence.persistNewState(removedDocs, function (err) { + if (err) { return callback(err) } + return callback(null, numRemoved) + }) + }) + } + + remove () { + this.executor.push({ this: this, fn: this._remove, arguments: arguments }) + } +} +util.inherits(Datastore, require('events').EventEmitter) -module.exports = Datastore; +module.exports = Datastore diff --git a/lib/executor.js b/lib/executor.js index 979ca7d..7e47e17 100755 --- a/lib/executor.js +++ b/lib/executor.js @@ -1,78 +1,73 @@ /** * Responsible for sequentially executing actions on the database */ +const async = require('async') -var async = require('async') - ; +class Executor { + constructor () { + this.buffer = [] + this.ready = false -function Executor () { - this.buffer = []; - this.ready = false; + // This queue will execute all commands, one-by-one in order + this.queue = async.queue(function (task, cb) { + const newArguments = [] - // This queue will execute all commands, one-by-one in order - this.queue = async.queue(function (task, cb) { - var newArguments = []; + // task.arguments is an array-like object on which adding a new field doesn't work, so we transform it into a real array + for (let i = 0; i < task.arguments.length; i += 1) { newArguments.push(task.arguments[i]) } + const lastArg = task.arguments[task.arguments.length - 1] - // task.arguments is an array-like object on which adding a new field doesn't work, so we transform it into a real array - for (var i = 0; i < task.arguments.length; i += 1) { newArguments.push(task.arguments[i]); } - var lastArg = task.arguments[task.arguments.length - 1]; - - // Always tell the queue task is complete. Execute callback if any was given. - if (typeof lastArg === 'function') { - // Callback was supplied - newArguments[newArguments.length - 1] = function () { - if (typeof setImmediate === 'function') { - setImmediate(cb); - } else { - process.nextTick(cb); + // Always tell the queue task is complete. Execute callback if any was given. + if (typeof lastArg === 'function') { + // Callback was supplied + newArguments[newArguments.length - 1] = function () { + if (typeof setImmediate === 'function') { + setImmediate(cb) + } else { + process.nextTick(cb) + } + lastArg.apply(null, arguments) } - lastArg.apply(null, arguments); - }; - } else if (!lastArg && task.arguments.length !== 0) { - // false/undefined/null supplied as callbback - newArguments[newArguments.length - 1] = function () { cb(); }; - } else { - // Nothing supplied as callback - newArguments.push(function () { cb(); }); - } + } else if (!lastArg && task.arguments.length !== 0) { + // false/undefined/null supplied as callback + newArguments[newArguments.length - 1] = function () { cb() } + } else { + // Nothing supplied as callback + newArguments.push(function () { cb() }) + } - - task.fn.apply(task.this, newArguments); - }, 1); -} - - -/** - * If executor is ready, queue task (and process it immediately if executor was idle) - * If not, buffer task for later processing - * @param {Object} task - * task.this - Object to use as this - * task.fn - Function to execute - * task.arguments - Array of arguments, IMPORTANT: only the last argument may be a function (the callback) - * and the last argument cannot be false/undefined/null - * @param {Boolean} forceQueuing Optional (defaults to false) force executor to queue task even if it is not ready - */ -Executor.prototype.push = function (task, forceQueuing) { - if (this.ready || forceQueuing) { - this.queue.push(task); - } else { - this.buffer.push(task); + task.fn.apply(task.this, newArguments) + }, 1) } -}; - - -/** - * Queue all tasks in buffer (in the same order they came in) - * Automatically sets executor as ready - */ -Executor.prototype.processBuffer = function () { - var i; - this.ready = true; - for (i = 0; i < this.buffer.length; i += 1) { this.queue.push(this.buffer[i]); } - this.buffer = []; -}; + /** + * If executor is ready, queue task (and process it immediately if executor was idle) + * If not, buffer task for later processing + * @param {Object} task + * task.this - Object to use as this + * task.fn - Function to execute + * task.arguments - Array of arguments, IMPORTANT: only the last argument may be a function (the callback) + * and the last argument cannot be false/undefined/null + * @param {Boolean} forceQueuing Optional (defaults to false) force executor to queue task even if it is not ready + */ + push (task, forceQueuing) { + if (this.ready || forceQueuing) { + this.queue.push(task) + } else { + this.buffer.push(task) + } + } + /** + * Queue all tasks in buffer (in the same order they came in) + * Automatically sets executor as ready + */ + processBuffer () { + let i + this.ready = true + for (i = 0; i < this.buffer.length; i += 1) { this.queue.push(this.buffer[i]) } + this.buffer = [] + } +} // Interface -module.exports = Executor; +module.exports = Executor diff --git a/lib/indexes.js b/lib/indexes.js index 391837b..7901ce8 100755 --- a/lib/indexes.js +++ b/lib/indexes.js @@ -1,294 +1,295 @@ -var BinarySearchTree = require('binary-search-tree').AVLTree - , model = require('./model') - , _ = require('underscore') - , util = require('util') - ; +const BinarySearchTree = require('@seald-io/binary-search-tree').BinarySearchTree +const model = require('./model') +const _ = require('underscore') /** * Two indexed pointers are equal iif they point to the same place */ function checkValueEquality (a, b) { - return a === b; + return a === b } /** * Type-aware projection */ function projectForUnique (elt) { - if (elt === null) { return '$null'; } - if (typeof elt === 'string') { return '$string' + elt; } - if (typeof elt === 'boolean') { return '$boolean' + elt; } - if (typeof elt === 'number') { return '$number' + elt; } - if (util.isArray(elt)) { return '$date' + elt.getTime(); } + if (elt === null) { return '$null' } + if (typeof elt === 'string') { return '$string' + elt } + if (typeof elt === 'boolean') { return '$boolean' + elt } + if (typeof elt === 'number') { return '$number' + elt } + if (Array.isArray(elt)) { return '$date' + elt.getTime() } - return elt; // Arrays and objects, will check for pointer equality + return elt // Arrays and objects, will check for pointer equality } +class Index { + /** + * Create a new index + * All methods on an index guarantee that either the whole operation was successful and the index changed + * or the operation was unsuccessful and an error is thrown while the index is unchanged + * @param {String} options.fieldName On which field should the index apply (can use dot notation to index on sub fields) + * @param {Boolean} options.unique Optional, enforce a unique constraint (default: false) + * @param {Boolean} options.sparse Optional, allow a sparse index (we can have documents for which fieldName is undefined) (default: false) + */ + constructor (options) { + this.fieldName = options.fieldName + this.unique = options.unique || false + this.sparse = options.sparse || false + + this.treeOptions = { unique: this.unique, compareKeys: model.compareThings, checkValueEquality: checkValueEquality } + + this.reset() // No data in the beginning + } -/** - * Create a new index - * All methods on an index guarantee that either the whole operation was successful and the index changed - * or the operation was unsuccessful and an error is thrown while the index is unchanged - * @param {String} options.fieldName On which field should the index apply (can use dot notation to index on sub fields) - * @param {Boolean} options.unique Optional, enforce a unique constraint (default: false) - * @param {Boolean} options.sparse Optional, allow a sparse index (we can have documents for which fieldName is undefined) (default: false) - */ -function Index (options) { - this.fieldName = options.fieldName; - this.unique = options.unique || false; - this.sparse = options.sparse || false; - - this.treeOptions = { unique: this.unique, compareKeys: model.compareThings, checkValueEquality: checkValueEquality }; - - this.reset(); // No data in the beginning -} - - -/** - * Reset an index - * @param {Document or Array of documents} newData Optional, data to initialize the index with - * If an error is thrown during insertion, the index is not modified - */ -Index.prototype.reset = function (newData) { - this.tree = new BinarySearchTree(this.treeOptions); - - if (newData) { this.insert(newData); } -}; - + /** + * Reset an index + * @param {Document or Array of documents} newData Optional, data to initialize the index with + * If an error is thrown during insertion, the index is not modified + */ + reset (newData) { + this.tree = new BinarySearchTree(this.treeOptions) -/** - * Insert a new document in the index - * If an array is passed, we insert all its elements (if one insertion fails the index is not modified) - * O(log(n)) - */ -Index.prototype.insert = function (doc) { - var key, self = this - , keys, i, failingI, error - ; + if (newData) { this.insert(newData) } + } - if (util.isArray(doc)) { this.insertMultipleDocs(doc); return; } + /** + * Insert a new document in the index + * If an array is passed, we insert all its elements (if one insertion fails the index is not modified) + * O(log(n)) + */ + insert (doc) { + let keys + let i + let failingI + let error + + if (Array.isArray(doc)) { + this.insertMultipleDocs(doc) + return + } - key = model.getDotValue(doc, this.fieldName); + const key = model.getDotValue(doc, this.fieldName) + + // We don't index documents that don't contain the field if the index is sparse + if (key === undefined && this.sparse) { return } + + if (!Array.isArray(key)) { + this.tree.insert(key, doc) + } else { + // If an insert fails due to a unique constraint, roll back all inserts before it + keys = _.uniq(key, projectForUnique) + + for (i = 0; i < keys.length; i += 1) { + try { + this.tree.insert(keys[i], doc) + } catch (e) { + error = e + failingI = i + break + } + } - // We don't index documents that don't contain the field if the index is sparse - if (key === undefined && this.sparse) { return; } + if (error) { + for (i = 0; i < failingI; i += 1) { + this.tree.delete(keys[i], doc) + } - if (!util.isArray(key)) { - this.tree.insert(key, doc); - } else { - // If an insert fails due to a unique constraint, roll back all inserts before it - keys = _.uniq(key, projectForUnique); + throw error + } + } + } - for (i = 0; i < keys.length; i += 1) { + /** + * Insert an array of documents in the index + * If a constraint is violated, the changes should be rolled back and an error thrown + * + * @API private + */ + insertMultipleDocs (docs) { + let i + let error + let failingI + + for (i = 0; i < docs.length; i += 1) { try { - this.tree.insert(keys[i], doc); + this.insert(docs[i]) } catch (e) { - error = e; - failingI = i; - break; + error = e + failingI = i + break } } if (error) { for (i = 0; i < failingI; i += 1) { - this.tree.delete(keys[i], doc); + this.remove(docs[i]) } - throw error; + throw error } } -}; - -/** - * Insert an array of documents in the index - * If a constraint is violated, the changes should be rolled back and an error thrown - * - * @API private - */ -Index.prototype.insertMultipleDocs = function (docs) { - var i, error, failingI; - - for (i = 0; i < docs.length; i += 1) { - try { - this.insert(docs[i]); - } catch (e) { - error = e; - failingI = i; - break; + /** + * Remove a document from the index + * If an array is passed, we remove all its elements + * The remove operation is safe with regards to the 'unique' constraint + * O(log(n)) + */ + remove (doc) { + const self = this + + if (Array.isArray(doc)) { + doc.forEach(function (d) { self.remove(d) }) + return } - } - if (error) { - for (i = 0; i < failingI; i += 1) { - this.remove(docs[i]); - } - - throw error; - } -}; - - -/** - * Remove a document from the index - * If an array is passed, we remove all its elements - * The remove operation is safe with regards to the 'unique' constraint - * O(log(n)) - */ -Index.prototype.remove = function (doc) { - var key, self = this; - - if (util.isArray(doc)) { doc.forEach(function (d) { self.remove(d); }); return; } - - key = model.getDotValue(doc, this.fieldName); - - if (key === undefined && this.sparse) { return; } - - if (!util.isArray(key)) { - this.tree.delete(key, doc); - } else { - _.uniq(key, projectForUnique).forEach(function (_key) { - self.tree.delete(_key, doc); - }); - } -}; - - -/** - * Update a document in the index - * If a constraint is violated, changes are rolled back and an error thrown - * Naive implementation, still in O(log(n)) - */ -Index.prototype.update = function (oldDoc, newDoc) { - if (util.isArray(oldDoc)) { this.updateMultipleDocs(oldDoc); return; } + const key = model.getDotValue(doc, this.fieldName) - this.remove(oldDoc); + if (key === undefined && this.sparse) { return } - try { - this.insert(newDoc); - } catch (e) { - this.insert(oldDoc); - throw e; + if (!Array.isArray(key)) { + this.tree.delete(key, doc) + } else { + _.uniq(key, projectForUnique).forEach(function (_key) { + self.tree.delete(_key, doc) + }) + } } -}; - -/** - * Update multiple documents in the index - * If a constraint is violated, the changes need to be rolled back - * and an error thrown - * @param {Array of oldDoc, newDoc pairs} pairs - * - * @API private - */ -Index.prototype.updateMultipleDocs = function (pairs) { - var i, failingI, error; + /** + * Update a document in the index + * If a constraint is violated, changes are rolled back and an error thrown + * Naive implementation, still in O(log(n)) + */ + update (oldDoc, newDoc) { + if (Array.isArray(oldDoc)) { + this.updateMultipleDocs(oldDoc) + return + } - for (i = 0; i < pairs.length; i += 1) { - this.remove(pairs[i].oldDoc); - } + this.remove(oldDoc) - for (i = 0; i < pairs.length; i += 1) { try { - this.insert(pairs[i].newDoc); + this.insert(newDoc) } catch (e) { - error = e; - failingI = i; - break; + this.insert(oldDoc) + throw e } } - // If an error was raised, roll back changes in the inverse order - if (error) { - for (i = 0; i < failingI; i += 1) { - this.remove(pairs[i].newDoc); + /** + * Update multiple documents in the index + * If a constraint is violated, the changes need to be rolled back + * and an error thrown + * @param {Array of oldDoc, newDoc pairs} pairs + * + * @API private + */ + updateMultipleDocs (pairs) { + let i + let failingI + let error + + for (i = 0; i < pairs.length; i += 1) { + this.remove(pairs[i].oldDoc) } for (i = 0; i < pairs.length; i += 1) { - this.insert(pairs[i].oldDoc); + try { + this.insert(pairs[i].newDoc) + } catch (e) { + error = e + failingI = i + break + } } - throw error; - } -}; + // If an error was raised, roll back changes in the inverse order + if (error) { + for (i = 0; i < failingI; i += 1) { + this.remove(pairs[i].newDoc) + } + for (i = 0; i < pairs.length; i += 1) { + this.insert(pairs[i].oldDoc) + } -/** - * Revert an update - */ -Index.prototype.revertUpdate = function (oldDoc, newDoc) { - var revert = []; - - if (!util.isArray(oldDoc)) { - this.update(newDoc, oldDoc); - } else { - oldDoc.forEach(function (pair) { - revert.push({ oldDoc: pair.newDoc, newDoc: pair.oldDoc }); - }); - this.update(revert); + throw error + } } -}; - - -/** - * 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) - * @param {Thing} value Value to match the key against - * @return {Array of documents} - */ -Index.prototype.getMatching = function (value) { - var self = this; - - if (!util.isArray(value)) { - return self.tree.search(value); - } else { - var _res = {}, res = []; - value.forEach(function (v) { - self.getMatching(v).forEach(function (doc) { - _res[doc._id] = doc; - }); - }); - - Object.keys(_res).forEach(function (_id) { - res.push(_res[_id]); - }); - - return res; + /** + * Revert an update + */ + revertUpdate (oldDoc, newDoc) { + const revert = [] + + if (!Array.isArray(oldDoc)) { + this.update(newDoc, oldDoc) + } else { + oldDoc.forEach(function (pair) { + revert.push({ oldDoc: pair.newDoc, newDoc: pair.oldDoc }) + }) + this.update(revert) + } } -}; - - -/** - * Get all documents in index whose key is between bounds are they are defined by query - * Documents are sorted by key - * @param {Query} query - * @return {Array of documents} - */ -Index.prototype.getBetweenBounds = function (query) { - return this.tree.betweenBounds(query); -}; - -/** - * Get all elements in the index - * @return {Array of documents} - */ -Index.prototype.getAll = function () { - var res = []; - - this.tree.executeOnEveryNode(function (node) { - var i; - - for (i = 0; i < node.data.length; i += 1) { - res.push(node.data[i]); + /** + * 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) + * @param {Thing} value Value to match the key against + * @return {Array of documents} + */ + getMatching (value) { + const self = this + + if (!Array.isArray(value)) { + return self.tree.search(value) + } else { + const _res = {} + const res = [] + + value.forEach(function (v) { + self.getMatching(v).forEach(function (doc) { + _res[doc._id] = doc + }) + }) + + Object.keys(_res).forEach(function (_id) { + res.push(_res[_id]) + }) + + return res } - }); + } - return res; -}; + /** + * Get all documents in index whose key is between bounds are they are defined by query + * Documents are sorted by key + * @param {Query} query + * @return {Array of documents} + */ + getBetweenBounds (query) { + return this.tree.betweenBounds(query) + } + + /** + * Get all elements in the index + * @return {Array of documents} + */ + getAll () { + const res = [] + this.tree.executeOnEveryNode(function (node) { + let i + for (i = 0; i < node.data.length; i += 1) { + res.push(node.data[i]) + } + }) + return res + } +} // Interface -module.exports = Index; +module.exports = Index diff --git a/lib/model.js b/lib/model.js index 0aa9e92..99a84b3 100755 --- a/lib/model.js +++ b/lib/model.js @@ -4,16 +4,13 @@ * Copying * Querying, update */ - -var util = require('util') - , _ = require('underscore') - , modifierFunctions = {} - , lastStepModifierFunctions = {} - , comparisonFunctions = {} - , logicalOperators = {} - , arrayComparisonFunctions = {} - ; - +const util = require('util') +const _ = require('underscore') +const modifierFunctions = {} +const lastStepModifierFunctions = {} +const comparisonFunctions = {} +const logicalOperators = {} +const arrayComparisonFunctions = {} /** * Check a key, throw an error if the key is non valid @@ -25,39 +22,37 @@ var util = require('util') */ function checkKey (k, v) { if (typeof k === 'number') { - k = k.toString(); + k = k.toString() } if (k[0] === '$' && !(k === '$$date' && typeof v === 'number') && !(k === '$$deleted' && v === true) && !(k === '$$indexCreated') && !(k === '$$indexRemoved')) { - throw new Error('Field names cannot begin with the $ character'); + throw new Error('Field names cannot begin with the $ character') } if (k.indexOf('.') !== -1) { - throw new Error('Field names cannot contain a .'); + throw new Error('Field names cannot contain a .') } } - /** * Check a DB object and throw an error if it's not valid * Works by applying the above checkKey function to all fields recursively */ function checkObject (obj) { - if (util.isArray(obj)) { + if (Array.isArray(obj)) { obj.forEach(function (o) { - checkObject(o); - }); + checkObject(o) + }) } if (typeof obj === 'object' && obj !== null) { Object.keys(obj).forEach(function (k) { - checkKey(k, obj[k]); - checkObject(obj[k]); - }); + checkKey(k, obj[k]) + checkObject(obj[k]) + }) } } - /** * Serialize an object to be persisted to a one-line string * For serialization/deserialization, we use the native JSON parser and not eval or Function @@ -67,115 +62,109 @@ function checkObject (obj) { * Accepted secondary types: Objects, Arrays */ function serialize (obj) { - var res; + const res = JSON.stringify(obj, function (k, v) { + checkKey(k, v) - res = JSON.stringify(obj, function (k, v) { - checkKey(k, v); - - if (v === undefined) { return undefined; } - if (v === null) { return null; } + if (v === undefined) { return undefined } + if (v === null) { return null } // Hackish way of checking if object is Date (this way it works between execution contexts in node-webkit). // 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 - if (typeof this[k].getTime === 'function') { return { $$date: this[k].getTime() }; } + if (typeof this[k].getTime === 'function') { return { $$date: this[k].getTime() } } - return v; - }); + return v + }) - return res; + return res } - /** * From a one-line representation of an object generate by the serialize function * Return the object itself */ function deserialize (rawData) { return JSON.parse(rawData, function (k, v) { - if (k === '$$date') { return new Date(v); } - if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null) { return v; } - if (v && v.$$date) { return v.$$date; } + if (k === '$$date') { return new Date(v) } + if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null) { return v } + if (v && v.$$date) { return v.$$date } - return v; - }); + return v + }) } - /** * Deep copy a DB object * The optional strictKeys flag (defaulting to false) indicates whether to copy everything or only fields * where the keys are valid, i.e. don't begin with $ and don't contain a . */ function deepCopy (obj, strictKeys) { - var res; + let res - if ( typeof obj === 'boolean' || - typeof obj === 'number' || - typeof obj === 'string' || - obj === null || - (util.isDate(obj)) ) { - return obj; + if (typeof obj === 'boolean' || + typeof obj === 'number' || + typeof obj === 'string' || + obj === null || + (util.types.isDate(obj))) { + return obj } - if (util.isArray(obj)) { - res = []; - obj.forEach(function (o) { res.push(deepCopy(o, strictKeys)); }); - return res; + if (Array.isArray(obj)) { + res = [] + obj.forEach(function (o) { res.push(deepCopy(o, strictKeys)) }) + return res } if (typeof obj === 'object') { - res = {}; + res = {} Object.keys(obj).forEach(function (k) { if (!strictKeys || (k[0] !== '$' && k.indexOf('.') === -1)) { - res[k] = deepCopy(obj[k], strictKeys); + res[k] = deepCopy(obj[k], strictKeys) } - }); - return res; + }) + return res } - return undefined; // For now everything else is undefined. We should probably throw an error instead + return undefined // For now everything else is undefined. We should probably throw an error instead } - /** * Tells if an object is a primitive type or a "real" object * Arrays are considered primitive */ function isPrimitiveType (obj) { - return ( typeof obj === 'boolean' || - typeof obj === 'number' || - typeof obj === 'string' || - obj === null || - util.isDate(obj) || - util.isArray(obj)); + return (typeof obj === 'boolean' || + typeof obj === 'number' || + typeof obj === 'string' || + obj === null || + util.types.isDate(obj) || + Array.isArray(obj)) } - /** * Utility functions for comparing things * Assumes type checking was already done (a and b already have the same type) * compareNSB works for numbers, strings and booleans */ function compareNSB (a, b) { - if (a < b) { return -1; } - if (a > b) { return 1; } - return 0; + if (a < b) { return -1 } + if (a > b) { return 1 } + return 0 } function compareArrays (a, b) { - var i, comp; + let i + let comp for (i = 0; i < Math.min(a.length, b.length); i += 1) { - comp = compareThings(a[i], b[i]); + comp = compareThings(a[i], b[i]) - if (comp !== 0) { return comp; } + if (comp !== 0) { return comp } } // Common section was identical, longest one wins - return compareNSB(a.length, b.length); + return compareNSB(a.length, b.length) } - /** * Compare { things U undefined } * Things are defined as any native types (string, number, boolean, null, date) and objects @@ -187,52 +176,51 @@ function compareArrays (a, b) { * @param {Function} _compareStrings String comparing function, returning -1, 0 or 1, overriding default string comparison (useful for languages with accented letters) */ function compareThings (a, b, _compareStrings) { - var aKeys, bKeys, comp, i - , compareStrings = _compareStrings || compareNSB; + let comp + let i + const compareStrings = _compareStrings || compareNSB // undefined - if (a === undefined) { return b === undefined ? 0 : -1; } - if (b === undefined) { return a === undefined ? 0 : 1; } + if (a === undefined) { return b === undefined ? 0 : -1 } + if (b === undefined) { return a === undefined ? 0 : 1 } // null - if (a === null) { return b === null ? 0 : -1; } - if (b === null) { return a === null ? 0 : 1; } + if (a === null) { return b === null ? 0 : -1 } + if (b === null) { return a === null ? 0 : 1 } // Numbers - if (typeof a === 'number') { return typeof b === 'number' ? compareNSB(a, b) : -1; } - if (typeof b === 'number') { return typeof a === 'number' ? compareNSB(a, b) : 1; } + if (typeof a === 'number') { return typeof b === 'number' ? compareNSB(a, b) : -1 } + if (typeof b === 'number') { return typeof a === 'number' ? compareNSB(a, b) : 1 } // Strings - if (typeof a === 'string') { return typeof b === 'string' ? compareStrings(a, b) : -1; } - if (typeof b === 'string') { return typeof a === 'string' ? compareStrings(a, b) : 1; } + if (typeof a === 'string') { return typeof b === 'string' ? compareStrings(a, b) : -1 } + if (typeof b === 'string') { return typeof a === 'string' ? compareStrings(a, b) : 1 } // Booleans - if (typeof a === 'boolean') { return typeof b === 'boolean' ? compareNSB(a, b) : -1; } - if (typeof b === 'boolean') { return typeof a === 'boolean' ? compareNSB(a, b) : 1; } + if (typeof a === 'boolean') { return typeof b === 'boolean' ? compareNSB(a, b) : -1 } + if (typeof b === 'boolean') { return typeof a === 'boolean' ? compareNSB(a, b) : 1 } // Dates - if (util.isDate(a)) { return util.isDate(b) ? compareNSB(a.getTime(), b.getTime()) : -1; } - if (util.isDate(b)) { return util.isDate(a) ? compareNSB(a.getTime(), b.getTime()) : 1; } + if (util.types.isDate(a)) { return util.types.isDate(b) ? compareNSB(a.getTime(), b.getTime()) : -1 } + if (util.types.isDate(b)) { return util.types.isDate(a) ? compareNSB(a.getTime(), b.getTime()) : 1 } // Arrays (first element is most significant and so on) - if (util.isArray(a)) { return util.isArray(b) ? compareArrays(a, b) : -1; } - if (util.isArray(b)) { return util.isArray(a) ? compareArrays(a, b) : 1; } + if (Array.isArray(a)) { return Array.isArray(b) ? compareArrays(a, b) : -1 } + if (Array.isArray(b)) { return Array.isArray(a) ? compareArrays(a, b) : 1 } // Objects - aKeys = Object.keys(a).sort(); - bKeys = Object.keys(b).sort(); + const aKeys = Object.keys(a).sort() + const bKeys = Object.keys(b).sort() for (i = 0; i < Math.min(aKeys.length, bKeys.length); i += 1) { - comp = compareThings(a[aKeys[i]], b[bKeys[i]]); + comp = compareThings(a[aKeys[i]], b[bKeys[i]]) - if (comp !== 0) { return comp; } + if (comp !== 0) { return comp } } - return compareNSB(aKeys.length, bKeys.length); + return compareNSB(aKeys.length, bKeys.length) } - - // ============================================================== // Updating documents // ============================================================== @@ -250,17 +238,15 @@ function compareThings (a, b, _compareStrings) { * Set a field to a new value */ lastStepModifierFunctions.$set = function (obj, field, value) { - obj[field] = value; -}; - + obj[field] = value +} /** * Unset a field */ lastStepModifierFunctions.$unset = function (obj, field, value) { - delete obj[field]; -}; - + delete obj[field] +} /** * Push an element to the end of an array field @@ -270,42 +256,43 @@ lastStepModifierFunctions.$unset = function (obj, field, value) { */ lastStepModifierFunctions.$push = function (obj, field, value) { // Create the array if it doesn't exist - if (!obj.hasOwnProperty(field)) { obj[field] = []; } + if (!Object.prototype.hasOwnProperty.call(obj, field)) { obj[field] = [] } - if (!util.isArray(obj[field])) { throw new Error("Can't $push an element on non-array values"); } + if (!Array.isArray(obj[field])) { throw new Error('Can\'t $push an element on non-array values') } if (value !== null && typeof value === 'object' && value.$slice && value.$each === undefined) { - value.$each = []; + value.$each = [] } if (value !== null && typeof value === 'object' && value.$each) { - 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"); } - if (!util.isArray(value.$each)) { throw new Error("$each requires an array value"); } + 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') } + if (!Array.isArray(value.$each)) { throw new Error('$each requires an array value') } value.$each.forEach(function (v) { - obj[field].push(v); - }); + obj[field].push(v) + }) - if (value.$slice === undefined || typeof value.$slice !== 'number') { return; } + if (value.$slice === undefined || typeof value.$slice !== 'number') { return } if (value.$slice === 0) { - obj[field] = []; + obj[field] = [] } else { - var start, end, n = obj[field].length; + let start + let end + const n = obj[field].length if (value.$slice < 0) { - start = Math.max(0, n + value.$slice); - end = n; + start = Math.max(0, n + value.$slice) + end = n } else if (value.$slice > 0) { - start = 0; - end = Math.min(n, value.$slice); + start = 0 + end = Math.min(n, value.$slice) } - obj[field] = obj[field].slice(start, end); + obj[field] = obj[field].slice(start, end) } } else { - obj[field].push(value); + obj[field].push(value) } -}; - +} /** * Add an element to an array field only if it is not already in it @@ -313,173 +300,164 @@ lastStepModifierFunctions.$push = function (obj, field, value) { * Note that it doesn't check whether the original array contains duplicates */ lastStepModifierFunctions.$addToSet = function (obj, field, value) { - var addToSet = true; + let addToSet = true // Create the array if it doesn't exist - if (!obj.hasOwnProperty(field)) { obj[field] = []; } + if (!Object.prototype.hasOwnProperty.call(obj, field)) { obj[field] = [] } - if (!util.isArray(obj[field])) { throw new Error("Can't $addToSet an element on non-array values"); } + if (!Array.isArray(obj[field])) { throw new Error('Can\'t $addToSet an element on non-array values') } if (value !== null && typeof value === 'object' && value.$each) { - if (Object.keys(value).length > 1) { throw new Error("Can't use another field in conjunction with $each"); } - if (!util.isArray(value.$each)) { throw new Error("$each requires an array value"); } + if (Object.keys(value).length > 1) { throw new Error('Can\'t use another field in conjunction with $each') } + if (!Array.isArray(value.$each)) { throw new Error('$each requires an array value') } value.$each.forEach(function (v) { - lastStepModifierFunctions.$addToSet(obj, field, v); - }); + lastStepModifierFunctions.$addToSet(obj, field, v) + }) } else { obj[field].forEach(function (v) { - if (compareThings(v, value) === 0) { addToSet = false; } - }); - if (addToSet) { obj[field].push(value); } + if (compareThings(v, value) === 0) { addToSet = false } + }) + if (addToSet) { obj[field].push(value) } } -}; - +} /** * Remove the first or last element of an array */ lastStepModifierFunctions.$pop = function (obj, field, value) { - if (!util.isArray(obj[field])) { throw new Error("Can't $pop an element from non-array values"); } - if (typeof value !== 'number') { throw new Error(value + " isn't an integer, can't use it with $pop"); } - if (value === 0) { return; } + if (!Array.isArray(obj[field])) { throw new Error('Can\'t $pop an element from non-array values') } + if (typeof value !== 'number') { throw new Error(value + ' isn\'t an integer, can\'t use it with $pop') } + if (value === 0) { return } if (value > 0) { - obj[field] = obj[field].slice(0, obj[field].length - 1); + obj[field] = obj[field].slice(0, obj[field].length - 1) } else { - obj[field] = obj[field].slice(1); + obj[field] = obj[field].slice(1) } -}; - +} /** * Removes all instances of a value from an existing array */ lastStepModifierFunctions.$pull = function (obj, field, value) { - var arr, i; + if (!Array.isArray(obj[field])) { throw new Error('Can\'t $pull an element from non-array values') } - if (!util.isArray(obj[field])) { throw new Error("Can't $pull an element from non-array values"); } - - arr = obj[field]; - for (i = arr.length - 1; i >= 0; i -= 1) { + const arr = obj[field] + for (let i = arr.length - 1; i >= 0; i -= 1) { if (match(arr[i], value)) { - arr.splice(i, 1); + arr.splice(i, 1) } } -}; - +} /** * Increment a numeric field's value */ lastStepModifierFunctions.$inc = function (obj, field, value) { - if (typeof value !== 'number') { throw new Error(value + " must be a number"); } + if (typeof value !== 'number') { throw new Error(value + ' must be a number') } if (typeof obj[field] !== 'number') { if (!_.has(obj, field)) { - obj[field] = value; + obj[field] = value } else { - throw new Error("Don't use the $inc modifier on non-number fields"); + throw new Error('Don\'t use the $inc modifier on non-number fields') } } else { - obj[field] += value; + obj[field] += value } -}; +} /** * Updates the value of the field, only if specified field is greater than the current value of the field */ lastStepModifierFunctions.$max = function (obj, field, value) { if (typeof obj[field] === 'undefined') { - obj[field] = value; + obj[field] = value } else if (value > obj[field]) { - obj[field] = value; + obj[field] = value } -}; +} /** * Updates the value of the field, only if specified field is smaller than the current value of the field */ lastStepModifierFunctions.$min = function (obj, field, value) { - if (typeof obj[field] === 'undefined') {  - obj[field] = value; + if (typeof obj[field] === 'undefined') { + obj[field] = value } else if (value < obj[field]) { - obj[field] = value; + obj[field] = value } -}; +} // Given its name, create the complete modifier function function createModifierFunction (modifier) { return function (obj, field, value) { - var fieldParts = typeof field === 'string' ? field.split('.') : field; + const fieldParts = typeof field === 'string' ? field.split('.') : field if (fieldParts.length === 1) { - lastStepModifierFunctions[modifier](obj, field, value); + lastStepModifierFunctions[modifier](obj, field, value) } else { if (obj[fieldParts[0]] === undefined) { - if (modifier === '$unset') { return; } // Bad looking specific fix, needs to be generalized modifiers that behave like $unset are implemented - obj[fieldParts[0]] = {}; + if (modifier === '$unset') { return } // Bad looking specific fix, needs to be generalized modifiers that behave like $unset are implemented + obj[fieldParts[0]] = {} } - modifierFunctions[modifier](obj[fieldParts[0]], fieldParts.slice(1), value); + modifierFunctions[modifier](obj[fieldParts[0]], fieldParts.slice(1), value) } - }; + } } // Actually create all modifier functions Object.keys(lastStepModifierFunctions).forEach(function (modifier) { - modifierFunctions[modifier] = createModifierFunction(modifier); -}); - + modifierFunctions[modifier] = createModifierFunction(modifier) +}) /** * Modify a DB object according to an update query */ function modify (obj, updateQuery) { - var keys = Object.keys(updateQuery) - , firstChars = _.map(keys, function (item) { return item[0]; }) - , dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; }) - , newDoc, modifiers - ; + const keys = Object.keys(updateQuery) + const firstChars = _.map(keys, function (item) { return item[0] }) + const dollarFirstChars = _.filter(firstChars, function (c) { return c === '$' }) + let newDoc + let modifiers - if (keys.indexOf('_id') !== -1 && updateQuery._id !== obj._id) { throw new Error("You cannot change a document's _id"); } + if (keys.indexOf('_id') !== -1 && updateQuery._id !== obj._id) { throw new Error('You cannot change a document\'s _id') } if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) { - throw new Error("You cannot mix modifiers and normal fields"); + throw new Error('You cannot mix modifiers and normal fields') } if (dollarFirstChars.length === 0) { // Simply replace the object with the update query contents - newDoc = deepCopy(updateQuery); - newDoc._id = obj._id; + newDoc = deepCopy(updateQuery) + newDoc._id = obj._id } else { // Apply modifiers - modifiers = _.uniq(keys); - newDoc = deepCopy(obj); + modifiers = _.uniq(keys) + newDoc = deepCopy(obj) modifiers.forEach(function (m) { - var keys; - - if (!modifierFunctions[m]) { throw new Error("Unknown modifier " + m); } + if (!modifierFunctions[m]) { throw new Error('Unknown modifier ' + m) } // Can't rely on Object.keys throwing on non objects since ES6 // Not 100% satisfying as non objects can be interpreted as objects but no false negatives so we can live with it if (typeof updateQuery[m] !== 'object') { - throw new Error("Modifier " + m + "'s argument must be an object"); + throw new Error('Modifier ' + m + '\'s argument must be an object') } - keys = Object.keys(updateQuery[m]); + const keys = Object.keys(updateQuery[m]) keys.forEach(function (k) { - modifierFunctions[m](newDoc, k, updateQuery[m][k]); - }); - }); + modifierFunctions[m](newDoc, k, updateQuery[m][k]) + }) + }) } // Check result is valid and return it - checkObject(newDoc); - - if (obj._id !== newDoc._id) { throw new Error("You can't change a document's _id"); } - return newDoc; -}; + checkObject(newDoc) + if (obj._id !== newDoc._id) { throw new Error('You can\'t change a document\'s _id') } + return newDoc +} // ============================================================== // Finding documents @@ -491,34 +469,34 @@ function modify (obj, updateQuery) { * @param {String} field */ function getDotValue (obj, field) { - var fieldParts = typeof field === 'string' ? field.split('.') : field - , i, objs; + const fieldParts = typeof field === 'string' ? field.split('.') : field + let i + let objs - if (!obj) { return undefined; } // field cannot be empty so that means we should return undefined so that nothing can match + if (!obj) { return undefined } // field cannot be empty so that means we should return undefined so that nothing can match - if (fieldParts.length === 0) { return obj; } + if (fieldParts.length === 0) { return obj } - if (fieldParts.length === 1) { return obj[fieldParts[0]]; } + if (fieldParts.length === 1) { return obj[fieldParts[0]] } - if (util.isArray(obj[fieldParts[0]])) { + if (Array.isArray(obj[fieldParts[0]])) { // If the next field is an integer, return only this item of the array - i = parseInt(fieldParts[1], 10); + i = parseInt(fieldParts[1], 10) if (typeof i === 'number' && !isNaN(i)) { return getDotValue(obj[fieldParts[0]][i], fieldParts.slice(2)) } // Return the array of values - objs = new Array(); + objs = [] for (i = 0; i < obj[fieldParts[0]].length; i += 1) { - objs.push(getDotValue(obj[fieldParts[0]][i], fieldParts.slice(1))); + objs.push(getDotValue(obj[fieldParts[0]][i], fieldParts.slice(1))) } - return objs; + return objs } else { - return getDotValue(obj[fieldParts[0]], fieldParts.slice(1)); + return getDotValue(obj[fieldParts[0]], fieldParts.slice(1)) } } - /** * Check whether 'things' are equal * Things are defined as any native types (string, number, boolean, null, date) and objects @@ -526,142 +504,141 @@ function getDotValue (obj, field) { * Returns true if they are, false otherwise */ function areThingsEqual (a, b) { - var aKeys , bKeys , i; + let aKeys + let bKeys + let i // Strings, booleans, numbers, null if (a === null || typeof a === 'string' || typeof a === 'boolean' || typeof a === 'number' || - b === null || typeof b === 'string' || typeof b === 'boolean' || typeof b === 'number') { return a === b; } + b === null || typeof b === 'string' || typeof b === 'boolean' || typeof b === 'number') { return a === b } // Dates - if (util.isDate(a) || util.isDate(b)) { return util.isDate(a) && util.isDate(b) && a.getTime() === b.getTime(); } + if (util.types.isDate(a) || util.types.isDate(b)) { return util.types.isDate(a) && util.types.isDate(b) && a.getTime() === b.getTime() } // Arrays (no match since arrays are used as a $in) // undefined (no match since they mean field doesn't exist and can't be serialized) - if ((!(util.isArray(a) && util.isArray(b)) && (util.isArray(a) || util.isArray(b))) || a === undefined || b === undefined) { return false; } + if ((!(Array.isArray(a) && Array.isArray(b)) && (Array.isArray(a) || Array.isArray(b))) || a === undefined || b === undefined) { return false } // General objects (check for deep equality) // a and b should be objects at this point try { - aKeys = Object.keys(a); - bKeys = Object.keys(b); + aKeys = Object.keys(a) + bKeys = Object.keys(b) } catch (e) { - return false; + return false } - if (aKeys.length !== bKeys.length) { return false; } + if (aKeys.length !== bKeys.length) { return false } for (i = 0; i < aKeys.length; i += 1) { - if (bKeys.indexOf(aKeys[i]) === -1) { return false; } - if (!areThingsEqual(a[aKeys[i]], b[aKeys[i]])) { return false; } + if (bKeys.indexOf(aKeys[i]) === -1) { return false } + if (!areThingsEqual(a[aKeys[i]], b[aKeys[i]])) { return false } } - return true; + return true } - /** * Check that two values are comparable */ function areComparable (a, b) { - if (typeof a !== 'string' && typeof a !== 'number' && !util.isDate(a) && - typeof b !== 'string' && typeof b !== 'number' && !util.isDate(b)) { - return false; + if (typeof a !== 'string' && typeof a !== 'number' && !util.types.isDate(a) && + typeof b !== 'string' && typeof b !== 'number' && !util.types.isDate(b)) { + return false } - if (typeof a !== typeof b) { return false; } + if (typeof a !== typeof b) { return false } - return true; + return true } - /** * Arithmetic and comparison operators * @param {Native value} a Value in the object * @param {Native value} b Value in the query */ comparisonFunctions.$lt = function (a, b) { - return areComparable(a, b) && a < b; -}; + return areComparable(a, b) && a < b +} comparisonFunctions.$lte = function (a, b) { - return areComparable(a, b) && a <= b; -}; + return areComparable(a, b) && a <= b +} comparisonFunctions.$gt = function (a, b) { - return areComparable(a, b) && a > b; -}; + return areComparable(a, b) && a > b +} comparisonFunctions.$gte = function (a, b) { - return areComparable(a, b) && a >= b; -}; + return areComparable(a, b) && a >= b +} comparisonFunctions.$ne = function (a, b) { - if (a === undefined) { return true; } - return !areThingsEqual(a, b); -}; + if (a === undefined) { return true } + return !areThingsEqual(a, b) +} comparisonFunctions.$in = function (a, b) { - var i; + let i - if (!util.isArray(b)) { throw new Error("$in operator called with a non-array"); } + if (!Array.isArray(b)) { throw new Error('$in operator called with a non-array') } for (i = 0; i < b.length; i += 1) { - if (areThingsEqual(a, b[i])) { return true; } + if (areThingsEqual(a, b[i])) { return true } } - return false; -}; + return false +} comparisonFunctions.$nin = function (a, b) { - if (!util.isArray(b)) { throw new Error("$nin operator called with a non-array"); } + if (!Array.isArray(b)) { throw new Error('$nin operator called with a non-array') } - return !comparisonFunctions.$in(a, b); -}; + return !comparisonFunctions.$in(a, b) +} comparisonFunctions.$regex = function (a, b) { - if (!util.isRegExp(b)) { throw new Error("$regex operator called with non regular expression"); } + if (!util.types.isRegExp(b)) { throw new Error('$regex operator called with non regular expression') } if (typeof a !== 'string') { return false } else { - return b.test(a); + return b.test(a) } -}; +} comparisonFunctions.$exists = function (value, exists) { - if (exists || exists === '') { // This will be true for all values of exists except false, null, undefined and 0 - exists = true; // That's strange behaviour (we should only use true/false) but that's the way Mongo does it... + if (exists || exists === '') { // This will be true for all values of stat except false, null, undefined and 0 + exists = true // That's strange behaviour (we should only use true/false) but that's the way Mongo does it... } else { - exists = false; + exists = false } if (value === undefined) { return !exists } else { - return exists; + return exists } -}; +} // Specific to arrays comparisonFunctions.$size = function (obj, value) { - if (!util.isArray(obj)) { return false; } - if (value % 1 !== 0) { throw new Error("$size operator called without an integer"); } + if (!Array.isArray(obj)) { return false } + if (value % 1 !== 0) { throw new Error('$size operator called without an integer') } - return (obj.length == value); -}; + return obj.length === value +} comparisonFunctions.$elemMatch = function (obj, value) { - if (!util.isArray(obj)) { return false; } - var i = obj.length; - var result = false; // Initialize result + if (!Array.isArray(obj)) { return false } + let i = obj.length + let result = false // Initialize result while (i--) { - if (match(obj[i], value)) { // If match for array element, return true - result = true; - break; + if (match(obj[i], value)) { // If match for array element, return true + result = true + break } } - return result; -}; -arrayComparisonFunctions.$size = true; -arrayComparisonFunctions.$elemMatch = true; - + return result +} +arrayComparisonFunctions.$size = true +arrayComparisonFunctions.$elemMatch = true /** * Match any of the subqueries @@ -669,17 +646,16 @@ arrayComparisonFunctions.$elemMatch = true; * @param {Array of Queries} query */ logicalOperators.$or = function (obj, query) { - var i; + let i - if (!util.isArray(query)) { throw new Error("$or operator used without an array"); } + if (!Array.isArray(query)) { throw new Error('$or operator used without an array') } for (i = 0; i < query.length; i += 1) { - if (match(obj, query[i])) { return true; } + if (match(obj, query[i])) { return true } } - return false; -}; - + return false +} /** * Match all of the subqueries @@ -687,17 +663,16 @@ logicalOperators.$or = function (obj, query) { * @param {Array of Queries} query */ logicalOperators.$and = function (obj, query) { - var i; + let i - if (!util.isArray(query)) { throw new Error("$and operator used without an array"); } + if (!Array.isArray(query)) { throw new Error('$and operator used without an array') } for (i = 0; i < query.length; i += 1) { - if (!match(obj, query[i])) { return false; } + if (!match(obj, query[i])) { return false } } - return true; -}; - + return true +} /** * Inverted match of the query @@ -705,9 +680,8 @@ logicalOperators.$and = function (obj, query) { * @param {Query} query */ logicalOperators.$not = function (obj, query) { - return !match(obj, query); -}; - + return !match(obj, query) +} /** * Use a function to match @@ -715,16 +689,13 @@ logicalOperators.$not = function (obj, query) { * @param {Query} query */ logicalOperators.$where = function (obj, fn) { - var result; - - if (!_.isFunction(fn)) { throw new Error("$where operator used without a function"); } + if (!_.isFunction(fn)) { throw new Error('$where operator used without a function') } - result = fn.call(obj); - if (!_.isBoolean(result)) { throw new Error("$where function must return boolean"); } - - return result; -}; + const result = fn.call(obj) + if (!_.isBoolean(result)) { throw new Error('$where function must return boolean') } + return result +} /** * Tell if a given document matches a query @@ -732,104 +703,107 @@ logicalOperators.$where = function (obj, fn) { * @param {Object} query */ function match (obj, query) { - var queryKeys, queryKey, queryValue, i; + let queryKey + let queryValue + let i // Primitive query against a primitive type // This is a bit of a hack since we construct an object with an arbitrary key only to dereference it later // But I don't have time for a cleaner implementation now if (isPrimitiveType(obj) || isPrimitiveType(query)) { - return matchQueryPart({ needAKey: obj }, 'needAKey', query); + return matchQueryPart({ needAKey: obj }, 'needAKey', query) } // Normal query - queryKeys = Object.keys(query); + const queryKeys = Object.keys(query) for (i = 0; i < queryKeys.length; i += 1) { - queryKey = queryKeys[i]; - queryValue = query[queryKey]; + queryKey = queryKeys[i] + queryValue = query[queryKey] if (queryKey[0] === '$') { - if (!logicalOperators[queryKey]) { throw new Error("Unknown logical operator " + queryKey); } - if (!logicalOperators[queryKey](obj, queryValue)) { return false; } + if (!logicalOperators[queryKey]) { throw new Error('Unknown logical operator ' + queryKey) } + if (!logicalOperators[queryKey](obj, queryValue)) { return false } } else { - if (!matchQueryPart(obj, queryKey, queryValue)) { return false; } + if (!matchQueryPart(obj, queryKey, queryValue)) { return false } } } - return true; -}; - + return true +} /** * Match an object against a specific { key: value } part of a query * if the treatObjAsValue flag is set, don't try to match every part separately, but the array as a whole */ function matchQueryPart (obj, queryKey, queryValue, treatObjAsValue) { - var objValue = getDotValue(obj, queryKey) - , i, keys, firstChars, dollarFirstChars; + const objValue = getDotValue(obj, queryKey) + let i + let keys + let firstChars + let dollarFirstChars // Check if the value is an array if we don't force a treatment as value - if (util.isArray(objValue) && !treatObjAsValue) { + if (Array.isArray(objValue) && !treatObjAsValue) { // If the queryValue is an array, try to perform an exact match - if (util.isArray(queryValue)) { - return matchQueryPart(obj, queryKey, queryValue, true); + if (Array.isArray(queryValue)) { + return matchQueryPart(obj, queryKey, queryValue, true) } // Check if we are using an array-specific comparison function - if (queryValue !== null && typeof queryValue === 'object' && !util.isRegExp(queryValue)) { - keys = Object.keys(queryValue); + if (queryValue !== null && typeof queryValue === 'object' && !util.types.isRegExp(queryValue)) { + keys = Object.keys(queryValue) for (i = 0; i < keys.length; i += 1) { - if (arrayComparisonFunctions[keys[i]]) { return matchQueryPart(obj, queryKey, queryValue, true); } + if (arrayComparisonFunctions[keys[i]]) { return matchQueryPart(obj, queryKey, queryValue, true) } } } // If not, treat it as an array of { obj, query } where there needs to be at least one match for (i = 0; i < objValue.length; i += 1) { - if (matchQueryPart({ k: objValue[i] }, 'k', queryValue)) { return true; } // k here could be any string + if (matchQueryPart({ k: objValue[i] }, 'k', queryValue)) { return true } // k here could be any string } - return false; + return false } // queryValue is an actual object. Determine whether it contains comparison operators // or only normal fields. Mixed objects are not allowed - if (queryValue !== null && typeof queryValue === 'object' && !util.isRegExp(queryValue) && !util.isArray(queryValue)) { - keys = Object.keys(queryValue); - firstChars = _.map(keys, function (item) { return item[0]; }); - dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; }); + if (queryValue !== null && typeof queryValue === 'object' && !util.types.isRegExp(queryValue) && !Array.isArray(queryValue)) { + keys = Object.keys(queryValue) + firstChars = _.map(keys, function (item) { return item[0] }) + dollarFirstChars = _.filter(firstChars, function (c) { return c === '$' }) if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) { - throw new Error("You cannot mix operators and normal fields"); + throw new Error('You cannot mix operators and normal fields') } // queryValue is an object of this form: { $comparisonOperator1: value1, ... } if (dollarFirstChars.length > 0) { for (i = 0; i < keys.length; i += 1) { - if (!comparisonFunctions[keys[i]]) { throw new Error("Unknown comparison function " + keys[i]); } + if (!comparisonFunctions[keys[i]]) { throw new Error('Unknown comparison function ' + keys[i]) } - if (!comparisonFunctions[keys[i]](objValue, queryValue[keys[i]])) { return false; } + if (!comparisonFunctions[keys[i]](objValue, queryValue[keys[i]])) { return false } } - return true; + return true } } // Using regular expressions with basic querying - if (util.isRegExp(queryValue)) { return comparisonFunctions.$regex(objValue, queryValue); } + if (util.types.isRegExp(queryValue)) { return comparisonFunctions.$regex(objValue, queryValue) } // queryValue is either a native value or a normal object // Basic matching is possible - if (!areThingsEqual(objValue, queryValue)) { return false; } + if (!areThingsEqual(objValue, queryValue)) { return false } - return true; + return true } - // Interface -module.exports.serialize = serialize; -module.exports.deserialize = deserialize; -module.exports.deepCopy = deepCopy; -module.exports.checkObject = checkObject; -module.exports.isPrimitiveType = isPrimitiveType; -module.exports.modify = modify; -module.exports.getDotValue = getDotValue; -module.exports.match = match; -module.exports.areThingsEqual = areThingsEqual; -module.exports.compareThings = compareThings; +module.exports.serialize = serialize +module.exports.deserialize = deserialize +module.exports.deepCopy = deepCopy +module.exports.checkObject = checkObject +module.exports.isPrimitiveType = isPrimitiveType +module.exports.modify = modify +module.exports.getDotValue = getDotValue +module.exports.match = match +module.exports.areThingsEqual = areThingsEqual +module.exports.compareThings = compareThings diff --git a/lib/persistence.js b/lib/persistence.js index 88a4948..ad97606 100755 --- a/lib/persistence.js +++ b/lib/persistence.js @@ -4,311 +4,303 @@ * * Persistence.loadDatabase(callback) and callback has signature err * * Persistence.persistNewState(newDocs, callback) where newDocs is an array of documents and callback has signature err */ +const storage = require('./storage') +const path = require('path') +const model = require('./model') +const async = require('async') +const customUtils = require('./customUtils') +const Index = require('./indexes') + +class Persistence { + /** + * Create a new Persistence object for database options.db + * @param {Datastore} options.db + * @param {Boolean} options.nodeWebkitAppName Optional, specify the name of your NW app if you want options.filename to be relative to the directory where + * Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion) + */ + constructor (options) { + let i + let j + let randomString + + this.db = options.db + this.inMemoryOnly = this.db.inMemoryOnly + this.filename = this.db.filename + this.corruptAlertThreshold = options.corruptAlertThreshold !== undefined ? options.corruptAlertThreshold : 0.1 + + if (!this.inMemoryOnly && this.filename && this.filename.charAt(this.filename.length - 1) === '~') { + throw new Error('The datafile name can\'t end with a ~, which is reserved for crash safe backup files') + } -var storage = require('./storage') - , path = require('path') - , model = require('./model') - , async = require('async') - , customUtils = require('./customUtils') - , Index = require('./indexes') - ; - - -/** - * Create a new Persistence object for database options.db - * @param {Datastore} options.db - * @param {Boolean} options.nodeWebkitAppName Optional, specify the name of your NW app if you want options.filename to be relative to the directory where - * Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion) - */ -function Persistence (options) { - var i, j, randomString; - - this.db = options.db; - this.inMemoryOnly = this.db.inMemoryOnly; - this.filename = this.db.filename; - this.corruptAlertThreshold = options.corruptAlertThreshold !== undefined ? options.corruptAlertThreshold : 0.1; - - if (!this.inMemoryOnly && this.filename && this.filename.charAt(this.filename.length - 1) === '~') { - throw new Error("The datafile name can't end with a ~, which is reserved for crash safe backup files"); - } - - // After serialization and before deserialization hooks with some basic sanity checks - if (options.afterSerialization && !options.beforeDeserialization) { - throw new Error("Serialization hook defined but deserialization hook undefined, cautiously refusing to start NeDB to prevent dataloss"); - } - if (!options.afterSerialization && options.beforeDeserialization) { - throw new Error("Serialization hook undefined but deserialization hook defined, cautiously refusing to start NeDB to prevent dataloss"); - } - this.afterSerialization = options.afterSerialization || function (s) { return s; }; - this.beforeDeserialization = options.beforeDeserialization || function (s) { return s; }; - for (i = 1; i < 30; i += 1) { - for (j = 0; j < 10; j += 1) { - randomString = customUtils.uid(i); - if (this.beforeDeserialization(this.afterSerialization(randomString)) !== randomString) { - throw new Error("beforeDeserialization is not the reverse of afterSerialization, cautiously refusing to start NeDB to prevent dataloss"); + // After serialization and before deserialization hooks with some basic sanity checks + if (options.afterSerialization && !options.beforeDeserialization) { + throw new Error('Serialization hook defined but deserialization hook undefined, cautiously refusing to start NeDB to prevent dataloss') + } + if (!options.afterSerialization && options.beforeDeserialization) { + throw new Error('Serialization hook undefined but deserialization hook defined, cautiously refusing to start NeDB to prevent dataloss') + } + this.afterSerialization = options.afterSerialization || function (s) { return s } + this.beforeDeserialization = options.beforeDeserialization || function (s) { return s } + for (i = 1; i < 30; i += 1) { + for (j = 0; j < 10; j += 1) { + randomString = customUtils.uid(i) + if (this.beforeDeserialization(this.afterSerialization(randomString)) !== randomString) { + throw new Error('beforeDeserialization is not the reverse of afterSerialization, cautiously refusing to start NeDB to prevent dataloss') + } } } - } - // For NW apps, store data in the same directory where NW stores application data - if (this.filename && options.nodeWebkitAppName) { - console.log("=================================================================="); - console.log("WARNING: The nodeWebkitAppName option is deprecated"); - console.log("To get the path to the directory where Node Webkit stores the data"); - console.log("for your app, use the internal nw.gui module like this"); - console.log("require('nw.gui').App.dataPath"); - console.log("See https://github.com/rogerwang/node-webkit/issues/500"); - console.log("=================================================================="); - this.filename = Persistence.getNWAppFilename(options.nodeWebkitAppName, this.filename); + // For NW apps, store data in the same directory where NW stores application data + if (this.filename && options.nodeWebkitAppName) { + console.log('==================================================================') + console.log('WARNING: The nodeWebkitAppName option is deprecated') + console.log('To get the path to the directory where Node Webkit stores the data') + console.log('for your app, use the internal nw.gui module like this') + console.log('require(\'nw.gui\').App.dataPath') + console.log('See https://github.com/rogerwang/node-webkit/issues/500') + console.log('==================================================================') + this.filename = Persistence.getNWAppFilename(options.nodeWebkitAppName, this.filename) + } } -}; - -/** - * Check if a directory exists and create it on the fly if it is not the case - * cb is optional, signature: err - */ -Persistence.ensureDirectoryExists = function (dir, cb) { - var callback = cb || function () {} - ; + /** + * Persist cached database + * This serves as a compaction function since the cache always contains only the number of documents in the collection + * while the data file is append-only so it may grow larger + * @param {Function} cb Optional callback, signature: err + */ + persistCachedDatabase (cb) { + const callback = cb || function () {} + let toPersist = '' + const self = this + + if (this.inMemoryOnly) { return callback(null) } + + this.db.getAllData().forEach(function (doc) { + toPersist += self.afterSerialization(model.serialize(doc)) + '\n' + }) + Object.keys(this.db.indexes).forEach(function (fieldName) { + if (fieldName !== '_id') { // The special _id index is managed by datastore.js, the others need to be persisted + toPersist += self.afterSerialization(model.serialize({ + $$indexCreated: { + fieldName: fieldName, + unique: self.db.indexes[fieldName].unique, + sparse: self.db.indexes[fieldName].sparse + } + })) + '\n' + } + }) - storage.mkdirp(dir, function (err) { return callback(err); }); -}; + storage.crashSafeWriteFile(this.filename, toPersist, function (err) { + if (err) { return callback(err) } + self.db.emit('compaction.done') + return callback(null) + }) + } + /** + * Queue a rewrite of the datafile + */ + compactDatafile () { + this.db.executor.push({ this: this, fn: this.persistCachedDatabase, arguments: [] }) + } + /** + * Set automatic compaction every interval ms + * @param {Number} interval in milliseconds, with an enforced minimum of 5 seconds + */ + setAutocompactionInterval (interval) { + const self = this + const minInterval = 5000 + const realInterval = Math.max(interval || 0, minInterval) + this.stopAutocompaction() -/** - * Return the path the datafile if the given filename is relative to the directory where Node Webkit stores - * data for this application. Probably the best place to store data - */ -Persistence.getNWAppFilename = function (appName, relativeFilename) { - var home; - - switch (process.platform) { - case 'win32': - case 'win64': - home = process.env.LOCALAPPDATA || process.env.APPDATA; - if (!home) { throw new Error("Couldn't find the base application data folder"); } - home = path.join(home, appName); - break; - case 'darwin': - home = process.env.HOME; - if (!home) { throw new Error("Couldn't find the base application data directory"); } - home = path.join(home, 'Library', 'Application Support', appName); - break; - case 'linux': - home = process.env.HOME; - if (!home) { throw new Error("Couldn't find the base application data directory"); } - home = path.join(home, '.config', appName); - break; - default: - throw new Error("Can't use the Node Webkit relative path for platform " + process.platform); - break; + this.autocompactionIntervalId = setInterval(function () { + self.compactDatafile() + }, realInterval) } - return path.join(home, 'nedb-data', relativeFilename); -} + /** + * Stop autocompaction (do nothing if autocompaction was not running) + */ + stopAutocompaction () { + if (this.autocompactionIntervalId) { clearInterval(this.autocompactionIntervalId) } + } + /** + * Persist new state for the given newDocs (can be insertion, update or removal) + * Use an append-only format + * @param {Array} newDocs Can be empty if no doc was updated/removed + * @param {Function} cb Optional, signature: err + */ + persistNewState (newDocs, cb) { + const self = this + let toPersist = '' + const callback = cb || function () {} + + // In-memory only datastore + if (self.inMemoryOnly) { return callback(null) } + + newDocs.forEach(function (doc) { + toPersist += self.afterSerialization(model.serialize(doc)) + '\n' + }) + + if (toPersist.length === 0) { return callback(null) } + + storage.appendFile(self.filename, toPersist, 'utf8', function (err) { + return callback(err) + }) + } -/** - * Persist cached database - * This serves as a compaction function since the cache always contains only the number of documents in the collection - * while the data file is append-only so it may grow larger - * @param {Function} cb Optional callback, signature: err - */ -Persistence.prototype.persistCachedDatabase = function (cb) { - var callback = cb || function () {} - , toPersist = '' - , self = this - ; - - if (this.inMemoryOnly) { return callback(null); } - - this.db.getAllData().forEach(function (doc) { - toPersist += self.afterSerialization(model.serialize(doc)) + '\n'; - }); - Object.keys(this.db.indexes).forEach(function (fieldName) { - if (fieldName != "_id") { // The special _id index is managed by datastore.js, the others need to be persisted - toPersist += self.afterSerialization(model.serialize({ $$indexCreated: { fieldName: fieldName, unique: self.db.indexes[fieldName].unique, sparse: self.db.indexes[fieldName].sparse }})) + '\n'; + /** + * From a database's raw data, return the corresponding + * machine understandable collection + */ + treatRawData (rawData) { + const data = rawData.split('\n') + const dataById = {} + const tdata = [] + let i + const indexes = {} + let corruptItems = -1 + + for (i = 0; i < data.length; i += 1) { + let doc + + try { + doc = model.deserialize(this.beforeDeserialization(data[i])) + if (doc._id) { + if (doc.$$deleted === true) { + delete dataById[doc._id] + } else { + dataById[doc._id] = doc + } + } else if (doc.$$indexCreated && doc.$$indexCreated.fieldName != null) { + indexes[doc.$$indexCreated.fieldName] = doc.$$indexCreated + } else if (typeof doc.$$indexRemoved === 'string') { + delete indexes[doc.$$indexRemoved] + } + } catch (e) { + corruptItems += 1 + } } - }); - - storage.crashSafeWriteFile(this.filename, toPersist, function (err) { - if (err) { return callback(err); } - self.db.emit('compaction.done'); - return callback(null); - }); -}; - -/** - * Queue a rewrite of the datafile - */ -Persistence.prototype.compactDatafile = function () { - this.db.executor.push({ this: this, fn: this.persistCachedDatabase, arguments: [] }); -}; - - -/** - * Set automatic compaction every interval ms - * @param {Number} interval in milliseconds, with an enforced minimum of 5 seconds - */ -Persistence.prototype.setAutocompactionInterval = function (interval) { - var self = this - , minInterval = 5000 - , realInterval = Math.max(interval || 0, minInterval) - ; - - this.stopAutocompaction(); - - this.autocompactionIntervalId = setInterval(function () { - self.compactDatafile(); - }, realInterval); -}; - - -/** - * Stop autocompaction (do nothing if autocompaction was not running) - */ -Persistence.prototype.stopAutocompaction = function () { - if (this.autocompactionIntervalId) { clearInterval(this.autocompactionIntervalId); } -}; - - -/** - * Persist new state for the given newDocs (can be insertion, update or removal) - * Use an append-only format - * @param {Array} newDocs Can be empty if no doc was updated/removed - * @param {Function} cb Optional, signature: err - */ -Persistence.prototype.persistNewState = function (newDocs, cb) { - var self = this - , toPersist = '' - , callback = cb || function () {} - ; - - // In-memory only datastore - if (self.inMemoryOnly) { return callback(null); } - - newDocs.forEach(function (doc) { - toPersist += self.afterSerialization(model.serialize(doc)) + '\n'; - }); - - if (toPersist.length === 0) { return callback(null); } + // A bit lenient on corruption + if (data.length > 0 && corruptItems / data.length > this.corruptAlertThreshold) { + 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') + } - storage.appendFile(self.filename, toPersist, 'utf8', function (err) { - return callback(err); - }); -}; + Object.keys(dataById).forEach(function (k) { + tdata.push(dataById[k]) + }) + return { data: tdata, indexes: indexes } + } -/** - * From a database's raw data, return the corresponding - * machine understandable collection - */ -Persistence.prototype.treatRawData = function (rawData) { - var data = rawData.split('\n') - , dataById = {} - , tdata = [] - , i - , indexes = {} - , corruptItems = -1 // Last line of every data file is usually blank so not really corrupt - ; - - for (i = 0; i < data.length; i += 1) { - var doc; - - try { - doc = model.deserialize(this.beforeDeserialization(data[i])); - if (doc._id) { - if (doc.$$deleted === true) { - delete dataById[doc._id]; - } else { - dataById[doc._id] = doc; - } - } else if (doc.$$indexCreated && doc.$$indexCreated.fieldName != undefined) { - indexes[doc.$$indexCreated.fieldName] = doc.$$indexCreated; - } else if (typeof doc.$$indexRemoved === "string") { - delete indexes[doc.$$indexRemoved]; + /** + * Load the database + * 1) Create all indexes + * 2) Insert all data + * 3) Compact the database + * This means pulling data out of the data file or creating it if it doesn't exist + * Also, all data is persisted right away, which has the effect of compacting the database file + * This operation is very quick at startup for a big collection (60ms for ~10k docs) + * @param {Function} cb Optional callback, signature: err + */ + loadDatabase (cb) { + const callback = cb || function () {} + const self = this + + self.db.resetIndexes() + + // In-memory only datastore + if (self.inMemoryOnly) { return callback(null) } + + async.waterfall([ + function (cb) { + // eslint-disable-next-line node/handle-callback-err + Persistence.ensureDirectoryExists(path.dirname(self.filename), function (err) { + // TODO: handle error + // eslint-disable-next-line node/handle-callback-err + storage.ensureDatafileIntegrity(self.filename, function (err) { + // TODO: handle error + storage.readFile(self.filename, 'utf8', function (err, rawData) { + if (err) { return cb(err) } + let treatedData + try { + treatedData = self.treatRawData(rawData) + } catch (e) { + return cb(e) + } + + // Recreate all indexes in the datafile + Object.keys(treatedData.indexes).forEach(function (key) { + self.db.indexes[key] = new Index(treatedData.indexes[key]) + }) + + // Fill cached database (i.e. all indexes) with data + try { + self.db.resetIndexes(treatedData.data) + } catch (e) { + self.db.resetIndexes() // Rollback any index which didn't fail + return cb(e) + } + + self.db.persistence.persistCachedDatabase(cb) + }) + }) + }) } - } catch (e) { - corruptItems += 1; - } - } + ], function (err) { + if (err) { return callback(err) } - // A bit lenient on corruption - if (data.length > 0 && corruptItems / data.length > this.corruptAlertThreshold) { - 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"); + self.db.executor.processBuffer() + return callback(null) + }) } - Object.keys(dataById).forEach(function (k) { - tdata.push(dataById[k]); - }); - - return { data: tdata, indexes: indexes }; -}; + /** + * Check if a directory stat and create it on the fly if it is not the case + * cb is optional, signature: err + */ + static ensureDirectoryExists (dir, cb) { + const callback = cb || function () {} + storage.mkdir(dir, { recursive: true }, err => { callback(err) }) + } -/** - * Load the database - * 1) Create all indexes - * 2) Insert all data - * 3) Compact the database - * This means pulling data out of the data file or creating it if it doesn't exist - * Also, all data is persisted right away, which has the effect of compacting the database file - * This operation is very quick at startup for a big collection (60ms for ~10k docs) - * @param {Function} cb Optional callback, signature: err - */ -Persistence.prototype.loadDatabase = function (cb) { - var callback = cb || function () {} - , self = this - ; - - self.db.resetIndexes(); - - // In-memory only datastore - if (self.inMemoryOnly) { return callback(null); } - - async.waterfall([ - function (cb) { - Persistence.ensureDirectoryExists(path.dirname(self.filename), function (err) { - storage.ensureDatafileIntegrity(self.filename, function (err) { - storage.readFile(self.filename, 'utf8', function (err, rawData) { - if (err) { return cb(err); } - - try { - var treatedData = self.treatRawData(rawData); - } catch (e) { - return cb(e); - } - - // Recreate all indexes in the datafile - Object.keys(treatedData.indexes).forEach(function (key) { - self.db.indexes[key] = new Index(treatedData.indexes[key]); - }); - - // Fill cached database (i.e. all indexes) with data - try { - self.db.resetIndexes(treatedData.data); - } catch (e) { - self.db.resetIndexes(); // Rollback any index which didn't fail - return cb(e); - } - - self.db.persistence.persistCachedDatabase(cb); - }); - }); - }); + /** + * Return the path the datafile if the given filename is relative to the directory where Node Webkit stores + * data for this application. Probably the best place to store data + */ + static getNWAppFilename (appName, relativeFilename) { + let home + + switch (process.platform) { + case 'win32': + case 'win64': + home = process.env.LOCALAPPDATA || process.env.APPDATA + if (!home) { throw new Error('Couldn\'t find the base application data folder') } + home = path.join(home, appName) + break + case 'darwin': + home = process.env.HOME + if (!home) { throw new Error('Couldn\'t find the base application data directory') } + home = path.join(home, 'Library', 'Application Support', appName) + break + case 'linux': + home = process.env.HOME + if (!home) { throw new Error('Couldn\'t find the base application data directory') } + home = path.join(home, '.config', appName) + break + default: + throw new Error('Can\'t use the Node Webkit relative path for platform ' + process.platform) } - ], function (err) { - if (err) { return callback(err); } - - self.db.executor.processBuffer(); - return callback(null); - }); -}; + return path.join(home, 'nedb-data', relativeFilename) + } +} // Interface -module.exports = Persistence; +module.exports = Persistence diff --git a/lib/storage.js b/lib/storage.js index 128f9cc..43c143f 100755 --- a/lib/storage.js +++ b/lib/storage.js @@ -6,34 +6,30 @@ * This version is the Node.js/Node Webkit version * It's essentially fs, mkdirp and crash safe write and read functions */ - -var fs = require('fs') - , mkdirp = require('mkdirp') - , async = require('async') - , path = require('path') - , storage = {} - ; - -storage.exists = fs.exists; -storage.rename = fs.rename; -storage.writeFile = fs.writeFile; -storage.unlink = fs.unlink; -storage.appendFile = fs.appendFile; -storage.readFile = fs.readFile; -storage.mkdirp = mkdirp; - +const fs = require('fs') +const async = require('async') +const path = require('path') +const storage = {} + +// eslint-disable-next-line node/no-callback-literal +storage.exists = (path, cb) => fs.access(path, fs.constants.F_OK, (err) => { cb(!err) }) +storage.rename = fs.rename +storage.writeFile = fs.writeFile +storage.unlink = fs.unlink +storage.appendFile = fs.appendFile +storage.readFile = fs.readFile +storage.mkdir = fs.mkdir /** * Explicit name ... */ storage.ensureFileDoesntExist = function (file, callback) { storage.exists(file, function (exists) { - if (!exists) { return callback(null); } - - storage.unlink(file, function (err) { return callback(err); }); - }); -}; + if (!exists) { return callback(null) } + storage.unlink(file, function (err) { return callback(err) }) + }) +} /** * Flush data in OS buffer to storage if corresponding option is set @@ -42,36 +38,36 @@ storage.ensureFileDoesntExist = function (file, callback) { * If options is a string, it is assumed that the flush of the file (not dir) called options was requested */ storage.flushToStorage = function (options, callback) { - var filename, flags; + let filename + let flags if (typeof options === 'string') { - filename = options; - flags = 'r+'; + filename = options + flags = 'r+' } else { - filename = options.filename; - flags = options.isDir ? 'r' : 'r+'; + filename = options.filename + flags = options.isDir ? 'r' : 'r+' } // Windows can't fsync (FlushFileBuffers) directories. We can live with this as it cannot cause 100% dataloss // except in the very rare event of the first time database is loaded and a crash happens - if (flags === 'r' && (process.platform === 'win32' || process.platform === 'win64')) { return callback(null); } + if (flags === 'r' && (process.platform === 'win32' || process.platform === 'win64')) { return callback(null) } fs.open(filename, flags, function (err, fd) { - if (err) { return callback(err); } + if (err) { return callback(err) } fs.fsync(fd, function (errFS) { fs.close(fd, function (errC) { if (errFS || errC) { - var e = new Error('Failed to flush to storage'); - e.errorOnFsync = errFS; - e.errorOnClose = errC; - return callback(e); + const e = new Error('Failed to flush to storage') + e.errorOnFsync = errFS + e.errorOnClose = errC + return callback(e) } else { - return callback(null); + return callback(null) } - }); - }); - }); -}; - + }) + }) + }) +} /** * Fully write or rewrite the datafile, immune to crashes during the write operation (data will not be lost) @@ -80,31 +76,30 @@ storage.flushToStorage = function (options, callback) { * @param {Function} cb Optional callback, signature: err */ storage.crashSafeWriteFile = function (filename, data, cb) { - var callback = cb || function () {} - , tempFilename = filename + '~'; + const callback = cb || function () {} + const tempFilename = filename + '~' async.waterfall([ - async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true }) - , function (cb) { + async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true }), + function (cb) { storage.exists(filename, function (exists) { if (exists) { - storage.flushToStorage(filename, function (err) { return cb(err); }); + storage.flushToStorage(filename, function (err) { return cb(err) }) } else { - return cb(); + return cb() } - }); - } - , function (cb) { - storage.writeFile(tempFilename, data, function (err) { return cb(err); }); - } - , async.apply(storage.flushToStorage, tempFilename) - , function (cb) { - storage.rename(tempFilename, filename, function (err) { return cb(err); }); - } - , async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true }) - ], function (err) { return callback(err); }) -}; - + }) + }, + function (cb) { + storage.writeFile(tempFilename, data, function (err) { return cb(err) }) + }, + async.apply(storage.flushToStorage, tempFilename), + function (cb) { + storage.rename(tempFilename, filename, function (err) { return cb(err) }) + }, + async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true }) + ], function (err) { return callback(err) }) +} /** * Ensure the datafile contains all the data, even if there was a crash during a full file write @@ -112,25 +107,23 @@ storage.crashSafeWriteFile = function (filename, data, cb) { * @param {Function} callback signature: err */ storage.ensureDatafileIntegrity = function (filename, callback) { - var tempFilename = filename + '~'; + const tempFilename = filename + '~' storage.exists(filename, function (filenameExists) { // Write was successful - if (filenameExists) { return callback(null); } + if (filenameExists) { return callback(null) } storage.exists(tempFilename, function (oldFilenameExists) { // New database if (!oldFilenameExists) { - return storage.writeFile(filename, '', 'utf8', function (err) { callback(err); }); + return storage.writeFile(filename, '', 'utf8', function (err) { callback(err) }) } // Write failed, use old version - storage.rename(tempFilename, filename, function (err) { return callback(err); }); - }); - }); -}; - - + storage.rename(tempFilename, filename, function (err) { return callback(err) }) + }) + }) +} // Interface -module.exports = storage; +module.exports = storage diff --git a/package.json b/package.json index 8ee7e4f..e61cddd 100755 --- a/package.json +++ b/package.json @@ -20,27 +20,36 @@ "url": "git@github.com:louischatriot/nedb.git" }, "dependencies": { + "@seald-io/binary-search-tree": "^1.0.0", "async": "0.2.10", - "binary-search-tree": "0.2.5", - "localforage": "^1.3.0", - "mkdirp": "~0.5.1", - "underscore": "~1.4.4" + "localforage": "^1.9.0", + "underscore": "^1.13.1" }, "devDependencies": { - "chai": "^3.2.0", - "mocha": "1.4.x", + "chai": "^4.3.4", + "commander": "1.1.1", + "exec-time": "0.0.2", + "mocha": "^8.4.0", "request": "2.9.x", + "semver": "^7.3.5", "sinon": "1.3.x", - "exec-time": "0.0.2", - "commander": "1.1.1" + "standard": "^16.0.3" }, "scripts": { - "test": "./node_modules/.bin/mocha --reporter spec --timeout 10000" + "test": "mocha --reporter spec --timeout 10000" }, - "main": "index", + "main": "index.js", "browser": { "./lib/customUtils.js": "./browser-version/browser-specific/lib/customUtils.js", "./lib/storage.js": "./browser-version/browser-specific/lib/storage.js" }, - "license": "SEE LICENSE IN LICENSE" + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "standard": { + "ignore": [ + "browser-version" + ] + } } diff --git a/test/cursor.test.js b/test/cursor.test.js index 3bffa4a..da4131d 100755 --- a/test/cursor.test.js +++ b/test/cursor.test.js @@ -1,862 +1,886 @@ -var should = require('chai').should() - , assert = require('chai').assert - , testDb = 'workspace/test.db' - , fs = require('fs') - , path = require('path') - , _ = require('underscore') - , async = require('async') - , model = require('../lib/model') - , Datastore = require('../lib/datastore') - , Persistence = require('../lib/persistence') - , Cursor = require('../lib/cursor') - ; - +/* eslint-env mocha */ +const chai = require('chai') +const testDb = 'workspace/test.db' +const fs = require('fs') +const path = require('path') +const _ = require('underscore') +const async = require('async') +const Datastore = require('../lib/datastore') +const Persistence = require('../lib/persistence') +const Cursor = require('../lib/cursor') + +const { assert } = chai +chai.should() describe('Cursor', function () { - var d; + let d beforeEach(function (done) { - d = new Datastore({ filename: testDb }); - d.filename.should.equal(testDb); - d.inMemoryOnly.should.equal(false); + d = new Datastore({ filename: testDb }) + d.filename.should.equal(testDb) + d.inMemoryOnly.should.equal(false) async.waterfall([ function (cb) { Persistence.ensureDirectoryExists(path.dirname(testDb), function () { - fs.exists(testDb, function (exists) { - if (exists) { - fs.unlink(testDb, cb); - } else { return cb(); } - }); - }); - } - , function (cb) { + fs.access(testDb, fs.constants.F_OK, function (err) { + if (!err) { + fs.unlink(testDb, cb) + } else { return cb() } + }) + }) + }, + function (cb) { d.loadDatabase(function (err) { - assert.isNull(err); - d.getAllData().length.should.equal(0); - return cb(); - }); + assert.isNull(err) + d.getAllData().length.should.equal(0) + return cb() + }) } - ], done); - }); + ], done) + }) describe('Without sorting', function () { - beforeEach(function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 5 }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 57 }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 52 }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 23 }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 89 }, function (err) { - return done(); - }); - }); - }); - }); - }); - }); + return done() + }) + }) + }) + }) + }) + }) it('Without query, an empty query or a simple query and no skip or limit', function (done) { async.waterfall([ function (cb) { - var cursor = new Cursor(d); - cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(5); - _.filter(docs, function(doc) { return doc.age === 5; })[0].age.should.equal(5); - _.filter(docs, function(doc) { return doc.age === 57; })[0].age.should.equal(57); - _.filter(docs, function(doc) { return doc.age === 52; })[0].age.should.equal(52); - _.filter(docs, function(doc) { return doc.age === 23; })[0].age.should.equal(23); - _.filter(docs, function(doc) { return doc.age === 89; })[0].age.should.equal(89); - cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); - cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(5); - _.filter(docs, function(doc) { return doc.age === 5; })[0].age.should.equal(5); - _.filter(docs, function(doc) { return doc.age === 57; })[0].age.should.equal(57); - _.filter(docs, function(doc) { return doc.age === 52; })[0].age.should.equal(52); - _.filter(docs, function(doc) { return doc.age === 23; })[0].age.should.equal(23); - _.filter(docs, function(doc) { return doc.age === 89; })[0].age.should.equal(89); - cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d, { age: { $gt: 23 } }); - cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(3); - _.filter(docs, function(doc) { return doc.age === 57; })[0].age.should.equal(57); - _.filter(docs, function(doc) { return doc.age === 52; })[0].age.should.equal(52); - _.filter(docs, function(doc) { return doc.age === 89; })[0].age.should.equal(89); - cb(); - }); - } - ], done); - }); + const cursor = new Cursor(d) + cursor.exec(function (err, docs) { + assert.isNull(err) + docs.length.should.equal(5) + _.filter(docs, function (doc) { return doc.age === 5 })[0].age.should.equal(5) + _.filter(docs, function (doc) { return doc.age === 57 })[0].age.should.equal(57) + _.filter(docs, function (doc) { return doc.age === 52 })[0].age.should.equal(52) + _.filter(docs, function (doc) { return doc.age === 23 })[0].age.should.equal(23) + _.filter(docs, function (doc) { return doc.age === 89 })[0].age.should.equal(89) + cb() + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + cursor.exec(function (err, docs) { + assert.isNull(err) + docs.length.should.equal(5) + _.filter(docs, function (doc) { return doc.age === 5 })[0].age.should.equal(5) + _.filter(docs, function (doc) { return doc.age === 57 })[0].age.should.equal(57) + _.filter(docs, function (doc) { return doc.age === 52 })[0].age.should.equal(52) + _.filter(docs, function (doc) { return doc.age === 23 })[0].age.should.equal(23) + _.filter(docs, function (doc) { return doc.age === 89 })[0].age.should.equal(89) + cb() + }) + }, + function (cb) { + const cursor = new Cursor(d, { age: { $gt: 23 } }) + cursor.exec(function (err, docs) { + assert.isNull(err) + docs.length.should.equal(3) + _.filter(docs, function (doc) { return doc.age === 57 })[0].age.should.equal(57) + _.filter(docs, function (doc) { return doc.age === 52 })[0].age.should.equal(52) + _.filter(docs, function (doc) { return doc.age === 89 })[0].age.should.equal(89) + cb() + }) + } + ], done) + }) it('With an empty collection', function (done) { async.waterfall([ function (cb) { - d.remove({}, { multi: true }, function(err) { return cb(err); }) - } - , function (cb) { - var cursor = new Cursor(d); + d.remove({}, { multi: true }, function (err) { return cb(err) }) + }, + function (cb) { + const cursor = new Cursor(d) cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - cb(); - }); + assert.isNull(err) + docs.length.should.equal(0) + cb() + }) } - ], done); - }); + ], done) + }) it('With a limit', function (done) { - var cursor = new Cursor(d); - cursor.limit(3); + const cursor = new Cursor(d) + cursor.limit(3) cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(3); + assert.isNull(err) + docs.length.should.equal(3) // No way to predict which results are returned of course ... - done(); - }); - }); + done() + }) + }) it('With a skip', function (done) { - var cursor = new Cursor(d); + const cursor = new Cursor(d) cursor.skip(2).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(3); + assert.isNull(err) + docs.length.should.equal(3) // No way to predict which results are returned of course ... - done(); - }); - }); + done() + }) + }) it('With a limit and a skip and method chaining', function (done) { - var cursor = new Cursor(d); - cursor.limit(4).skip(3); // Only way to know that the right number of results was skipped is if limit + skip > number of results + const cursor = new Cursor(d) + cursor.limit(4).skip(3) // Only way to know that the right number of results was skipped is if limit + skip > number of results cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); + assert.isNull(err) + docs.length.should.equal(2) // No way to predict which results are returned of course ... - done(); - }); - }); - - }); // ===== End of 'Without sorting' ===== - + done() + }) + }) + }) // ===== End of 'Without sorting' ===== describe('Sorting of the results', function () { - beforeEach(function (done) { // We don't know the order in which docs wil be inserted but we ensure correctness by testing both sort orders + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 5 }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 57 }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 52 }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 23 }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 89 }, function (err) { - return done(); - }); - }); - }); - }); - }); - }); + return done() + }) + }) + }) + }) + }) + }) it('Using one sort', function (done) { - var cursor, i; - - cursor = new Cursor(d, {}); - cursor.sort({ age: 1 }); + const cursor = new Cursor(d, {}) + cursor.sort({ age: 1 }) cursor.exec(function (err, docs) { - assert.isNull(err); + assert.isNull(err) // Results are in ascending order - for (i = 0; i < docs.length - 1; i += 1) { + for (let i = 0; i < docs.length - 1; i += 1) { assert(docs[i].age < docs[i + 1].age) } - cursor.sort({ age: -1 }); + cursor.sort({ age: -1 }) cursor.exec(function (err, docs) { - assert.isNull(err); + assert.isNull(err) // Results are in descending order - for (i = 0; i < docs.length - 1; i += 1) { + for (let i = 0; i < docs.length - 1; i += 1) { assert(docs[i].age > docs[i + 1].age) } - done(); - }); - }); - }); + done() + }) + }) + }) - it("Sorting strings with custom string comparison function", function (done) { - var db = new Datastore({ inMemoryOnly: true, autoload: true - , compareStrings: function (a, b) { return a.length - b.length; } - }); + it('Sorting strings with custom string comparison function', function (done) { + const db = new Datastore({ + inMemoryOnly: true, + autoload: true, + compareStrings: function (a, b) { return a.length - b.length } + }) - db.insert({ name: 'alpha' }); - db.insert({ name: 'charlie' }); - db.insert({ name: 'zulu' }); + db.insert({ name: 'alpha' }) + db.insert({ name: 'charlie' }) + db.insert({ name: 'zulu' }) + // eslint-disable-next-line node/handle-callback-err db.find({}).sort({ name: 1 }).exec(function (err, docs) { - _.pluck(docs, 'name')[0].should.equal('zulu'); - _.pluck(docs, 'name')[1].should.equal('alpha'); - _.pluck(docs, 'name')[2].should.equal('charlie'); + _.pluck(docs, 'name')[0].should.equal('zulu') + _.pluck(docs, 'name')[1].should.equal('alpha') + _.pluck(docs, 'name')[2].should.equal('charlie') - delete db.compareStrings; + delete db.compareStrings + // eslint-disable-next-line node/handle-callback-err db.find({}).sort({ name: 1 }).exec(function (err, docs) { - _.pluck(docs, 'name')[0].should.equal('alpha'); - _.pluck(docs, 'name')[1].should.equal('charlie'); - _.pluck(docs, 'name')[2].should.equal('zulu'); + _.pluck(docs, 'name')[0].should.equal('alpha') + _.pluck(docs, 'name')[1].should.equal('charlie') + _.pluck(docs, 'name')[2].should.equal('zulu') - done(); - }); - }); - }); + done() + }) + }) + }) it('With an empty collection', function (done) { async.waterfall([ function (cb) { - d.remove({}, { multi: true }, function(err) { return cb(err); }) + d.remove({}, { multi: true }, function (err) { return cb(err) }) + }, + function (cb) { + const cursor = new Cursor(d) + cursor.sort({ age: 1 }) + cursor.exec(function (err, docs) { + assert.isNull(err) + docs.length.should.equal(0) + cb() + }) } - , function (cb) { - var cursor = new Cursor(d); - cursor.sort({ age: 1 }); - cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - cb(); - }); - } - ], done); - }); + ], done) + }) it('Ability to chain sorting and exec', function (done) { - var i; + let i async.waterfall([ function (cb) { - var cursor = new Cursor(d); + const cursor = new Cursor(d) cursor.sort({ age: 1 }).exec(function (err, docs) { - assert.isNull(err); + assert.isNull(err) // Results are in ascending order for (i = 0; i < docs.length - 1; i += 1) { assert(docs[i].age < docs[i + 1].age) } - cb(); - }); + cb() + }) + }, + function (cb) { + const cursor = new Cursor(d) + cursor.sort({ age: -1 }).exec(function (err, docs) { + assert.isNull(err) + // Results are in descending order + for (i = 0; i < docs.length - 1; i += 1) { + assert(docs[i].age > docs[i + 1].age) + } + cb() + }) } - , function (cb) { - var cursor = new Cursor(d); - cursor.sort({ age: -1 }).exec(function (err, docs) { - assert.isNull(err); - // Results are in descending order - for (i = 0; i < docs.length - 1; i += 1) { - assert(docs[i].age > docs[i + 1].age) - } - cb(); - }); - } - ], done); - }); + ], done) + }) it('Using limit and sort', function (done) { - var i; async.waterfall([ function (cb) { - var cursor = new Cursor(d); + const cursor = new Cursor(d) cursor.sort({ age: 1 }).limit(3).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(3); - docs[0].age.should.equal(5); - docs[1].age.should.equal(23); - docs[2].age.should.equal(52); - cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d); - cursor.sort({ age: -1 }).limit(2).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - docs[0].age.should.equal(89); - docs[1].age.should.equal(57); - cb(); - }); - } - ], done); - }); + assert.isNull(err) + docs.length.should.equal(3) + docs[0].age.should.equal(5) + docs[1].age.should.equal(23) + docs[2].age.should.equal(52) + cb() + }) + }, + function (cb) { + const cursor = new Cursor(d) + cursor.sort({ age: -1 }).limit(2).exec(function (err, docs) { + assert.isNull(err) + docs.length.should.equal(2) + docs[0].age.should.equal(89) + docs[1].age.should.equal(57) + cb() + }) + } + ], done) + }) it('Using a limit higher than total number of docs shouldnt cause an error', function (done) { - var i; async.waterfall([ function (cb) { - var cursor = new Cursor(d); + const cursor = new Cursor(d) cursor.sort({ age: 1 }).limit(7).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(5); - docs[0].age.should.equal(5); - docs[1].age.should.equal(23); - docs[2].age.should.equal(52); - docs[3].age.should.equal(57); - docs[4].age.should.equal(89); - cb(); - }); - } - ], done); - }); + assert.isNull(err) + docs.length.should.equal(5) + docs[0].age.should.equal(5) + docs[1].age.should.equal(23) + docs[2].age.should.equal(52) + docs[3].age.should.equal(57) + docs[4].age.should.equal(89) + cb() + }) + } + ], done) + }) it('Using limit and skip with sort', function (done) { - var i; async.waterfall([ function (cb) { - var cursor = new Cursor(d); + const cursor = new Cursor(d) cursor.sort({ age: 1 }).limit(1).skip(2).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - docs[0].age.should.equal(52); - cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d); + assert.isNull(err) + docs.length.should.equal(1) + docs[0].age.should.equal(52) + cb() + }) + }, + function (cb) { + const cursor = new Cursor(d) cursor.sort({ age: 1 }).limit(3).skip(1).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(3); - docs[0].age.should.equal(23); - docs[1].age.should.equal(52); - docs[2].age.should.equal(57); - cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d); + assert.isNull(err) + docs.length.should.equal(3) + docs[0].age.should.equal(23) + docs[1].age.should.equal(52) + docs[2].age.should.equal(57) + cb() + }) + }, + function (cb) { + const cursor = new Cursor(d) cursor.sort({ age: -1 }).limit(2).skip(2).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - docs[0].age.should.equal(52); - docs[1].age.should.equal(23); - cb(); - }); + assert.isNull(err) + docs.length.should.equal(2) + docs[0].age.should.equal(52) + docs[1].age.should.equal(23) + cb() + }) } - ], done); - }); - + ], done) + }) + it('Using too big a limit and a skip with sort', function (done) { - var i; async.waterfall([ function (cb) { - var cursor = new Cursor(d); + const cursor = new Cursor(d) cursor.sort({ age: 1 }).limit(8).skip(2).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(3); - docs[0].age.should.equal(52); - docs[1].age.should.equal(57); - docs[2].age.should.equal(89); - cb(); - }); + assert.isNull(err) + docs.length.should.equal(3) + docs[0].age.should.equal(52) + docs[1].age.should.equal(57) + docs[2].age.should.equal(89) + cb() + }) } - ], done); - }); + ], done) + }) it('Using too big a skip with sort should return no result', function (done) { - var i; async.waterfall([ function (cb) { - var cursor = new Cursor(d); + const cursor = new Cursor(d) cursor.sort({ age: 1 }).skip(5).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d); + assert.isNull(err) + docs.length.should.equal(0) + cb() + }) + }, + function (cb) { + const cursor = new Cursor(d) cursor.sort({ age: 1 }).skip(7).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d); + assert.isNull(err) + docs.length.should.equal(0) + cb() + }) + }, + function (cb) { + const cursor = new Cursor(d) cursor.sort({ age: 1 }).limit(3).skip(7).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d); + assert.isNull(err) + docs.length.should.equal(0) + cb() + }) + }, + function (cb) { + const cursor = new Cursor(d) cursor.sort({ age: 1 }).limit(6).skip(7).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - cb(); - }); + assert.isNull(err) + docs.length.should.equal(0) + cb() + }) } - ], done); - }); - + ], done) + }) + it('Sorting strings', function (done) { async.waterfall([ function (cb) { d.remove({}, { multi: true }, function (err) { - if (err) { return cb(err); } + if (err) { return cb(err) } - d.insert({ name: 'jako'}, function () { + d.insert({ name: 'jako' }, function () { d.insert({ name: 'jakeb' }, function () { d.insert({ name: 'sue' }, function () { - return cb(); - }); - }); - }); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); + return cb() + }) + }) + }) + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ name: 1 }).exec(function (err, docs) { - docs.length.should.equal(3); - docs[0].name.should.equal('jakeb'); - docs[1].name.should.equal('jako'); - docs[2].name.should.equal('sue'); - return cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); + docs.length.should.equal(3) + docs[0].name.should.equal('jakeb') + docs[1].name.should.equal('jako') + docs[2].name.should.equal('sue') + return cb() + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ name: -1 }).exec(function (err, docs) { - docs.length.should.equal(3); - docs[0].name.should.equal('sue'); - docs[1].name.should.equal('jako'); - docs[2].name.should.equal('jakeb'); - return cb(); - }); + docs.length.should.equal(3) + docs[0].name.should.equal('sue') + docs[1].name.should.equal('jako') + docs[2].name.should.equal('jakeb') + return cb() + }) } - ], done); - }); - + ], done) + }) + it('Sorting nested fields with dates', function (done) { - var doc1, doc2, doc3; - + let doc1 + let doc2 + let doc3 + async.waterfall([ function (cb) { d.remove({}, { multi: true }, function (err) { - if (err) { return cb(err); } + if (err) { return cb(err) } + // eslint-disable-next-line node/handle-callback-err d.insert({ event: { recorded: new Date(400) } }, function (err, _doc1) { - doc1 = _doc1; + doc1 = _doc1 + // eslint-disable-next-line node/handle-callback-err d.insert({ event: { recorded: new Date(60000) } }, function (err, _doc2) { - doc2 = _doc2; + doc2 = _doc2 + // eslint-disable-next-line node/handle-callback-err d.insert({ event: { recorded: new Date(32) } }, function (err, _doc3) { - doc3 = _doc3; - return cb(); - }); - }); - }); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); - cursor.sort({ "event.recorded": 1 }).exec(function (err, docs) { - docs.length.should.equal(3); - docs[0]._id.should.equal(doc3._id); - docs[1]._id.should.equal(doc1._id); - docs[2]._id.should.equal(doc2._id); - return cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); - cursor.sort({ "event.recorded": -1 }).exec(function (err, docs) { - docs.length.should.equal(3); - docs[0]._id.should.equal(doc2._id); - docs[1]._id.should.equal(doc1._id); - docs[2]._id.should.equal(doc3._id); - return cb(); - }); - } - ], done); - }); - - it('Sorting when some fields are undefined', function (done) { + doc3 = _doc3 + return cb() + }) + }) + }) + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err + cursor.sort({ 'event.recorded': 1 }).exec(function (err, docs) { + docs.length.should.equal(3) + docs[0]._id.should.equal(doc3._id) + docs[1]._id.should.equal(doc1._id) + docs[2]._id.should.equal(doc2._id) + return cb() + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err + cursor.sort({ 'event.recorded': -1 }).exec(function (err, docs) { + docs.length.should.equal(3) + docs[0]._id.should.equal(doc2._id) + docs[1]._id.should.equal(doc1._id) + docs[2]._id.should.equal(doc3._id) + return cb() + }) + } + ], done) + }) + + it('Sorting when some fields are undefined', function (done) { async.waterfall([ function (cb) { d.remove({}, { multi: true }, function (err) { - if (err) { return cb(err); } + if (err) { return cb(err) } d.insert({ name: 'jako', other: 2 }, function () { d.insert({ name: 'jakeb', other: 3 }, function () { d.insert({ name: 'sue' }, function () { d.insert({ name: 'henry', other: 4 }, function () { - return cb(); - }); - }); - }); - }); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); + return cb() + }) + }) + }) + }) + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ other: 1 }).exec(function (err, docs) { - docs.length.should.equal(4); - docs[0].name.should.equal('sue'); - assert.isUndefined(docs[0].other); - docs[1].name.should.equal('jako'); - docs[1].other.should.equal(2); - docs[2].name.should.equal('jakeb'); - docs[2].other.should.equal(3); - docs[3].name.should.equal('henry'); - docs[3].other.should.equal(4); - return cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d, { name: { $in: [ 'suzy', 'jakeb', 'jako' ] } }); + docs.length.should.equal(4) + docs[0].name.should.equal('sue') + assert.isUndefined(docs[0].other) + docs[1].name.should.equal('jako') + docs[1].other.should.equal(2) + docs[2].name.should.equal('jakeb') + docs[2].other.should.equal(3) + docs[3].name.should.equal('henry') + docs[3].other.should.equal(4) + return cb() + }) + }, + function (cb) { + const cursor = new Cursor(d, { name: { $in: ['suzy', 'jakeb', 'jako'] } }) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ other: -1 }).exec(function (err, docs) { - docs.length.should.equal(2); - docs[0].name.should.equal('jakeb'); - docs[0].other.should.equal(3); - docs[1].name.should.equal('jako'); - docs[1].other.should.equal(2); - return cb(); - }); - } - ], done); - }); - - it('Sorting when all fields are undefined', function (done) { + docs.length.should.equal(2) + docs[0].name.should.equal('jakeb') + docs[0].other.should.equal(3) + docs[1].name.should.equal('jako') + docs[1].other.should.equal(2) + return cb() + }) + } + ], done) + }) + + it('Sorting when all fields are undefined', function (done) { async.waterfall([ function (cb) { d.remove({}, { multi: true }, function (err) { - if (err) { return cb(err); } + if (err) { return cb(err) } - d.insert({ name: 'jako'}, function () { + d.insert({ name: 'jako' }, function () { d.insert({ name: 'jakeb' }, function () { d.insert({ name: 'sue' }, function () { - return cb(); - }); - }); - }); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); + return cb() + }) + }) + }) + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ other: 1 }).exec(function (err, docs) { - docs.length.should.equal(3); - return cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d, { name: { $in: [ 'sue', 'jakeb', 'jakob' ] } }); + docs.length.should.equal(3) + return cb() + }) + }, + function (cb) { + const cursor = new Cursor(d, { name: { $in: ['sue', 'jakeb', 'jakob'] } }) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ other: -1 }).exec(function (err, docs) { - docs.length.should.equal(2); - return cb(); - }); + docs.length.should.equal(2) + return cb() + }) } - ], done); - }); + ], done) + }) - it('Multiple consecutive sorts', function(done) { + it('Multiple consecutive sorts', function (done) { async.waterfall([ function (cb) { d.remove({}, { multi: true }, function (err) { - if (err) { return cb(err); } + if (err) { return cb(err) } d.insert({ name: 'jako', age: 43, nid: 1 }, function () { d.insert({ name: 'jakeb', age: 43, nid: 2 }, function () { d.insert({ name: 'sue', age: 12, nid: 3 }, function () { d.insert({ name: 'zoe', age: 23, nid: 4 }, function () { d.insert({ name: 'jako', age: 35, nid: 5 }, function () { - return cb(); - }); - }); - }); - }); - }); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); + return cb() + }) + }) + }) + }) + }) + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ name: 1, age: -1 }).exec(function (err, docs) { - docs.length.should.equal(5); - - docs[0].nid.should.equal(2); - docs[1].nid.should.equal(1); - docs[2].nid.should.equal(5); - docs[3].nid.should.equal(3); - docs[4].nid.should.equal(4); - return cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); + docs.length.should.equal(5) + + docs[0].nid.should.equal(2) + docs[1].nid.should.equal(1) + docs[2].nid.should.equal(5) + docs[3].nid.should.equal(3) + docs[4].nid.should.equal(4) + return cb() + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ name: 1, age: 1 }).exec(function (err, docs) { - docs.length.should.equal(5); - - docs[0].nid.should.equal(2); - docs[1].nid.should.equal(5); - docs[2].nid.should.equal(1); - docs[3].nid.should.equal(3); - docs[4].nid.should.equal(4); - return cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); + docs.length.should.equal(5) + + docs[0].nid.should.equal(2) + docs[1].nid.should.equal(5) + docs[2].nid.should.equal(1) + docs[3].nid.should.equal(3) + docs[4].nid.should.equal(4) + return cb() + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ age: 1, name: 1 }).exec(function (err, docs) { - docs.length.should.equal(5); - - docs[0].nid.should.equal(3); - docs[1].nid.should.equal(4); - docs[2].nid.should.equal(5); - docs[3].nid.should.equal(2); - docs[4].nid.should.equal(1); - return cb(); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); + docs.length.should.equal(5) + + docs[0].nid.should.equal(3) + docs[1].nid.should.equal(4) + docs[2].nid.should.equal(5) + docs[3].nid.should.equal(2) + docs[4].nid.should.equal(1) + return cb() + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ age: 1, name: -1 }).exec(function (err, docs) { - docs.length.should.equal(5); - - docs[0].nid.should.equal(3); - docs[1].nid.should.equal(4); - docs[2].nid.should.equal(5); - docs[3].nid.should.equal(1); - docs[4].nid.should.equal(2); - return cb(); - }); - } - ], done); }); - - it('Similar data, multiple consecutive sorts', function(done) { - var i, j, id - , companies = [ 'acme', 'milkman', 'zoinks' ] - , entities = [] - ; - + docs.length.should.equal(5) + + docs[0].nid.should.equal(3) + docs[1].nid.should.equal(4) + docs[2].nid.should.equal(5) + docs[3].nid.should.equal(1) + docs[4].nid.should.equal(2) + return cb() + }) + } + ], done) + }) + + it('Similar data, multiple consecutive sorts', function (done) { + let i + let j + let id + const companies = ['acme', 'milkman', 'zoinks'] + const entities = [] + async.waterfall([ function (cb) { d.remove({}, { multi: true }, function (err) { - if (err) { return cb(err); } - - id = 1; + if (err) { return cb(err) } + + id = 1 for (i = 0; i < companies.length; i++) { for (j = 5; j <= 100; j += 5) { entities.push({ company: companies[i], cost: j, nid: id - }); - id++; + }) + id++ } } - async.each(entities, function(entity, callback) { - d.insert(entity, function() { - callback(); - }); - }, function(err) { - return cb(); - }); - }); - } - , function (cb) { - var cursor = new Cursor(d, {}); + async.each(entities, function (entity, callback) { + d.insert(entity, function () { + callback() + }) + }, // eslint-disable-next-line node/handle-callback-err + function (err) { + return cb() + }) + }) + }, + function (cb) { + const cursor = new Cursor(d, {}) + // eslint-disable-next-line node/handle-callback-err cursor.sort({ company: 1, cost: 1 }).exec(function (err, docs) { - docs.length.should.equal(60); - - for (var i = 0; i < docs.length; i++) { - docs[i].nid.should.equal(i+1); - }; - return cb(); - }); - } - ], done); }); + docs.length.should.equal(60) - }); // ===== End of 'Sorting' ===== + for (let i = 0; i < docs.length; i++) { + docs[i].nid.should.equal(i + 1) + } + return cb() + }) + } + ], done) + }) + }) // ===== End of 'Sorting' ===== describe('Projections', function () { - var doc1, doc2, doc3, doc4, doc0; - + let doc1 + let doc2 + let doc3 + let doc4 + let doc0 beforeEach(function (done) { // We don't know the order in which docs wil be inserted but we ensure correctness by testing both sort orders + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 5, name: 'Jo', planet: 'B', toys: { bebe: true, ballon: 'much' } }, function (err, _doc0) { - doc0 = _doc0; + doc0 = _doc0 + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 57, name: 'Louis', planet: 'R', toys: { ballon: 'yeah', bebe: false } }, function (err, _doc1) { - doc1 = _doc1; + doc1 = _doc1 + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 52, name: 'Grafitti', planet: 'C', toys: { bebe: 'kind of' } }, function (err, _doc2) { - doc2 = _doc2; + doc2 = _doc2 + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 23, name: 'LM', planet: 'S' }, function (err, _doc3) { - doc3 = _doc3; + doc3 = _doc3 + // eslint-disable-next-line node/handle-callback-err d.insert({ age: 89, planet: 'Earth' }, function (err, _doc4) { - doc4 = _doc4; - return done(); - }); - }); - }); - }); - }); - }); + doc4 = _doc4 + return done() + }) + }) + }) + }) + }) + }) it('Takes all results if no projection or empty object given', function (done) { - var cursor = new Cursor(d, {}); - cursor.sort({ age: 1 }); // For easier finding + const cursor = new Cursor(d, {}) + cursor.sort({ age: 1 }) // For easier finding cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(5); - assert.deepEqual(docs[0], doc0); - assert.deepEqual(docs[1], doc3); - assert.deepEqual(docs[2], doc2); - assert.deepEqual(docs[3], doc1); - assert.deepEqual(docs[4], doc4); - - cursor.projection({}); + assert.isNull(err) + docs.length.should.equal(5) + assert.deepEqual(docs[0], doc0) + assert.deepEqual(docs[1], doc3) + assert.deepEqual(docs[2], doc2) + assert.deepEqual(docs[3], doc1) + assert.deepEqual(docs[4], doc4) + + cursor.projection({}) cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(5); - assert.deepEqual(docs[0], doc0); - assert.deepEqual(docs[1], doc3); - assert.deepEqual(docs[2], doc2); - assert.deepEqual(docs[3], doc1); - assert.deepEqual(docs[4], doc4); - - done(); - }); - }); - }); + assert.isNull(err) + docs.length.should.equal(5) + assert.deepEqual(docs[0], doc0) + assert.deepEqual(docs[1], doc3) + assert.deepEqual(docs[2], doc2) + assert.deepEqual(docs[3], doc1) + assert.deepEqual(docs[4], doc4) + + done() + }) + }) + }) it('Can take only the expected fields', function (done) { - var cursor = new Cursor(d, {}); - cursor.sort({ age: 1 }); // For easier finding - cursor.projection({ age: 1, name: 1 }); + const cursor = new Cursor(d, {}) + cursor.sort({ age: 1 }) // For easier finding + cursor.projection({ age: 1, name: 1 }) cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(5); + assert.isNull(err) + docs.length.should.equal(5) // Takes the _id by default - assert.deepEqual(docs[0], { age: 5, name: 'Jo', _id: doc0._id }); - assert.deepEqual(docs[1], { age: 23, name: 'LM', _id: doc3._id }); - assert.deepEqual(docs[2], { age: 52, name: 'Grafitti', _id: doc2._id }); - assert.deepEqual(docs[3], { age: 57, name: 'Louis', _id: doc1._id }); - assert.deepEqual(docs[4], { age: 89, _id: doc4._id }); // No problems if one field to take doesn't exist + assert.deepEqual(docs[0], { age: 5, name: 'Jo', _id: doc0._id }) + assert.deepEqual(docs[1], { age: 23, name: 'LM', _id: doc3._id }) + assert.deepEqual(docs[2], { age: 52, name: 'Grafitti', _id: doc2._id }) + assert.deepEqual(docs[3], { age: 57, name: 'Louis', _id: doc1._id }) + assert.deepEqual(docs[4], { age: 89, _id: doc4._id }) // No problems if one field to take doesn't exist - cursor.projection({ age: 1, name: 1, _id: 0 }); + cursor.projection({ age: 1, name: 1, _id: 0 }) cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(5); - assert.deepEqual(docs[0], { age: 5, name: 'Jo' }); - assert.deepEqual(docs[1], { age: 23, name: 'LM' }); - assert.deepEqual(docs[2], { age: 52, name: 'Grafitti' }); - assert.deepEqual(docs[3], { age: 57, name: 'Louis' }); - assert.deepEqual(docs[4], { age: 89 }); // No problems if one field to take doesn't exist - - done(); - }); - }); - }); + assert.isNull(err) + docs.length.should.equal(5) + assert.deepEqual(docs[0], { age: 5, name: 'Jo' }) + assert.deepEqual(docs[1], { age: 23, name: 'LM' }) + assert.deepEqual(docs[2], { age: 52, name: 'Grafitti' }) + assert.deepEqual(docs[3], { age: 57, name: 'Louis' }) + assert.deepEqual(docs[4], { age: 89 }) // No problems if one field to take doesn't exist + + done() + }) + }) + }) it('Can omit only the expected fields', function (done) { - var cursor = new Cursor(d, {}); - cursor.sort({ age: 1 }); // For easier finding - cursor.projection({ age: 0, name: 0 }); + const cursor = new Cursor(d, {}) + cursor.sort({ age: 1 }) // For easier finding + cursor.projection({ age: 0, name: 0 }) cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(5); + assert.isNull(err) + docs.length.should.equal(5) // Takes the _id by default - assert.deepEqual(docs[0], { planet: 'B', _id: doc0._id, toys: { bebe: true, ballon: 'much' } }); - assert.deepEqual(docs[1], { planet: 'S', _id: doc3._id }); - assert.deepEqual(docs[2], { planet: 'C', _id: doc2._id, toys: { bebe: 'kind of' } }); - assert.deepEqual(docs[3], { planet: 'R', _id: doc1._id, toys: { bebe: false, ballon: 'yeah' } }); - assert.deepEqual(docs[4], { planet: 'Earth', _id: doc4._id }); + assert.deepEqual(docs[0], { planet: 'B', _id: doc0._id, toys: { bebe: true, ballon: 'much' } }) + assert.deepEqual(docs[1], { planet: 'S', _id: doc3._id }) + assert.deepEqual(docs[2], { planet: 'C', _id: doc2._id, toys: { bebe: 'kind of' } }) + assert.deepEqual(docs[3], { planet: 'R', _id: doc1._id, toys: { bebe: false, ballon: 'yeah' } }) + assert.deepEqual(docs[4], { planet: 'Earth', _id: doc4._id }) - cursor.projection({ age: 0, name: 0, _id: 0 }); + cursor.projection({ age: 0, name: 0, _id: 0 }) cursor.exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(5); - assert.deepEqual(docs[0], { planet: 'B', toys: { bebe: true, ballon: 'much' } }); - assert.deepEqual(docs[1], { planet: 'S' }); - assert.deepEqual(docs[2], { planet: 'C', toys: { bebe: 'kind of' } }); - assert.deepEqual(docs[3], { planet: 'R', toys: { bebe: false, ballon: 'yeah' } }); - assert.deepEqual(docs[4], { planet: 'Earth' }); - - done(); - }); - }); - }); + assert.isNull(err) + docs.length.should.equal(5) + assert.deepEqual(docs[0], { planet: 'B', toys: { bebe: true, ballon: 'much' } }) + assert.deepEqual(docs[1], { planet: 'S' }) + assert.deepEqual(docs[2], { planet: 'C', toys: { bebe: 'kind of' } }) + assert.deepEqual(docs[3], { planet: 'R', toys: { bebe: false, ballon: 'yeah' } }) + assert.deepEqual(docs[4], { planet: 'Earth' }) + + done() + }) + }) + }) it('Cannot use both modes except for _id', function (done) { - var cursor = new Cursor(d, {}); - cursor.sort({ age: 1 }); // For easier finding - cursor.projection({ age: 1, name: 0 }); + const cursor = new Cursor(d, {}) + cursor.sort({ age: 1 }) // For easier finding + cursor.projection({ age: 1, name: 0 }) cursor.exec(function (err, docs) { - assert.isNotNull(err); - assert.isUndefined(docs); + assert.isNotNull(err) + assert.isUndefined(docs) - cursor.projection({ age: 1, _id: 0 }); + cursor.projection({ age: 1, _id: 0 }) cursor.exec(function (err, docs) { - assert.isNull(err); - assert.deepEqual(docs[0], { age: 5 }); - assert.deepEqual(docs[1], { age: 23 }); - assert.deepEqual(docs[2], { age: 52 }); - assert.deepEqual(docs[3], { age: 57 }); - assert.deepEqual(docs[4], { age: 89 }); - - cursor.projection({ age: 0, toys: 0, planet: 0, _id: 1 }); + assert.isNull(err) + assert.deepEqual(docs[0], { age: 5 }) + assert.deepEqual(docs[1], { age: 23 }) + assert.deepEqual(docs[2], { age: 52 }) + assert.deepEqual(docs[3], { age: 57 }) + assert.deepEqual(docs[4], { age: 89 }) + + cursor.projection({ age: 0, toys: 0, planet: 0, _id: 1 }) cursor.exec(function (err, docs) { - assert.isNull(err); - assert.deepEqual(docs[0], { name: 'Jo', _id: doc0._id }); - assert.deepEqual(docs[1], { name: 'LM', _id: doc3._id }); - assert.deepEqual(docs[2], { name: 'Grafitti', _id: doc2._id }); - assert.deepEqual(docs[3], { name: 'Louis', _id: doc1._id }); - assert.deepEqual(docs[4], { _id: doc4._id }); - - done(); - }); - }); - }); - }); - - it("Projections on embedded documents - omit type", function (done) { - var cursor = new Cursor(d, {}); - cursor.sort({ age: 1 }); // For easier finding - cursor.projection({ name: 0, planet: 0, 'toys.bebe': 0, _id: 0 }); + assert.isNull(err) + assert.deepEqual(docs[0], { name: 'Jo', _id: doc0._id }) + assert.deepEqual(docs[1], { name: 'LM', _id: doc3._id }) + assert.deepEqual(docs[2], { name: 'Grafitti', _id: doc2._id }) + assert.deepEqual(docs[3], { name: 'Louis', _id: doc1._id }) + assert.deepEqual(docs[4], { _id: doc4._id }) + + done() + }) + }) + }) + }) + + it('Projections on embedded documents - omit type', function (done) { + const cursor = new Cursor(d, {}) + cursor.sort({ age: 1 }) // For easier finding + cursor.projection({ name: 0, planet: 0, 'toys.bebe': 0, _id: 0 }) + // eslint-disable-next-line node/handle-callback-err cursor.exec(function (err, docs) { - assert.deepEqual(docs[0], { age: 5, toys: { ballon: 'much' } }); - assert.deepEqual(docs[1], { age: 23 }); - assert.deepEqual(docs[2], { age: 52, toys: {} }); - assert.deepEqual(docs[3], { age: 57, toys: { ballon: 'yeah' } }); - assert.deepEqual(docs[4], { age: 89 }); - - done(); - }); - }); - - it("Projections on embedded documents - pick type", function (done) { - var cursor = new Cursor(d, {}); - cursor.sort({ age: 1 }); // For easier finding - cursor.projection({ name: 1, 'toys.ballon': 1, _id: 0 }); + assert.deepEqual(docs[0], { age: 5, toys: { ballon: 'much' } }) + assert.deepEqual(docs[1], { age: 23 }) + assert.deepEqual(docs[2], { age: 52, toys: {} }) + assert.deepEqual(docs[3], { age: 57, toys: { ballon: 'yeah' } }) + assert.deepEqual(docs[4], { age: 89 }) + + done() + }) + }) + + it('Projections on embedded documents - pick type', function (done) { + const cursor = new Cursor(d, {}) + cursor.sort({ age: 1 }) // For easier finding + cursor.projection({ name: 1, 'toys.ballon': 1, _id: 0 }) + // eslint-disable-next-line node/handle-callback-err cursor.exec(function (err, docs) { - assert.deepEqual(docs[0], { name: 'Jo', toys: { ballon: 'much' } }); - assert.deepEqual(docs[1], { name: 'LM' }); - assert.deepEqual(docs[2], { name: 'Grafitti' }); - assert.deepEqual(docs[3], { name: 'Louis', toys: { ballon: 'yeah' } }); - assert.deepEqual(docs[4], {}); - - done(); - }); - }); - - }); // ==== End of 'Projections' ==== - -}); - - - - - - - - + assert.deepEqual(docs[0], { name: 'Jo', toys: { ballon: 'much' } }) + assert.deepEqual(docs[1], { name: 'LM' }) + assert.deepEqual(docs[2], { name: 'Grafitti' }) + assert.deepEqual(docs[3], { name: 'Louis', toys: { ballon: 'yeah' } }) + assert.deepEqual(docs[4], {}) + + done() + }) + }) + }) // ==== End of 'Projections' ==== +}) diff --git a/test/customUtil.test.js b/test/customUtil.test.js index cb69def..c92c894 100755 --- a/test/customUtil.test.js +++ b/test/customUtil.test.js @@ -1,26 +1,20 @@ -var should = require('chai').should() - , assert = require('chai').assert - , customUtils = require('../lib/customUtils') - , fs = require('fs') - ; - +/* eslint-env mocha */ +const chai = require('chai') +const customUtils = require('../lib/customUtils') +chai.should() describe('customUtils', function () { - describe('uid', function () { - it('Generates a string of the expected length', function () { - customUtils.uid(3).length.should.equal(3); - customUtils.uid(16).length.should.equal(16); - customUtils.uid(42).length.should.equal(42); - customUtils.uid(1000).length.should.equal(1000); - }); + customUtils.uid(3).length.should.equal(3) + customUtils.uid(16).length.should.equal(16) + customUtils.uid(42).length.should.equal(42) + customUtils.uid(1000).length.should.equal(1000) + }) // Very small probability of conflict it('Generated uids should not be the same', function () { - customUtils.uid(56).should.not.equal(customUtils.uid(56)); - }); - - }); - -}); + customUtils.uid(56).should.not.equal(customUtils.uid(56)) + }) + }) +}) diff --git a/test/db.test.js b/test/db.test.js index ab68788..6015793 100755 --- a/test/db.test.js +++ b/test/db.test.js @@ -1,383 +1,422 @@ -var should = require('chai').should() - , assert = require('chai').assert - , testDb = 'workspace/test.db' - , fs = require('fs') - , path = require('path') - , _ = require('underscore') - , async = require('async') - , model = require('../lib/model') - , Datastore = require('../lib/datastore') - , Persistence = require('../lib/persistence') - , reloadTimeUpperBound = 60; // In ms, an upper bound for the reload time used to check createdAt and updatedAt - ; - +/* eslint-env mocha */ +const chai = require('chai') +const testDb = 'workspace/test.db' +const fs = require('fs') +const path = require('path') +const _ = require('underscore') +const async = require('async') +const model = require('../lib/model') +const Datastore = require('../lib/datastore') +const Persistence = require('../lib/persistence') +const reloadTimeUpperBound = 60 // In ms, an upper bound for the reload time used to check createdAt and updatedAt + +const { assert } = chai +chai.should() describe('Database', function () { - var d; + let d beforeEach(function (done) { - d = new Datastore({ filename: testDb }); - d.filename.should.equal(testDb); - d.inMemoryOnly.should.equal(false); + d = new Datastore({ filename: testDb }) + d.filename.should.equal(testDb) + d.inMemoryOnly.should.equal(false) async.waterfall([ function (cb) { Persistence.ensureDirectoryExists(path.dirname(testDb), function () { - fs.exists(testDb, function (exists) { - if (exists) { - fs.unlink(testDb, cb); - } else { return cb(); } - }); - }); - } - , function (cb) { + fs.access(testDb, fs.constants.FS_OK, function (err) { + if (!err) { + fs.unlink(testDb, cb) + } else { return cb() } + }) + }) + }, + function (cb) { d.loadDatabase(function (err) { - assert.isNull(err); - d.getAllData().length.should.equal(0); - return cb(); - }); + assert.isNull(err) + d.getAllData().length.should.equal(0) + return cb() + }) } - ], done); - }); + ], done) + }) it('Constructor compatibility with v0.6-', function () { - var dbef = new Datastore('somefile'); - dbef.filename.should.equal('somefile'); - dbef.inMemoryOnly.should.equal(false); + let dbef = new Datastore('somefile') + dbef.filename.should.equal('somefile') + dbef.inMemoryOnly.should.equal(false) - var dbef = new Datastore(''); - assert.isNull(dbef.filename); - dbef.inMemoryOnly.should.equal(true); + dbef = new Datastore('') + assert.isNull(dbef.filename) + dbef.inMemoryOnly.should.equal(true) - var dbef = new Datastore(); - assert.isNull(dbef.filename); - dbef.inMemoryOnly.should.equal(true); - }); + dbef = new Datastore() + assert.isNull(dbef.filename) + dbef.inMemoryOnly.should.equal(true) + }) describe('Autoloading', function () { - it('Can autoload a database and query it right away', function (done) { - var fileStr = model.serialize({ _id: '1', a: 5, planet: 'Earth' }) + '\n' + model.serialize({ _id: '2', a: 5, planet: 'Mars' }) + '\n' - , autoDb = 'workspace/auto.db' - , db - ; + const fileStr = model.serialize({ _id: '1', a: 5, planet: 'Earth' }) + '\n' + model.serialize({ + _id: '2', + a: 5, + planet: 'Mars' + }) + '\n' + const autoDb = 'workspace/auto.db' - fs.writeFileSync(autoDb, fileStr, 'utf8'); - db = new Datastore({ filename: autoDb, autoload: true }) + fs.writeFileSync(autoDb, fileStr, 'utf8') + const db = new Datastore({ filename: autoDb, autoload: true }) db.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - done(); - }); - }); + assert.isNull(err) + docs.length.should.equal(2) + done() + }) + }) it('Throws if autoload fails', function (done) { - var fileStr = model.serialize({ _id: '1', a: 5, planet: 'Earth' }) + '\n' + model.serialize({ _id: '2', a: 5, planet: 'Mars' }) + '\n' + '{"$$indexCreated":{"fieldName":"a","unique":true}}' - , autoDb = 'workspace/auto.db' - , db - ; + const fileStr = model.serialize({ _id: '1', a: 5, planet: 'Earth' }) + '\n' + model.serialize({ + _id: '2', + a: 5, + planet: 'Mars' + }) + '\n' + '{"$$indexCreated":{"fieldName":"a","unique":true}}' + const autoDb = 'workspace/auto.db' - fs.writeFileSync(autoDb, fileStr, 'utf8'); + fs.writeFileSync(autoDb, fileStr, 'utf8') // Check the loadDatabase generated an error function onload (err) { - err.errorType.should.equal('uniqueViolated'); - done(); + err.errorType.should.equal('uniqueViolated') + done() } - db = new Datastore({ filename: autoDb, autoload: true, onload: onload }) + const db = new Datastore({ filename: autoDb, autoload: true, onload: onload }) + // eslint-disable-next-line node/handle-callback-err db.find({}, function (err, docs) { - done(new Error("Find should not be executed since autoload failed")); - }); - }); - - }); + done(new Error('Find should not be executed since autoload failed')) + }) + }) + }) describe('Insert', function () { - it('Able to insert a document in the database, setting an _id if none provided, and retrieve it even after a reload', function (done) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(0); + docs.length.should.equal(0) + // eslint-disable-next-line node/handle-callback-err d.insert({ somedata: 'ok' }, function (err) { // The data was correctly updated d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - Object.keys(docs[0]).length.should.equal(2); - docs[0].somedata.should.equal('ok'); - assert.isDefined(docs[0]._id); + assert.isNull(err) + docs.length.should.equal(1) + Object.keys(docs[0]).length.should.equal(2) + docs[0].somedata.should.equal('ok') + assert.isDefined(docs[0]._id) // After a reload the data has been correctly persisted + // eslint-disable-next-line node/handle-callback-err d.loadDatabase(function (err) { d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - Object.keys(docs[0]).length.should.equal(2); - docs[0].somedata.should.equal('ok'); - assert.isDefined(docs[0]._id); - - done(); - }); - }); - }); - }); - }); - }); + assert.isNull(err) + docs.length.should.equal(1) + Object.keys(docs[0]).length.should.equal(2) + docs[0].somedata.should.equal('ok') + assert.isDefined(docs[0]._id) + + done() + }) + }) + }) + }) + }) + }) it('Can insert multiple documents in the database', function (done) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(0); + docs.length.should.equal(0) + // eslint-disable-next-line node/handle-callback-err d.insert({ somedata: 'ok' }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ somedata: 'another' }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ somedata: 'again' }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(3); - _.pluck(docs, 'somedata').should.contain('ok'); - _.pluck(docs, 'somedata').should.contain('another'); - _.pluck(docs, 'somedata').should.contain('again'); - done(); - }); - }); - }); - }); - }); - }); + docs.length.should.equal(3) + _.pluck(docs, 'somedata').should.contain('ok') + _.pluck(docs, 'somedata').should.contain('another') + _.pluck(docs, 'somedata').should.contain('again') + done() + }) + }) + }) + }) + }) + }) it('Can insert and get back from DB complex objects with all primitive and secondary types', function (done) { - var da = new Date() - , obj = { a: ['ee', 'ff', 42], date: da, subobj: { a: 'b', b: 'c' } } - ; + const da = new Date() + const obj = { a: ['ee', 'ff', 42], date: da, subobj: { a: 'b', b: 'c' } } + // eslint-disable-next-line node/handle-callback-err d.insert(obj, function (err) { d.findOne({}, function (err, res) { - assert.isNull(err); - res.a.length.should.equal(3); - res.a[0].should.equal('ee'); - res.a[1].should.equal('ff'); - res.a[2].should.equal(42); - res.date.getTime().should.equal(da.getTime()); - res.subobj.a.should.equal('b'); - res.subobj.b.should.equal('c'); - - done(); - }); - }); - }); + assert.isNull(err) + res.a.length.should.equal(3) + res.a[0].should.equal('ee') + res.a[1].should.equal('ff') + res.a[2].should.equal(42) + res.date.getTime().should.equal(da.getTime()) + res.subobj.a.should.equal('b') + res.subobj.b.should.equal('c') + + done() + }) + }) + }) it('If an object returned from the DB is modified and refetched, the original value should be found', function (done) { d.insert({ a: 'something' }, function () { + // eslint-disable-next-line node/handle-callback-err d.findOne({}, function (err, doc) { - doc.a.should.equal('something'); - doc.a = 'another thing'; - doc.a.should.equal('another thing'); + doc.a.should.equal('something') + doc.a = 'another thing' + doc.a.should.equal('another thing') // Re-fetching with findOne should yield the persisted value + // eslint-disable-next-line node/handle-callback-err d.findOne({}, function (err, doc) { - doc.a.should.equal('something'); - doc.a = 'another thing'; - doc.a.should.equal('another thing'); + doc.a.should.equal('something') + doc.a = 'another thing' + doc.a.should.equal('another thing') // Re-fetching with find should yield the persisted value + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs[0].a.should.equal('something'); + docs[0].a.should.equal('something') - done(); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) it('Cannot insert a doc that has a field beginning with a $ sign', function (done) { d.insert({ $something: 'atest' }, function (err) { - assert.isDefined(err); - done(); - }); - }); + assert.isDefined(err) + done() + }) + }) it('If an _id is already given when we insert a document, use that instead of generating a random one', function (done) { d.insert({ _id: 'test', stuff: true }, function (err, newDoc) { - if (err) { return done(err); } + if (err) { return done(err) } - newDoc.stuff.should.equal(true); - newDoc._id.should.equal('test'); + newDoc.stuff.should.equal(true) + newDoc._id.should.equal('test') d.insert({ _id: 'test', otherstuff: 42 }, function (err) { - err.errorType.should.equal('uniqueViolated'); + err.errorType.should.equal('uniqueViolated') - done(); - }); - }); - }); + done() + }) + }) + }) it('Modifying the insertedDoc after an insert doesnt change the copy saved in the database', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, hello: 'world' }, function (err, newDoc) { - newDoc.hello = 'changed'; + newDoc.hello = 'changed' + // eslint-disable-next-line node/handle-callback-err d.findOne({ a: 2 }, function (err, doc) { - doc.hello.should.equal('world'); - done(); - }); - }); - }); + doc.hello.should.equal('world') + done() + }) + }) + }) it('Can insert an array of documents at once', function (done) { - var docs = [{ a: 5, b: 'hello' }, { a: 42, b: 'world' }]; + const docs = [{ a: 5, b: 'hello' }, { a: 42, b: 'world' }] + // eslint-disable-next-line node/handle-callback-err d.insert(docs, function (err) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - var data; - - docs.length.should.equal(2); - _.find(docs, function (doc) { return doc.a === 5; }).b.should.equal('hello'); - _.find(docs, function (doc) { return doc.a === 42; }).b.should.equal('world'); + docs.length.should.equal(2) + _.find(docs, function (doc) { return doc.a === 5 }).b.should.equal('hello') + _.find(docs, function (doc) { return doc.a === 42 }).b.should.equal('world') // The data has been persisted correctly - data = _.filter(fs.readFileSync(testDb, 'utf8').split('\n'), function (line) { return line.length > 0; }); - data.length.should.equal(2); - model.deserialize(data[0]).a.should.equal(5); - model.deserialize(data[0]).b.should.equal('hello'); - model.deserialize(data[1]).a.should.equal(42); - model.deserialize(data[1]).b.should.equal('world'); - - done(); - }); - }); - }); + const data = _.filter(fs.readFileSync(testDb, 'utf8').split('\n'), function (line) { return line.length > 0 }) + data.length.should.equal(2) + model.deserialize(data[0]).a.should.equal(5) + model.deserialize(data[0]).b.should.equal('hello') + model.deserialize(data[1]).a.should.equal(42) + model.deserialize(data[1]).b.should.equal('world') + + done() + }) + }) + }) it('If a bulk insert violates a constraint, all changes are rolled back', function (done) { - var docs = [{ a: 5, b: 'hello' }, { a: 42, b: 'world' }, { a: 5, b: 'bloup' }, { a: 7 }]; + const docs = [{ a: 5, b: 'hello' }, { a: 42, b: 'world' }, { a: 5, b: 'bloup' }, { a: 7 }] - d.ensureIndex({ fieldName: 'a', unique: true }, function () { // Important to specify callback here to make sure filesystem synced + d.ensureIndex({ fieldName: 'a', unique: true }, function () { // Important to specify callback here to make sure filesystem synced d.insert(docs, function (err) { - err.errorType.should.equal('uniqueViolated'); + err.errorType.should.equal('uniqueViolated') + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { // Datafile only contains index definition - var datafileContents = model.deserialize(fs.readFileSync(testDb, 'utf8')); - assert.deepEqual(datafileContents, { $$indexCreated: { fieldName: 'a', unique: true } }); + const datafileContents = model.deserialize(fs.readFileSync(testDb, 'utf8')) + assert.deepEqual(datafileContents, { $$indexCreated: { fieldName: 'a', unique: true } }) - docs.length.should.equal(0); + docs.length.should.equal(0) - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) - it("If timestampData option is set, a createdAt field is added and persisted", function (done) { - var newDoc = { hello: 'world' }, beginning = Date.now(); - d = new Datastore({ filename: testDb, timestampData: true, autoload: true }); + it('If timestampData option is set, a createdAt field is added and persisted', function (done) { + const newDoc = { hello: 'world' }; const beginning = Date.now() + d = new Datastore({ filename: testDb, timestampData: true, autoload: true }) d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); + assert.isNull(err) + docs.length.should.equal(0) + // eslint-disable-next-line node/handle-callback-err d.insert(newDoc, function (err, insertedDoc) { // No side effect on given input - assert.deepEqual(newDoc, { hello: 'world' }); + assert.deepEqual(newDoc, { hello: 'world' }) // Insert doc has two new fields, _id and createdAt - insertedDoc.hello.should.equal('world'); - assert.isDefined(insertedDoc.createdAt); - assert.isDefined(insertedDoc.updatedAt); - insertedDoc.createdAt.should.equal(insertedDoc.updatedAt); - assert.isDefined(insertedDoc._id); - Object.keys(insertedDoc).length.should.equal(4); - assert.isBelow(Math.abs(insertedDoc.createdAt.getTime() - beginning), reloadTimeUpperBound); // No more than 30ms should have elapsed (worst case, if there is a flush) + insertedDoc.hello.should.equal('world') + assert.isDefined(insertedDoc.createdAt) + assert.isDefined(insertedDoc.updatedAt) + insertedDoc.createdAt.should.equal(insertedDoc.updatedAt) + assert.isDefined(insertedDoc._id) + Object.keys(insertedDoc).length.should.equal(4) + assert.isBelow(Math.abs(insertedDoc.createdAt.getTime() - beginning), reloadTimeUpperBound) // No more than 30ms should have elapsed (worst case, if there is a flush) // Modifying results of insert doesn't change the cache - insertedDoc.bloup = "another"; - Object.keys(insertedDoc).length.should.equal(5); + insertedDoc.bloup = 'another' + Object.keys(insertedDoc).length.should.equal(5) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - assert.deepEqual(newDoc, { hello: 'world' }); - assert.deepEqual({ hello: 'world', _id: insertedDoc._id, createdAt: insertedDoc.createdAt, updatedAt: insertedDoc.updatedAt }, docs[0]); + docs.length.should.equal(1) + assert.deepEqual(newDoc, { hello: 'world' }) + assert.deepEqual({ + hello: 'world', + _id: insertedDoc._id, + createdAt: insertedDoc.createdAt, + updatedAt: insertedDoc.updatedAt + }, docs[0]) // All data correctly persisted on disk d.loadDatabase(function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - assert.deepEqual(newDoc, { hello: 'world' }); - assert.deepEqual({ hello: 'world', _id: insertedDoc._id, createdAt: insertedDoc.createdAt, updatedAt: insertedDoc.updatedAt }, docs[0]); - - done(); - }); - }); - }); - }); - }); - }); - - it("If timestampData option not set, don't create a createdAt and a updatedAt field", function (done) { + docs.length.should.equal(1) + assert.deepEqual(newDoc, { hello: 'world' }) + assert.deepEqual({ + hello: 'world', + _id: insertedDoc._id, + createdAt: insertedDoc.createdAt, + updatedAt: insertedDoc.updatedAt + }, docs[0]) + + done() + }) + }) + }) + }) + }) + }) + + it('If timestampData option not set, don\'t create a createdAt and a updatedAt field', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ hello: 'world' }, function (err, insertedDoc) { - Object.keys(insertedDoc).length.should.equal(2); - assert.isUndefined(insertedDoc.createdAt); - assert.isUndefined(insertedDoc.updatedAt); + Object.keys(insertedDoc).length.should.equal(2) + assert.isUndefined(insertedDoc.createdAt) + assert.isUndefined(insertedDoc.updatedAt) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - assert.deepEqual(docs[0], insertedDoc); + docs.length.should.equal(1) + assert.deepEqual(docs[0], insertedDoc) - done(); - }); - }); - }); + done() + }) + }) + }) - it("If timestampData is set but createdAt is specified by user, don't change it", function (done) { - var newDoc = { hello: 'world', createdAt: new Date(234) }, beginning = Date.now(); - d = new Datastore({ filename: testDb, timestampData: true, autoload: true }); + it('If timestampData is set but createdAt is specified by user, don\'t change it', function (done) { + const newDoc = { hello: 'world', createdAt: new Date(234) }; const beginning = Date.now() + d = new Datastore({ filename: testDb, timestampData: true, autoload: true }) + // eslint-disable-next-line node/handle-callback-err d.insert(newDoc, function (err, insertedDoc) { - Object.keys(insertedDoc).length.should.equal(4); - insertedDoc.createdAt.getTime().should.equal(234); // Not modified - assert.isBelow(insertedDoc.updatedAt.getTime() - beginning, reloadTimeUpperBound); // Created + Object.keys(insertedDoc).length.should.equal(4) + insertedDoc.createdAt.getTime().should.equal(234) // Not modified + assert.isBelow(insertedDoc.updatedAt.getTime() - beginning, reloadTimeUpperBound) // Created + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - assert.deepEqual(insertedDoc, docs[0]); + assert.deepEqual(insertedDoc, docs[0]) d.loadDatabase(function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - assert.deepEqual(insertedDoc, docs[0]); - - done(); - }); - }); - }); - }); - }); - - it("If timestampData is set but updatedAt is specified by user, don't change it", function (done) { - var newDoc = { hello: 'world', updatedAt: new Date(234) }, beginning = Date.now(); - d = new Datastore({ filename: testDb, timestampData: true, autoload: true }); + assert.deepEqual(insertedDoc, docs[0]) + + done() + }) + }) + }) + }) + }) + + it('If timestampData is set but updatedAt is specified by user, don\'t change it', function (done) { + const newDoc = { hello: 'world', updatedAt: new Date(234) }; const beginning = Date.now() + d = new Datastore({ filename: testDb, timestampData: true, autoload: true }) + // eslint-disable-next-line node/handle-callback-err d.insert(newDoc, function (err, insertedDoc) { - Object.keys(insertedDoc).length.should.equal(4); - insertedDoc.updatedAt.getTime().should.equal(234); // Not modified - assert.isBelow(insertedDoc.createdAt.getTime() - beginning, reloadTimeUpperBound); // Created + Object.keys(insertedDoc).length.should.equal(4) + insertedDoc.updatedAt.getTime().should.equal(234) // Not modified + assert.isBelow(insertedDoc.createdAt.getTime() - beginning, reloadTimeUpperBound) // Created + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - assert.deepEqual(insertedDoc, docs[0]); + assert.deepEqual(insertedDoc, docs[0]) d.loadDatabase(function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - assert.deepEqual(insertedDoc, docs[0]); + assert.deepEqual(insertedDoc, docs[0]) - done(); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) it('Can insert a doc with id 0', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ _id: 0, hello: 'world' }, function (err, doc) { - doc._id.should.equal(0); - doc.hello.should.equal('world'); - done(); - }); - }); + doc._id.should.equal(0) + doc.hello.should.equal('world') + done() + }) + }) /** * Complicated behavior here. Basically we need to test that when a user function throws an exception, it is not caught @@ -391,2486 +430,2598 @@ describe('Database', function () { * Note: maybe using an in-memory only NeDB would give us an easier solution */ it('If the callback throws an uncaught exception, do not catch it inside findOne, this is userspace concern', function (done) { - var tryCount = 0 - , currentUncaughtExceptionHandlers = process.listeners('uncaughtException') - , i - ; + let tryCount = 0 + const currentUncaughtExceptionHandlers = process.listeners('uncaughtException') + let i - process.removeAllListeners('uncaughtException'); + process.removeAllListeners('uncaughtException') process.on('uncaughtException', function MINE (ex) { - process.removeAllListeners('uncaughtException'); + process.removeAllListeners('uncaughtException') for (i = 0; i < currentUncaughtExceptionHandlers.length; i += 1) { - process.on('uncaughtException', currentUncaughtExceptionHandlers[i]); + process.on('uncaughtException', currentUncaughtExceptionHandlers[i]) } - ex.message.should.equal('SOME EXCEPTION'); - done(); - }); + ex.message.should.equal('SOME EXCEPTION') + done() + }) d.insert({ a: 5 }, function () { - d.findOne({ a : 5}, function (err, doc) { + // eslint-disable-next-line node/handle-callback-err + d.findOne({ a: 5 }, function (err, doc) { if (tryCount === 0) { - tryCount += 1; - throw new Error('SOME EXCEPTION'); + tryCount += 1 + throw new Error('SOME EXCEPTION') } else { - done(new Error('Callback was called twice')); + done(new Error('Callback was called twice')) } - }); - }); - }); - - }); // ==== End of 'Insert' ==== // - + }) + }) + }) + }) // ==== End of 'Insert' ==== // describe('#getCandidates', function () { - it('Can use an index to get docs with a basic match', function (done) { + // eslint-disable-next-line node/handle-callback-err d.ensureIndex({ fieldName: 'tf' }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 4 }, function (err, _doc1) { d.insert({ tf: 6 }, function () { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 4, an: 'other' }, function (err, _doc2) { d.insert({ tf: 9 }, function () { + // eslint-disable-next-line node/handle-callback-err d.getCandidates({ r: 6, tf: 4 }, function (err, data) { - var doc1 = _.find(data, function (d) { return d._id === _doc1._id; }) - , doc2 = _.find(data, function (d) { return d._id === _doc2._id; }) - ; - - data.length.should.equal(2); - assert.deepEqual(doc1, { _id: doc1._id, tf: 4 }); - assert.deepEqual(doc2, { _id: doc2._id, tf: 4, an: 'other' }); - - done(); - }); - }); - }); - }); - }); - }); - }); + const doc1 = _.find(data, function (d) { return d._id === _doc1._id }) + const doc2 = _.find(data, function (d) { return d._id === _doc2._id }) + + data.length.should.equal(2) + assert.deepEqual(doc1, { _id: doc1._id, tf: 4 }) + assert.deepEqual(doc2, { _id: doc2._id, tf: 4, an: 'other' }) + + done() + }) + }) + }) + }) + }) + }) + }) it('Can use an index to get docs with a $in match', function (done) { + // eslint-disable-next-line node/handle-callback-err d.ensureIndex({ fieldName: 'tf' }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 4 }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 6 }, function (err, _doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 4, an: 'other' }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 9 }, function (err, _doc2) { + // eslint-disable-next-line node/handle-callback-err d.getCandidates({ r: 6, tf: { $in: [6, 9, 5] } }, function (err, data) { - var doc1 = _.find(data, function (d) { return d._id === _doc1._id; }) - , doc2 = _.find(data, function (d) { return d._id === _doc2._id; }) - ; - - data.length.should.equal(2); - assert.deepEqual(doc1, { _id: doc1._id, tf: 6 }); - assert.deepEqual(doc2, { _id: doc2._id, tf: 9 }); - - done(); - }); - }); - }); - }); - }); - }); - }); + const doc1 = _.find(data, function (d) { return d._id === _doc1._id }) + const doc2 = _.find(data, function (d) { return d._id === _doc2._id }) + + data.length.should.equal(2) + assert.deepEqual(doc1, { _id: doc1._id, tf: 6 }) + assert.deepEqual(doc2, { _id: doc2._id, tf: 9 }) + + done() + }) + }) + }) + }) + }) + }) + }) it('If no index can be used, return the whole database', function (done) { + // eslint-disable-next-line node/handle-callback-err d.ensureIndex({ fieldName: 'tf' }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 4 }, function (err, _doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 6 }, function (err, _doc2) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 4, an: 'other' }, function (err, _doc3) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 9 }, function (err, _doc4) { + // eslint-disable-next-line node/handle-callback-err d.getCandidates({ r: 6, notf: { $in: [6, 9, 5] } }, function (err, data) { - var doc1 = _.find(data, function (d) { return d._id === _doc1._id; }) - , doc2 = _.find(data, function (d) { return d._id === _doc2._id; }) - , doc3 = _.find(data, function (d) { return d._id === _doc3._id; }) - , doc4 = _.find(data, function (d) { return d._id === _doc4._id; }) - ; - - data.length.should.equal(4); - assert.deepEqual(doc1, { _id: doc1._id, tf: 4 }); - assert.deepEqual(doc2, { _id: doc2._id, tf: 6 }); - assert.deepEqual(doc3, { _id: doc3._id, tf: 4, an: 'other' }); - assert.deepEqual(doc4, { _id: doc4._id, tf: 9 }); - - done(); - }); - }); - }); - }); - }); - }); - }); + const doc1 = _.find(data, function (d) { return d._id === _doc1._id }) + const doc2 = _.find(data, function (d) { return d._id === _doc2._id }) + const doc3 = _.find(data, function (d) { return d._id === _doc3._id }) + const doc4 = _.find(data, function (d) { return d._id === _doc4._id }) + + data.length.should.equal(4) + assert.deepEqual(doc1, { _id: doc1._id, tf: 4 }) + assert.deepEqual(doc2, { _id: doc2._id, tf: 6 }) + assert.deepEqual(doc3, { _id: doc3._id, tf: 4, an: 'other' }) + assert.deepEqual(doc4, { _id: doc4._id, tf: 9 }) + + done() + }) + }) + }) + }) + }) + }) + }) it('Can use indexes for comparison matches', function (done) { + // eslint-disable-next-line node/handle-callback-err d.ensureIndex({ fieldName: 'tf' }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 4 }, function (err, _doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 6 }, function (err, _doc2) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 4, an: 'other' }, function (err, _doc3) { + // eslint-disable-next-line node/handle-callback-err d.insert({ tf: 9 }, function (err, _doc4) { + // eslint-disable-next-line node/handle-callback-err d.getCandidates({ r: 6, tf: { $lte: 9, $gte: 6 } }, function (err, data) { - var doc2 = _.find(data, function (d) { return d._id === _doc2._id; }) - , doc4 = _.find(data, function (d) { return d._id === _doc4._id; }) - ; - - data.length.should.equal(2); - assert.deepEqual(doc2, { _id: doc2._id, tf: 6 }); - assert.deepEqual(doc4, { _id: doc4._id, tf: 9 }); - - done(); - }); - }); - }); - }); - }); - }); - }); - - it("Can set a TTL index that expires documents", function (done) { + const doc2 = _.find(data, function (d) { return d._id === _doc2._id }) + const doc4 = _.find(data, function (d) { return d._id === _doc4._id }) + + data.length.should.equal(2) + assert.deepEqual(doc2, { _id: doc2._id, tf: 6 }) + assert.deepEqual(doc4, { _id: doc4._id, tf: 9 }) + + done() + }) + }) + }) + }) + }) + }) + }) + + it('Can set a TTL index that expires documents', function (done) { d.ensureIndex({ fieldName: 'exp', expireAfterSeconds: 0.2 }, function () { d.insert({ hello: 'world', exp: new Date() }, function () { setTimeout(function () { d.findOne({}, function (err, doc) { - assert.isNull(err); - doc.hello.should.equal('world'); + assert.isNull(err) + doc.hello.should.equal('world') setTimeout(function () { d.findOne({}, function (err, doc) { - assert.isNull(err); - assert.isNull(doc); + assert.isNull(err) + assert.isNull(doc) d.on('compaction.done', function () { // After compaction, no more mention of the document, correctly removed - var datafileContents = fs.readFileSync(testDb, 'utf8'); - datafileContents.split('\n').length.should.equal(2); - assert.isNull(datafileContents.match(/world/)); + const datafileContents = fs.readFileSync(testDb, 'utf8') + datafileContents.split('\n').length.should.equal(2) + assert.isNull(datafileContents.match(/world/)) // New datastore on same datafile is empty - var d2 = new Datastore({ filename: testDb, autoload: true }); + const d2 = new Datastore({ filename: testDb, autoload: true }) d2.findOne({}, function (err, doc) { - assert.isNull(err); - assert.isNull(doc); - - done(); - }); - }); - - d.persistence.compactDatafile(); - }); - }, 101); - }); - }, 100); - }); - }); - }); - - it("TTL indexes can expire multiple documents and only what needs to be expired", function (done) { + assert.isNull(err) + assert.isNull(doc) + + done() + }) + }) + + d.persistence.compactDatafile() + }) + }, 101) + }) + }, 100) + }) + }) + }) + + it('TTL indexes can expire multiple documents and only what needs to be expired', function (done) { d.ensureIndex({ fieldName: 'exp', expireAfterSeconds: 0.2 }, function () { d.insert({ hello: 'world1', exp: new Date() }, function () { d.insert({ hello: 'world2', exp: new Date() }, function () { d.insert({ hello: 'world3', exp: new Date((new Date()).getTime() + 100) }, function () { setTimeout(function () { d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(3); + assert.isNull(err) + docs.length.should.equal(3) setTimeout(function () { d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - docs[0].hello.should.equal('world3'); + assert.isNull(err) + docs.length.should.equal(1) + docs[0].hello.should.equal('world3') setTimeout(function () { d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - - done(); - }); - }, 101); - }); - }, 101); - }); - }, 100); - }); - }); - }); - }); - }); - - it("Document where indexed field is absent or not a date are ignored", function (done) { + assert.isNull(err) + docs.length.should.equal(0) + + done() + }) + }, 101) + }) + }, 101) + }) + }, 100) + }) + }) + }) + }) + }) + + it('Document where indexed field is absent or not a date are ignored', function (done) { d.ensureIndex({ fieldName: 'exp', expireAfterSeconds: 0.2 }, function () { d.insert({ hello: 'world1', exp: new Date() }, function () { - d.insert({ hello: 'world2', exp: "not a date" }, function () { + d.insert({ hello: 'world2', exp: 'not a date' }, function () { d.insert({ hello: 'world3' }, function () { setTimeout(function () { d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(3); + assert.isNull(err) + docs.length.should.equal(3) setTimeout(function () { d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - - - docs[0].hello.should.not.equal('world1'); - docs[1].hello.should.not.equal('world1'); - - done(); - }); - }, 101); - }); - }, 100); - }); - }); - }); - }); - }); - - }); // ==== End of '#getCandidates' ==== // - + assert.isNull(err) + docs.length.should.equal(2) + + docs[0].hello.should.not.equal('world1') + docs[1].hello.should.not.equal('world1') + + done() + }) + }, 101) + }) + }, 100) + }) + }) + }) + }) + }) + }) // ==== End of '#getCandidates' ==== // describe('Find', function () { - it('Can find all documents if an empty query is used', function (done) { async.waterfall([ - function (cb) { - d.insert({ somedata: 'ok' }, function (err) { - d.insert({ somedata: 'another', plus: 'additional data' }, function (err) { - d.insert({ somedata: 'again' }, function (err) { return cb(err); }); - }); - }); - } - , function (cb) { // Test with empty object - d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(3); - _.pluck(docs, 'somedata').should.contain('ok'); - _.pluck(docs, 'somedata').should.contain('another'); - _.find(docs, function (d) { return d.somedata === 'another' }).plus.should.equal('additional data'); - _.pluck(docs, 'somedata').should.contain('again'); - return cb(); - }); - } - ], done); - }); + function (cb) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'ok' }, function (err) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'another', plus: 'additional data' }, function (err) { + d.insert({ somedata: 'again' }, function (err) { return cb(err) }) + }) + }) + }, + function (cb) { // Test with empty object + d.find({}, function (err, docs) { + assert.isNull(err) + docs.length.should.equal(3) + _.pluck(docs, 'somedata').should.contain('ok') + _.pluck(docs, 'somedata').should.contain('another') + _.find(docs, function (d) { return d.somedata === 'another' }).plus.should.equal('additional data') + _.pluck(docs, 'somedata').should.contain('again') + return cb() + }) + } + ], done) + }) it('Can find all documents matching a basic query', function (done) { async.waterfall([ - function (cb) { - d.insert({ somedata: 'ok' }, function (err) { - d.insert({ somedata: 'again', plus: 'additional data' }, function (err) { - d.insert({ somedata: 'again' }, function (err) { return cb(err); }); - }); - }); - } - , function (cb) { // Test with query that will return docs - d.find({ somedata: 'again' }, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - _.pluck(docs, 'somedata').should.not.contain('ok'); - return cb(); - }); - } - , function (cb) { // Test with query that doesn't match anything - d.find({ somedata: 'nope' }, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - return cb(); - }); - } - ], done); - }); + function (cb) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'ok' }, function (err) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'again', plus: 'additional data' }, function (err) { + d.insert({ somedata: 'again' }, function (err) { return cb(err) }) + }) + }) + }, + function (cb) { // Test with query that will return docs + d.find({ somedata: 'again' }, function (err, docs) { + assert.isNull(err) + docs.length.should.equal(2) + _.pluck(docs, 'somedata').should.not.contain('ok') + return cb() + }) + }, + function (cb) { // Test with query that doesn't match anything + d.find({ somedata: 'nope' }, function (err, docs) { + assert.isNull(err) + docs.length.should.equal(0) + return cb() + }) + } + ], done) + }) it('Can find one document matching a basic query and return null if none is found', function (done) { async.waterfall([ - function (cb) { - d.insert({ somedata: 'ok' }, function (err) { - d.insert({ somedata: 'again', plus: 'additional data' }, function (err) { - d.insert({ somedata: 'again' }, function (err) { return cb(err); }); - }); - }); - } - , function (cb) { // Test with query that will return docs - d.findOne({ somedata: 'ok' }, function (err, doc) { - assert.isNull(err); - Object.keys(doc).length.should.equal(2); - doc.somedata.should.equal('ok'); - assert.isDefined(doc._id); - return cb(); - }); - } - , function (cb) { // Test with query that doesn't match anything - d.findOne({ somedata: 'nope' }, function (err, doc) { - assert.isNull(err); - assert.isNull(doc); - return cb(); - }); - } - ], done); - }); + function (cb) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'ok' }, function (err) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'again', plus: 'additional data' }, function (err) { + d.insert({ somedata: 'again' }, function (err) { return cb(err) }) + }) + }) + }, + function (cb) { // Test with query that will return docs + d.findOne({ somedata: 'ok' }, function (err, doc) { + assert.isNull(err) + Object.keys(doc).length.should.equal(2) + doc.somedata.should.equal('ok') + assert.isDefined(doc._id) + return cb() + }) + }, + function (cb) { // Test with query that doesn't match anything + d.findOne({ somedata: 'nope' }, function (err, doc) { + assert.isNull(err) + assert.isNull(doc) + return cb() + }) + } + ], done) + }) it('Can find dates and objects (non JS-native types)', function (done) { - var date1 = new Date(1234543) - , date2 = new Date(9999) - ; + const date1 = new Date(1234543) + const date2 = new Date(9999) d.insert({ now: date1, sth: { name: 'nedb' } }, function () { d.findOne({ now: date1 }, function (err, doc) { - assert.isNull(err); - doc.sth.name.should.equal('nedb'); + assert.isNull(err) + doc.sth.name.should.equal('nedb') d.findOne({ now: date2 }, function (err, doc) { - assert.isNull(err); - assert.isNull(doc); + assert.isNull(err) + assert.isNull(doc) d.findOne({ sth: { name: 'nedb' } }, function (err, doc) { - assert.isNull(err); - doc.sth.name.should.equal('nedb'); + assert.isNull(err) + doc.sth.name.should.equal('nedb') d.findOne({ sth: { name: 'other' } }, function (err, doc) { - assert.isNull(err); - assert.isNull(doc); + assert.isNull(err) + assert.isNull(doc) - done(); - }); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) + }) it('Can use dot-notation to query subfields', function (done) { d.insert({ greeting: { english: 'hello' } }, function () { - d.findOne({ "greeting.english": 'hello' }, function (err, doc) { - assert.isNull(err); - doc.greeting.english.should.equal('hello'); + d.findOne({ 'greeting.english': 'hello' }, function (err, doc) { + assert.isNull(err) + doc.greeting.english.should.equal('hello') - d.findOne({ "greeting.english": 'hellooo' }, function (err, doc) { - assert.isNull(err); - assert.isNull(doc); + d.findOne({ 'greeting.english': 'hellooo' }, function (err, doc) { + assert.isNull(err) + assert.isNull(doc) - d.findOne({ "greeting.englis": 'hello' }, function (err, doc) { - assert.isNull(err); - assert.isNull(doc); + d.findOne({ 'greeting.englis': 'hello' }, function (err, doc) { + assert.isNull(err) + assert.isNull(doc) - done(); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) it('Array fields match if any element matches', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ fruits: ['pear', 'apple', 'banana'] }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ fruits: ['coconut', 'orange', 'pear'] }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err d.insert({ fruits: ['banana'] }, function (err, doc3) { d.find({ fruits: 'pear' }, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - _.pluck(docs, '_id').should.contain(doc1._id); - _.pluck(docs, '_id').should.contain(doc2._id); + assert.isNull(err) + docs.length.should.equal(2) + _.pluck(docs, '_id').should.contain(doc1._id) + _.pluck(docs, '_id').should.contain(doc2._id) d.find({ fruits: 'banana' }, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - _.pluck(docs, '_id').should.contain(doc1._id); - _.pluck(docs, '_id').should.contain(doc3._id); + assert.isNull(err) + docs.length.should.equal(2) + _.pluck(docs, '_id').should.contain(doc1._id) + _.pluck(docs, '_id').should.contain(doc3._id) d.find({ fruits: 'doesntexist' }, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - - done(); - }); - }); - }); - }); - }); - }); - }); + assert.isNull(err) + docs.length.should.equal(0) + + done() + }) + }) + }) + }) + }) + }) + }) it('Returns an error if the query is not well formed', function (done) { d.insert({ hello: 'world' }, function () { d.find({ $or: { hello: 'world' } }, function (err, docs) { - assert.isDefined(err); - assert.isUndefined(docs); + assert.isDefined(err) + assert.isUndefined(docs) d.findOne({ $or: { hello: 'world' } }, function (err, doc) { - assert.isDefined(err); - assert.isUndefined(doc); + assert.isDefined(err) + assert.isUndefined(doc) - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('Changing the documents returned by find or findOne do not change the database state', function (done) { d.insert({ a: 2, hello: 'world' }, function () { + // eslint-disable-next-line node/handle-callback-err d.findOne({ a: 2 }, function (err, doc) { - doc.hello = 'changed'; + doc.hello = 'changed' + // eslint-disable-next-line node/handle-callback-err d.findOne({ a: 2 }, function (err, doc) { - doc.hello.should.equal('world'); + doc.hello.should.equal('world') + // eslint-disable-next-line node/handle-callback-err d.find({ a: 2 }, function (err, docs) { - docs[0].hello = 'changed'; + docs[0].hello = 'changed' + // eslint-disable-next-line node/handle-callback-err d.findOne({ a: 2 }, function (err, doc) { - doc.hello.should.equal('world'); - - done(); - }); - }); - }); - }); - }); - }); - + doc.hello.should.equal('world') + + done() + }) + }) + }) + }) + }) + }) + it('Can use sort, skip and limit if the callback is not passed to find but to exec', function (done) { d.insert({ a: 2, hello: 'world' }, function () { d.insert({ a: 24, hello: 'earth' }, function () { d.insert({ a: 13, hello: 'blueplanet' }, function () { d.insert({ a: 15, hello: 'home' }, function () { d.find({}).sort({ a: 1 }).limit(2).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - docs[0].hello.should.equal('world'); - docs[1].hello.should.equal('blueplanet'); - done(); - }); - }); - }); - }); - }); - }); - - it('Can use sort and skip if the callback is not passed to findOne but to exec', function (done) { + assert.isNull(err) + docs.length.should.equal(2) + docs[0].hello.should.equal('world') + docs[1].hello.should.equal('blueplanet') + done() + }) + }) + }) + }) + }) + }) + + it('Can use sort and skip if the callback is not passed to findOne but to exec', function (done) { d.insert({ a: 2, hello: 'world' }, function () { d.insert({ a: 24, hello: 'earth' }, function () { d.insert({ a: 13, hello: 'blueplanet' }, function () { d.insert({ a: 15, hello: 'home' }, function () { // No skip no query d.findOne({}).sort({ a: 1 }).exec(function (err, doc) { - assert.isNull(err); - doc.hello.should.equal('world'); - + assert.isNull(err) + doc.hello.should.equal('world') + // A query d.findOne({ a: { $gt: 14 } }).sort({ a: 1 }).exec(function (err, doc) { - assert.isNull(err); - doc.hello.should.equal('home'); + assert.isNull(err) + doc.hello.should.equal('home') // And a skip d.findOne({ a: { $gt: 14 } }).sort({ a: 1 }).skip(1).exec(function (err, doc) { - assert.isNull(err); - doc.hello.should.equal('earth'); + assert.isNull(err) + doc.hello.should.equal('earth') // No result d.findOne({ a: { $gt: 14 } }).sort({ a: 1 }).skip(2).exec(function (err, doc) { - assert.isNull(err); - assert.isNull(doc); - - done(); - }); - }); - }); - }); - }); - }); - }); - }); - }); + assert.isNull(err) + assert.isNull(doc) + + done() + }) + }) + }) + }) + }) + }) + }) + }) + }) it('Can use projections in find, normal or cursor way', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, hello: 'world' }, function (err, doc0) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 24, hello: 'earth' }, function (err, doc1) { d.find({ a: 2 }, { a: 0, _id: 0 }, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - assert.deepEqual(docs[0], { hello: 'world' }); + assert.isNull(err) + docs.length.should.equal(1) + assert.deepEqual(docs[0], { hello: 'world' }) d.find({ a: 2 }, { a: 0, _id: 0 }).exec(function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - assert.deepEqual(docs[0], { hello: 'world' }); + assert.isNull(err) + docs.length.should.equal(1) + assert.deepEqual(docs[0], { hello: 'world' }) // Can't use both modes at once if not _id d.find({ a: 2 }, { a: 0, hello: 1 }, function (err, docs) { - assert.isNotNull(err); - assert.isUndefined(docs); + assert.isNotNull(err) + assert.isUndefined(docs) d.find({ a: 2 }, { a: 0, hello: 1 }).exec(function (err, docs) { - assert.isNotNull(err); - assert.isUndefined(docs); - - done(); - }); - }); - }); - }); - }); - }); - }); + assert.isNotNull(err) + assert.isUndefined(docs) + + done() + }) + }) + }) + }) + }) + }) + }) it('Can use projections in findOne, normal or cursor way', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, hello: 'world' }, function (err, doc0) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 24, hello: 'earth' }, function (err, doc1) { d.findOne({ a: 2 }, { a: 0, _id: 0 }, function (err, doc) { - assert.isNull(err); - assert.deepEqual(doc, { hello: 'world' }); + assert.isNull(err) + assert.deepEqual(doc, { hello: 'world' }) d.findOne({ a: 2 }, { a: 0, _id: 0 }).exec(function (err, doc) { - assert.isNull(err); - assert.deepEqual(doc, { hello: 'world' }); + assert.isNull(err) + assert.deepEqual(doc, { hello: 'world' }) // Can't use both modes at once if not _id d.findOne({ a: 2 }, { a: 0, hello: 1 }, function (err, doc) { - assert.isNotNull(err); - assert.isUndefined(doc); + assert.isNotNull(err) + assert.isUndefined(doc) d.findOne({ a: 2 }, { a: 0, hello: 1 }).exec(function (err, doc) { - assert.isNotNull(err); - assert.isUndefined(doc); + assert.isNotNull(err) + assert.isUndefined(doc) - done(); - }); - }); - }); - }); - }); - }); - }); - - }); // ==== End of 'Find' ==== // - - describe('Count', function() { + done() + }) + }) + }) + }) + }) + }) + }) + }) // ==== End of 'Find' ==== // + describe('Count', function () { it('Count all documents if an empty query is used', function (done) { async.waterfall([ - function (cb) { - d.insert({ somedata: 'ok' }, function (err) { - d.insert({ somedata: 'another', plus: 'additional data' }, function (err) { - d.insert({ somedata: 'again' }, function (err) { return cb(err); }); - }); - }); - } - , function (cb) { // Test with empty object - d.count({}, function (err, docs) { - assert.isNull(err); - docs.should.equal(3); - return cb(); - }); - } - ], done); - }); + function (cb) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'ok' }, function (err) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'another', plus: 'additional data' }, function (err) { + d.insert({ somedata: 'again' }, function (err) { return cb(err) }) + }) + }) + }, + function (cb) { // Test with empty object + d.count({}, function (err, docs) { + assert.isNull(err) + docs.should.equal(3) + return cb() + }) + } + ], done) + }) it('Count all documents matching a basic query', function (done) { async.waterfall([ - function (cb) { - d.insert({ somedata: 'ok' }, function (err) { - d.insert({ somedata: 'again', plus: 'additional data' }, function (err) { - d.insert({ somedata: 'again' }, function (err) { return cb(err); }); - }); - }); - } - , function (cb) { // Test with query that will return docs - d.count({ somedata: 'again' }, function (err, docs) { - assert.isNull(err); - docs.should.equal(2); - return cb(); - }); - } - , function (cb) { // Test with query that doesn't match anything - d.count({ somedata: 'nope' }, function (err, docs) { - assert.isNull(err); - docs.should.equal(0); - return cb(); - }); - } - ], done); - }); + function (cb) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'ok' }, function (err) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'again', plus: 'additional data' }, function (err) { + d.insert({ somedata: 'again' }, function (err) { return cb(err) }) + }) + }) + }, + function (cb) { // Test with query that will return docs + d.count({ somedata: 'again' }, function (err, docs) { + assert.isNull(err) + docs.should.equal(2) + return cb() + }) + }, + function (cb) { // Test with query that doesn't match anything + d.count({ somedata: 'nope' }, function (err, docs) { + assert.isNull(err) + docs.should.equal(0) + return cb() + }) + } + ], done) + }) it('Array fields match if any element matches', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ fruits: ['pear', 'apple', 'banana'] }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ fruits: ['coconut', 'orange', 'pear'] }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err d.insert({ fruits: ['banana'] }, function (err, doc3) { d.count({ fruits: 'pear' }, function (err, docs) { - assert.isNull(err); - docs.should.equal(2); + assert.isNull(err) + docs.should.equal(2) d.count({ fruits: 'banana' }, function (err, docs) { - assert.isNull(err); - docs.should.equal(2); + assert.isNull(err) + docs.should.equal(2) d.count({ fruits: 'doesntexist' }, function (err, docs) { - assert.isNull(err); - docs.should.equal(0); - - done(); - }); - }); - }); - }); - }); - }); - }); + assert.isNull(err) + docs.should.equal(0) + + done() + }) + }) + }) + }) + }) + }) + }) it('Returns an error if the query is not well formed', function (done) { d.insert({ hello: 'world' }, function () { d.count({ $or: { hello: 'world' } }, function (err, docs) { - assert.isDefined(err); - assert.isUndefined(docs); - - done(); - }); - }); - }); + assert.isDefined(err) + assert.isUndefined(docs) - }); + done() + }) + }) + }) + }) describe('Update', function () { - - it("If the query doesn't match anything, database is not modified", function (done) { + it('If the query doesn\'t match anything, database is not modified', function (done) { async.waterfall([ - function (cb) { - d.insert({ somedata: 'ok' }, function (err) { - d.insert({ somedata: 'again', plus: 'additional data' }, function (err) { - d.insert({ somedata: 'another' }, function (err) { return cb(err); }); - }); - }); - } - , function (cb) { // Test with query that doesn't match anything - d.update({ somedata: 'nope' }, { newDoc: 'yes' }, { multi: true }, function (err, n) { - assert.isNull(err); - n.should.equal(0); + function (cb) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'ok' }, function (err) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'again', plus: 'additional data' }, function (err) { + d.insert({ somedata: 'another' }, function (err) { return cb(err) }) + }) + }) + }, + function (cb) { // Test with query that doesn't match anything + d.update({ somedata: 'nope' }, { newDoc: 'yes' }, { multi: true }, function (err, n) { + assert.isNull(err) + n.should.equal(0) - d.find({}, function (err, docs) { - var doc1 = _.find(docs, function (d) { return d.somedata === 'ok'; }) - , doc2 = _.find(docs, function (d) { return d.somedata === 'again'; }) - , doc3 = _.find(docs, function (d) { return d.somedata === 'another'; }) - ; + // eslint-disable-next-line node/handle-callback-err + d.find({}, function (err, docs) { + const doc1 = _.find(docs, function (d) { return d.somedata === 'ok' }) + const doc2 = _.find(docs, function (d) { return d.somedata === 'again' }) + const doc3 = _.find(docs, function (d) { return d.somedata === 'another' }) - docs.length.should.equal(3); - assert.isUndefined(_.find(docs, function (d) { return d.newDoc === 'yes'; })); + docs.length.should.equal(3) + assert.isUndefined(_.find(docs, function (d) { return d.newDoc === 'yes' })) - assert.deepEqual(doc1, { _id: doc1._id, somedata: 'ok' }); - assert.deepEqual(doc2, { _id: doc2._id, somedata: 'again', plus: 'additional data' }); - assert.deepEqual(doc3, { _id: doc3._id, somedata: 'another' }); + assert.deepEqual(doc1, { _id: doc1._id, somedata: 'ok' }) + assert.deepEqual(doc2, { _id: doc2._id, somedata: 'again', plus: 'additional data' }) + assert.deepEqual(doc3, { _id: doc3._id, somedata: 'another' }) - return cb(); - }); - }); - } - ], done); - }); + return cb() + }) + }) + } + ], done) + }) - it("If timestampData option is set, update the updatedAt field", function (done) { - var beginning = Date.now(); - d = new Datastore({ filename: testDb, autoload: true, timestampData: true }); + it('If timestampData option is set, update the updatedAt field', function (done) { + const beginning = Date.now() + d = new Datastore({ filename: testDb, autoload: true, timestampData: true }) + // eslint-disable-next-line node/handle-callback-err d.insert({ hello: 'world' }, function (err, insertedDoc) { - assert.isBelow(insertedDoc.updatedAt.getTime() - beginning, reloadTimeUpperBound); - assert.isBelow(insertedDoc.createdAt.getTime() - beginning, reloadTimeUpperBound); - Object.keys(insertedDoc).length.should.equal(4); + assert.isBelow(insertedDoc.updatedAt.getTime() - beginning, reloadTimeUpperBound) + assert.isBelow(insertedDoc.createdAt.getTime() - beginning, reloadTimeUpperBound) + Object.keys(insertedDoc).length.should.equal(4) // Wait 100ms before performing the update setTimeout(function () { - var step1 = Date.now(); + const step1 = Date.now() d.update({ _id: insertedDoc._id }, { $set: { hello: 'mars' } }, {}, function () { + // eslint-disable-next-line node/handle-callback-err d.find({ _id: insertedDoc._id }, function (err, docs) { - docs.length.should.equal(1); - Object.keys(docs[0]).length.should.equal(4); - docs[0]._id.should.equal(insertedDoc._id); - docs[0].createdAt.should.equal(insertedDoc.createdAt); - docs[0].hello.should.equal('mars'); - assert.isAbove(docs[0].updatedAt.getTime() - beginning, 99); // updatedAt modified - assert.isBelow(docs[0].updatedAt.getTime() - step1, reloadTimeUpperBound); // updatedAt modified - - done(); - }); + docs.length.should.equal(1) + Object.keys(docs[0]).length.should.equal(4) + docs[0]._id.should.equal(insertedDoc._id) + docs[0].createdAt.should.equal(insertedDoc.createdAt) + docs[0].hello.should.equal('mars') + assert.isAbove(docs[0].updatedAt.getTime() - beginning, 99) // updatedAt modified + assert.isBelow(docs[0].updatedAt.getTime() - step1, reloadTimeUpperBound) // updatedAt modified + + done() + }) }) - }, 100); - }); - }); + }, 100) + }) + }) - it("Can update multiple documents matching the query", function (done) { - var id1, id2, id3; + it('Can update multiple documents matching the query', function (done) { + let id1 + let id2 + let id3 // Test DB state after update and reload function testPostUpdateState (cb) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - var doc1 = _.find(docs, function (d) { return d._id === id1; }) - , doc2 = _.find(docs, function (d) { return d._id === id2; }) - , doc3 = _.find(docs, function (d) { return d._id === id3; }) - ; + const doc1 = _.find(docs, function (d) { return d._id === id1 }) + const doc2 = _.find(docs, function (d) { return d._id === id2 }) + const doc3 = _.find(docs, function (d) { return d._id === id3 }) - docs.length.should.equal(3); + docs.length.should.equal(3) - Object.keys(doc1).length.should.equal(2); - doc1.somedata.should.equal('ok'); - doc1._id.should.equal(id1); + Object.keys(doc1).length.should.equal(2) + doc1.somedata.should.equal('ok') + doc1._id.should.equal(id1) - Object.keys(doc2).length.should.equal(2); - doc2.newDoc.should.equal('yes'); - doc2._id.should.equal(id2); + Object.keys(doc2).length.should.equal(2) + doc2.newDoc.should.equal('yes') + doc2._id.should.equal(id2) - Object.keys(doc3).length.should.equal(2); - doc3.newDoc.should.equal('yes'); - doc3._id.should.equal(id3); + Object.keys(doc3).length.should.equal(2) + doc3.newDoc.should.equal('yes') + doc3._id.should.equal(id3) - return cb(); - }); + return cb() + }) } // Actually launch the tests async.waterfall([ - function (cb) { - d.insert({ somedata: 'ok' }, function (err, doc1) { - id1 = doc1._id; - d.insert({ somedata: 'again', plus: 'additional data' }, function (err, doc2) { - id2 = doc2._id; - d.insert({ somedata: 'again' }, function (err, doc3) { - id3 = doc3._id; - return cb(err); - }); - }); - }); - } - , function (cb) { - d.update({ somedata: 'again' }, { newDoc: 'yes' }, { multi: true }, function (err, n) { - assert.isNull(err); - n.should.equal(2); - return cb(); - }); - } - , async.apply(testPostUpdateState) - , function (cb) { - d.loadDatabase(function (err) { cb(err); }); - } - , async.apply(testPostUpdateState) - ], done); - }); - - it("Can update only one document matching the query", function (done) { - var id1, id2, id3; + function (cb) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'ok' }, function (err, doc1) { + id1 = doc1._id + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'again', plus: 'additional data' }, function (err, doc2) { + id2 = doc2._id + d.insert({ somedata: 'again' }, function (err, doc3) { + id3 = doc3._id + return cb(err) + }) + }) + }) + }, + function (cb) { + d.update({ somedata: 'again' }, { newDoc: 'yes' }, { multi: true }, function (err, n) { + assert.isNull(err) + n.should.equal(2) + return cb() + }) + }, + async.apply(testPostUpdateState), + function (cb) { + d.loadDatabase(function (err) { cb(err) }) + }, + async.apply(testPostUpdateState) + ], done) + }) + + it('Can update only one document matching the query', function (done) { + let id1 + let id2 + let id3 // Test DB state after update and reload function testPostUpdateState (cb) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - var doc1 = _.find(docs, function (d) { return d._id === id1; }) - , doc2 = _.find(docs, function (d) { return d._id === id2; }) - , doc3 = _.find(docs, function (d) { return d._id === id3; }) - ; + const doc1 = _.find(docs, function (d) { return d._id === id1 }) + const doc2 = _.find(docs, function (d) { return d._id === id2 }) + const doc3 = _.find(docs, function (d) { return d._id === id3 }) - docs.length.should.equal(3); + docs.length.should.equal(3) - assert.deepEqual(doc1, { somedata: 'ok', _id: doc1._id }); + assert.deepEqual(doc1, { somedata: 'ok', _id: doc1._id }) // doc2 or doc3 was modified. Since we sort on _id and it is random // it can be either of two situations try { - assert.deepEqual(doc2, { newDoc: 'yes', _id: doc2._id }); - assert.deepEqual(doc3, { somedata: 'again', _id: doc3._id }); + assert.deepEqual(doc2, { newDoc: 'yes', _id: doc2._id }) + assert.deepEqual(doc3, { somedata: 'again', _id: doc3._id }) } catch (e) { - assert.deepEqual(doc2, { somedata: 'again', plus: 'additional data', _id: doc2._id }); - assert.deepEqual(doc3, { newDoc: 'yes', _id: doc3._id }); + assert.deepEqual(doc2, { somedata: 'again', plus: 'additional data', _id: doc2._id }) + assert.deepEqual(doc3, { newDoc: 'yes', _id: doc3._id }) } - return cb(); - }); + return cb() + }) } // Actually launch the test async.waterfall([ - function (cb) { - d.insert({ somedata: 'ok' }, function (err, doc1) { - id1 = doc1._id; - d.insert({ somedata: 'again', plus: 'additional data' }, function (err, doc2) { - id2 = doc2._id; - d.insert({ somedata: 'again' }, function (err, doc3) { - id3 = doc3._id; - return cb(err); - }); - }); - }); - } - , function (cb) { // Test with query that doesn't match anything - d.update({ somedata: 'again' }, { newDoc: 'yes' }, { multi: false }, function (err, n) { - assert.isNull(err); - n.should.equal(1); - return cb(); - }); - } - , async.apply(testPostUpdateState) - , function (cb) { - d.loadDatabase(function (err) { return cb(err); }); - } - , async.apply(testPostUpdateState) // The persisted state has been updated - ], done); - }); + function (cb) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'ok' }, function (err, doc1) { + id1 = doc1._id + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'again', plus: 'additional data' }, function (err, doc2) { + id2 = doc2._id + d.insert({ somedata: 'again' }, function (err, doc3) { + id3 = doc3._id + return cb(err) + }) + }) + }) + }, + function (cb) { // Test with query that doesn't match anything + d.update({ somedata: 'again' }, { newDoc: 'yes' }, { multi: false }, function (err, n) { + assert.isNull(err) + n.should.equal(1) + return cb() + }) + }, + async.apply(testPostUpdateState), + function (cb) { + d.loadDatabase(function (err) { return cb(err) }) + }, + async.apply(testPostUpdateState) // The persisted state has been updated + ], done) + }) describe('Upserts', function () { - it('Can perform upserts if needed', function (done) { d.update({ impossible: 'db is empty anyway' }, { newDoc: true }, {}, function (err, nr, upsert) { - assert.isNull(err); - nr.should.equal(0); - assert.isUndefined(upsert); + assert.isNull(err) + nr.should.equal(0) + assert.isUndefined(upsert) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(0); // Default option for upsert is false + docs.length.should.equal(0) // Default option for upsert is false - d.update({ impossible: 'db is empty anyway' }, { something: "created ok" }, { upsert: true }, function (err, nr, newDoc) { - assert.isNull(err); - nr.should.equal(1); - newDoc.something.should.equal("created ok"); - assert.isDefined(newDoc._id); + d.update({ impossible: 'db is empty anyway' }, { something: 'created ok' }, { upsert: true }, function (err, nr, newDoc) { + assert.isNull(err) + nr.should.equal(1) + newDoc.something.should.equal('created ok') + assert.isDefined(newDoc._id) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); // Default option for upsert is false - docs[0].something.should.equal("created ok"); - + docs.length.should.equal(1) // Default option for upsert is false + docs[0].something.should.equal('created ok') + // Modifying the returned upserted document doesn't modify the database - newDoc.newField = true; + newDoc.newField = true + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs[0].something.should.equal("created ok"); - assert.isUndefined(docs[0].newField); - - done(); - }); - }); - }); - }); - }); - }); - + docs[0].something.should.equal('created ok') + assert.isUndefined(docs[0].newField) + + done() + }) + }) + }) + }) + }) + }) + it('If the update query is a normal object with no modifiers, it is the doc that will be upserted', function (done) { + // eslint-disable-next-line node/handle-callback-err d.update({ $or: [{ a: 4 }, { a: 5 }] }, { hello: 'world', bloup: 'blap' }, { upsert: true }, function (err) { d.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - var doc = docs[0]; - Object.keys(doc).length.should.equal(3); - doc.hello.should.equal('world'); - doc.bloup.should.equal('blap'); - done(); - }); - }); - }); - + assert.isNull(err) + docs.length.should.equal(1) + const doc = docs[0] + Object.keys(doc).length.should.equal(3) + doc.hello.should.equal('world') + doc.bloup.should.equal('blap') + done() + }) + }) + }) + it('If the update query contains modifiers, it is applied to the object resulting from removing all operators from the find query 1', function (done) { - d.update({ $or: [{ a: 4 }, { a: 5 }] }, { $set: { hello: 'world' }, $inc: { bloup: 3 } }, { upsert: true }, function (err) { + d.update({ $or: [{ a: 4 }, { a: 5 }] }, { + $set: { hello: 'world' }, + $inc: { bloup: 3 } + // eslint-disable-next-line node/handle-callback-err + }, { upsert: true }, function (err) { d.find({ hello: 'world' }, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - var doc = docs[0]; - Object.keys(doc).length.should.equal(3); - doc.hello.should.equal('world'); - doc.bloup.should.equal(3); - done(); - }); - }); - }); - + assert.isNull(err) + docs.length.should.equal(1) + const doc = docs[0] + Object.keys(doc).length.should.equal(3) + doc.hello.should.equal('world') + doc.bloup.should.equal(3) + done() + }) + }) + }) + it('If the update query contains modifiers, it is applied to the object resulting from removing all operators from the find query 2', function (done) { - d.update({ $or: [{ a: 4 }, { a: 5 }], cac: 'rrr' }, { $set: { hello: 'world' }, $inc: { bloup: 3 } }, { upsert: true }, function (err) { + d.update({ $or: [{ a: 4 }, { a: 5 }], cac: 'rrr' }, { + $set: { hello: 'world' }, + $inc: { bloup: 3 } + // eslint-disable-next-line node/handle-callback-err + }, { upsert: true }, function (err) { d.find({ hello: 'world' }, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - var doc = docs[0]; - Object.keys(doc).length.should.equal(4); - doc.cac.should.equal('rrr'); - doc.hello.should.equal('world'); - doc.bloup.should.equal(3); - done(); - }); - }); - }); - - it('Performing upsert with badly formatted fields yields a standard error not an exception', function(done) { - d.update({_id: '1234'}, { $set: { $$badfield: 5 }}, { upsert: true }, function(err, doc) { - assert.isDefined(err); - done(); - }) - }); - - - }); // ==== End of 'Upserts' ==== // + assert.isNull(err) + docs.length.should.equal(1) + const doc = docs[0] + Object.keys(doc).length.should.equal(4) + doc.cac.should.equal('rrr') + doc.hello.should.equal('world') + doc.bloup.should.equal(3) + done() + }) + }) + }) + + it('Performing upsert with badly formatted fields yields a standard error not an exception', function (done) { + d.update({ _id: '1234' }, { $set: { $$badfield: 5 } }, { upsert: true }, function (err, doc) { + assert.isDefined(err) + done() + }) + }) + }) // ==== End of 'Upserts' ==== // it('Cannot perform update if the update query is not either registered-modifiers-only or copy-only, or contain badly formatted fields', function (done) { d.insert({ something: 'yup' }, function () { d.update({}, { boom: { $badfield: 5 } }, { multi: false }, function (err) { - assert.isDefined(err); + assert.isDefined(err) - d.update({}, { boom: { "bad.field": 5 } }, { multi: false }, function (err) { - assert.isDefined(err); + d.update({}, { boom: { 'bad.field': 5 } }, { multi: false }, function (err) { + assert.isDefined(err) d.update({}, { $inc: { test: 5 }, mixed: 'rrr' }, { multi: false }, function (err) { - assert.isDefined(err); + assert.isDefined(err) d.update({}, { $inexistent: { test: 5 } }, { multi: false }, function (err) { - assert.isDefined(err); + assert.isDefined(err) - done(); - }); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) + }) it('Can update documents using multiple modifiers', function (done) { - var id; + let id + // eslint-disable-next-line node/handle-callback-err d.insert({ something: 'yup', other: 40 }, function (err, newDoc) { - id = newDoc._id; + id = newDoc._id d.update({}, { $set: { something: 'changed' }, $inc: { other: 10 } }, { multi: false }, function (err, nr) { - assert.isNull(err); - nr.should.equal(1); + assert.isNull(err) + nr.should.equal(1) + // eslint-disable-next-line node/handle-callback-err d.findOne({ _id: id }, function (err, doc) { - Object.keys(doc).length.should.equal(3); - doc._id.should.equal(id); - doc.something.should.equal('changed'); - doc.other.should.equal(50); + Object.keys(doc).length.should.equal(3) + doc._id.should.equal(id) + doc.something.should.equal('changed') + doc.other.should.equal(50) - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('Can upsert a document even with modifiers', function (done) { d.update({ bloup: 'blap' }, { $set: { hello: 'world' } }, { upsert: true }, function (err, nr, newDoc) { - assert.isNull(err); - nr.should.equal(1); - newDoc.bloup.should.equal('blap'); - newDoc.hello.should.equal('world'); - assert.isDefined(newDoc._id); + assert.isNull(err) + nr.should.equal(1) + newDoc.bloup.should.equal('blap') + newDoc.hello.should.equal('world') + assert.isDefined(newDoc._id) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - Object.keys(docs[0]).length.should.equal(3); - docs[0].hello.should.equal('world'); - docs[0].bloup.should.equal('blap'); - assert.isDefined(docs[0]._id); + docs.length.should.equal(1) + Object.keys(docs[0]).length.should.equal(3) + docs[0].hello.should.equal('world') + docs[0].bloup.should.equal('blap') + assert.isDefined(docs[0]._id) - done(); - }); - }); - }); + done() + }) + }) + }) it('When using modifiers, the only way to update subdocs is with the dot-notation', function (done) { - d.insert({ bloup: { blip: "blap", other: true } }, function () { + d.insert({ bloup: { blip: 'blap', other: true } }, function () { // Correct methos - d.update({}, { $set: { "bloup.blip": "hello" } }, {}, function () { + d.update({}, { $set: { 'bloup.blip': 'hello' } }, {}, function () { + // eslint-disable-next-line node/handle-callback-err d.findOne({}, function (err, doc) { - doc.bloup.blip.should.equal("hello"); - doc.bloup.other.should.equal(true); + doc.bloup.blip.should.equal('hello') + doc.bloup.other.should.equal(true) // Wrong - d.update({}, { $set: { bloup: { blip: "ola" } } }, {}, function () { + d.update({}, { $set: { bloup: { blip: 'ola' } } }, {}, function () { + // eslint-disable-next-line node/handle-callback-err d.findOne({}, function (err, doc) { - doc.bloup.blip.should.equal("ola"); - assert.isUndefined(doc.bloup.other); // This information was lost + doc.bloup.blip.should.equal('ola') + assert.isUndefined(doc.bloup.other) // This information was lost - done(); - }); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) + }) it('Returns an error if the query is not well formed', function (done) { d.insert({ hello: 'world' }, function () { d.update({ $or: { hello: 'world' } }, { a: 1 }, {}, function (err, nr, upsert) { - assert.isDefined(err); - assert.isUndefined(nr); - assert.isUndefined(upsert); + assert.isDefined(err) + assert.isUndefined(nr) + assert.isUndefined(upsert) - done(); - }); - }); - }); + done() + }) + }) + }) it('If an error is thrown by a modifier, the database state is not changed', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ hello: 'world' }, function (err, newDoc) { d.update({}, { $inc: { hello: 4 } }, {}, function (err, nr) { - assert.isDefined(err); - assert.isUndefined(nr); + assert.isDefined(err) + assert.isUndefined(nr) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - assert.deepEqual(docs, [ { _id: newDoc._id, hello: 'world' } ]); + assert.deepEqual(docs, [{ _id: newDoc._id, hello: 'world' }]) - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('Cant change the _id of a document', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2 }, function (err, newDoc) { d.update({ a: 2 }, { a: 2, _id: 'nope' }, {}, function (err) { - assert.isDefined(err); + assert.isDefined(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - Object.keys(docs[0]).length.should.equal(2); - docs[0].a.should.equal(2); - docs[0]._id.should.equal(newDoc._id); + docs.length.should.equal(1) + Object.keys(docs[0]).length.should.equal(2) + docs[0].a.should.equal(2) + docs[0]._id.should.equal(newDoc._id) d.update({ a: 2 }, { $set: { _id: 'nope' } }, {}, function (err) { - assert.isDefined(err); + assert.isDefined(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - Object.keys(docs[0]).length.should.equal(2); - docs[0].a.should.equal(2); - docs[0]._id.should.equal(newDoc._id); - - done(); - }); - }); - }); - }); - }); - }); + docs.length.should.equal(1) + Object.keys(docs[0]).length.should.equal(2) + docs[0].a.should.equal(2) + docs[0]._id.should.equal(newDoc._id) + + done() + }) + }) + }) + }) + }) + }) it('Non-multi updates are persistent', function (done) { - d.insert({ a:1, hello: 'world' }, function (err, doc1) { - d.insert({ a:2, hello: 'earth' }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 1, hello: 'world' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 2, hello: 'earth' }, function (err, doc2) { d.update({ a: 2 }, { $set: { hello: 'changed' } }, {}, function (err) { - assert.isNull(err); + assert.isNull(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.sort(function (a, b) { return a.a - b.a; }); - docs.length.should.equal(2); - _.isEqual(docs[0], { _id: doc1._id, a:1, hello: 'world' }).should.equal(true); - _.isEqual(docs[1], { _id: doc2._id, a:2, hello: 'changed' }).should.equal(true); + docs.sort(function (a, b) { return a.a - b.a }) + docs.length.should.equal(2) + _.isEqual(docs[0], { _id: doc1._id, a: 1, hello: 'world' }).should.equal(true) + _.isEqual(docs[1], { _id: doc2._id, a: 2, hello: 'changed' }).should.equal(true) // Even after a reload the database state hasn't changed d.loadDatabase(function (err) { - assert.isNull(err); + assert.isNull(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.sort(function (a, b) { return a.a - b.a; }); - docs.length.should.equal(2); - _.isEqual(docs[0], { _id: doc1._id, a:1, hello: 'world' }).should.equal(true); - _.isEqual(docs[1], { _id: doc2._id, a:2, hello: 'changed' }).should.equal(true); - - done(); - }); - }); - }); - }); - }); - }); - }); + docs.sort(function (a, b) { return a.a - b.a }) + docs.length.should.equal(2) + _.isEqual(docs[0], { _id: doc1._id, a: 1, hello: 'world' }).should.equal(true) + _.isEqual(docs[1], { _id: doc2._id, a: 2, hello: 'changed' }).should.equal(true) + + done() + }) + }) + }) + }) + }) + }) + }) it('Multi updates are persistent', function (done) { - d.insert({ a:1, hello: 'world' }, function (err, doc1) { - d.insert({ a:2, hello: 'earth' }, function (err, doc2) { - d.insert({ a:5, hello: 'pluton' }, function (err, doc3) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 1, hello: 'world' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 2, hello: 'earth' }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 5, hello: 'pluton' }, function (err, doc3) { d.update({ a: { $in: [1, 2] } }, { $set: { hello: 'changed' } }, { multi: true }, function (err) { - assert.isNull(err); + assert.isNull(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.sort(function (a, b) { return a.a - b.a; }); - docs.length.should.equal(3); - _.isEqual(docs[0], { _id: doc1._id, a:1, hello: 'changed' }).should.equal(true); - _.isEqual(docs[1], { _id: doc2._id, a:2, hello: 'changed' }).should.equal(true); - _.isEqual(docs[2], { _id: doc3._id, a:5, hello: 'pluton' }).should.equal(true); + docs.sort(function (a, b) { return a.a - b.a }) + docs.length.should.equal(3) + _.isEqual(docs[0], { _id: doc1._id, a: 1, hello: 'changed' }).should.equal(true) + _.isEqual(docs[1], { _id: doc2._id, a: 2, hello: 'changed' }).should.equal(true) + _.isEqual(docs[2], { _id: doc3._id, a: 5, hello: 'pluton' }).should.equal(true) // Even after a reload the database state hasn't changed d.loadDatabase(function (err) { - assert.isNull(err); + assert.isNull(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.sort(function (a, b) { return a.a - b.a; }); - docs.length.should.equal(3); - _.isEqual(docs[0], { _id: doc1._id, a:1, hello: 'changed' }).should.equal(true); - _.isEqual(docs[1], { _id: doc2._id, a:2, hello: 'changed' }).should.equal(true); - _.isEqual(docs[2], { _id: doc3._id, a:5, hello: 'pluton' }).should.equal(true); - - done(); - }); - }); - }); - }); - }); - }); - }); - }); - + docs.sort(function (a, b) { return a.a - b.a }) + docs.length.should.equal(3) + _.isEqual(docs[0], { _id: doc1._id, a: 1, hello: 'changed' }).should.equal(true) + _.isEqual(docs[1], { _id: doc2._id, a: 2, hello: 'changed' }).should.equal(true) + _.isEqual(docs[2], { _id: doc3._id, a: 5, hello: 'pluton' }).should.equal(true) + + done() + }) + }) + }) + }) + }) + }) + }) + }) + it('Can update without the options arg (will use defaults then)', function (done) { - d.insert({ a:1, hello: 'world' }, function (err, doc1) { - d.insert({ a:2, hello: 'earth' }, function (err, doc2) { - d.insert({ a:5, hello: 'pluton' }, function (err, doc3) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 1, hello: 'world' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 2, hello: 'earth' }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 5, hello: 'pluton' }, function (err, doc3) { d.update({ a: 2 }, { $inc: { a: 10 } }, function (err, nr) { - assert.isNull(err); - nr.should.equal(1); + assert.isNull(err) + nr.should.equal(1) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - var d1 = _.find(docs, function (doc) { return doc._id === doc1._id }) - , d2 = _.find(docs, function (doc) { return doc._id === doc2._id }) - , d3 = _.find(docs, function (doc) { return doc._id === doc3._id }) - ; - - d1.a.should.equal(1); - d2.a.should.equal(12); - d3.a.should.equal(5); - - done(); - }); - }); - }); - }); - }); - }); + const d1 = _.find(docs, function (doc) { return doc._id === doc1._id }) + const d2 = _.find(docs, function (doc) { return doc._id === doc2._id }) + const d3 = _.find(docs, function (doc) { return doc._id === doc3._id }) + + d1.a.should.equal(1) + d2.a.should.equal(12) + d3.a.should.equal(5) + + done() + }) + }) + }) + }) + }) + }) it('If a multi update fails on one document, previous updates should be rolled back', function (done) { - d.ensureIndex({ fieldName: 'a' }); + d.ensureIndex({ fieldName: 'a' }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 4 }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 5 }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 'abc' }, function (err, doc3) { // With this query, candidates are always returned in the order 4, 5, 'abc' so it's always the last one which fails d.update({ a: { $in: [4, 5, 'abc'] } }, { $inc: { a: 10 } }, { multi: true }, function (err) { - assert.isDefined(err); + assert.isDefined(err) // No index modified _.each(d.indexes, function (index) { - var docs = index.getAll() - , d1 = _.find(docs, function (doc) { return doc._id === doc1._id }) - , d2 = _.find(docs, function (doc) { return doc._id === doc2._id }) - , d3 = _.find(docs, function (doc) { return doc._id === doc3._id }) - ; + const docs = index.getAll() + const d1 = _.find(docs, function (doc) { return doc._id === doc1._id }) + const d2 = _.find(docs, function (doc) { return doc._id === doc2._id }) + const d3 = _.find(docs, function (doc) { return doc._id === doc3._id }) // All changes rolled back, including those that didn't trigger an error - d1.a.should.equal(4); - d2.a.should.equal(5); - d3.a.should.equal('abc'); - }); - - done(); - }); - }); - }); - }); - }); + d1.a.should.equal(4) + d2.a.should.equal(5) + d3.a.should.equal('abc') + }) + + done() + }) + }) + }) + }) + }) it('If an index constraint is violated by an update, all changes should be rolled back', function (done) { - d.ensureIndex({ fieldName: 'a', unique: true }); + d.ensureIndex({ fieldName: 'a', unique: true }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 4 }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 5 }, function (err, doc2) { // With this query, candidates are always returned in the order 4, 5, 'abc' so it's always the last one which fails - d.update({ a: { $in: [4, 5, 'abc'] } }, { $set: { a: 10 } }, { multi: true }, function (err) { - assert.isDefined(err); + d.update({ a: { $in: [4, 5, 'abc'] } }, { $set: { a: 10 } }, { multi: true }, function (err) { + assert.isDefined(err) // Check that no index was modified _.each(d.indexes, function (index) { - var docs = index.getAll() - , d1 = _.find(docs, function (doc) { return doc._id === doc1._id }) - , d2 = _.find(docs, function (doc) { return doc._id === doc2._id }) - ; - - d1.a.should.equal(4); - d2.a.should.equal(5); - }); - - done(); - }); - }); - }); - }); - - it("If options.returnUpdatedDocs is true, return all matched docs", function (done) { + const docs = index.getAll() + const d1 = _.find(docs, function (doc) { return doc._id === doc1._id }) + const d2 = _.find(docs, function (doc) { return doc._id === doc2._id }) + + d1.a.should.equal(4) + d2.a.should.equal(5) + }) + + done() + }) + }) + }) + }) + + it('If options.returnUpdatedDocs is true, return all matched docs', function (done) { + // eslint-disable-next-line node/handle-callback-err d.insert([{ a: 4 }, { a: 5 }, { a: 6 }], function (err, docs) { - docs.length.should.equal(3); - - d.update({ a: 7 }, { $set: { u: 1 } }, { multi: true, returnUpdatedDocs: true }, function (err, num, updatedDocs) { - num.should.equal(0); - updatedDocs.length.should.equal(0); - - d.update({ a: 5 }, { $set: { u: 2 } }, { multi: true, returnUpdatedDocs: true }, function (err, num, updatedDocs) { - num.should.equal(1); - updatedDocs.length.should.equal(1); - updatedDocs[0].a.should.equal(5); - updatedDocs[0].u.should.equal(2); - - d.update({ a: { $in: [4, 6] } }, { $set: { u: 3 } }, { multi: true, returnUpdatedDocs: true }, function (err, num, updatedDocs) { - num.should.equal(2); - updatedDocs.length.should.equal(2); - updatedDocs[0].u.should.equal(3); - updatedDocs[1].u.should.equal(3); + docs.length.should.equal(3) + + d.update({ a: 7 }, { $set: { u: 1 } }, { + multi: true, + returnUpdatedDocs: true + // eslint-disable-next-line node/handle-callback-err + }, function (err, num, updatedDocs) { + num.should.equal(0) + updatedDocs.length.should.equal(0) + + d.update({ a: 5 }, { $set: { u: 2 } }, { + multi: true, + returnUpdatedDocs: true + // eslint-disable-next-line node/handle-callback-err + }, function (err, num, updatedDocs) { + num.should.equal(1) + updatedDocs.length.should.equal(1) + updatedDocs[0].a.should.equal(5) + updatedDocs[0].u.should.equal(2) + + d.update({ a: { $in: [4, 6] } }, { $set: { u: 3 } }, { + multi: true, + returnUpdatedDocs: true + // eslint-disable-next-line node/handle-callback-err + }, function (err, num, updatedDocs) { + num.should.equal(2) + updatedDocs.length.should.equal(2) + updatedDocs[0].u.should.equal(3) + updatedDocs[1].u.should.equal(3) if (updatedDocs[0].a === 4) { - updatedDocs[0].a.should.equal(4); - updatedDocs[1].a.should.equal(6); + updatedDocs[0].a.should.equal(4) + updatedDocs[1].a.should.equal(6) } else { - updatedDocs[0].a.should.equal(6); - updatedDocs[1].a.should.equal(4); + updatedDocs[0].a.should.equal(6) + updatedDocs[1].a.should.equal(4) } - done(); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) - it("createdAt property is unchanged and updatedAt correct after an update, even a complete document replacement", function (done) { - var d2 = new Datastore({ inMemoryOnly: true, timestampData: true }); - d2.insert({ a: 1 }); + it('createdAt property is unchanged and updatedAt correct after an update, even a complete document replacement', function (done) { + const d2 = new Datastore({ inMemoryOnly: true, timestampData: true }) + d2.insert({ a: 1 }) + // eslint-disable-next-line node/handle-callback-err d2.findOne({ a: 1 }, function (err, doc) { - var createdAt = doc.createdAt.getTime(); + const createdAt = doc.createdAt.getTime() // Modifying update setTimeout(function () { - d2.update({ a: 1 }, { $set: { b: 2 } }, {}); + d2.update({ a: 1 }, { $set: { b: 2 } }, {}) + // eslint-disable-next-line node/handle-callback-err d2.findOne({ a: 1 }, function (err, doc) { - doc.createdAt.getTime().should.equal(createdAt); - assert.isBelow(Date.now() - doc.updatedAt.getTime(), 5); + doc.createdAt.getTime().should.equal(createdAt) + assert.isBelow(Date.now() - doc.updatedAt.getTime(), 5) // Complete replacement setTimeout(function () { - d2.update({ a: 1 }, { c: 3 }, {}); + d2.update({ a: 1 }, { c: 3 }, {}) + // eslint-disable-next-line node/handle-callback-err d2.findOne({ c: 3 }, function (err, doc) { - doc.createdAt.getTime().should.equal(createdAt); - assert.isBelow(Date.now() - doc.updatedAt.getTime(), 5); + doc.createdAt.getTime().should.equal(createdAt) + assert.isBelow(Date.now() - doc.updatedAt.getTime(), 5) - done(); - }); - }, 20); - }); - }, 20); - }); - }); - - - describe("Callback signature", function () { + done() + }) + }, 20) + }) + }, 20) + }) + }) - it("Regular update, multi false", function (done) { - d.insert({ a: 1 }); - d.insert({ a: 2 }); + describe('Callback signature', function () { + it('Regular update, multi false', function (done) { + d.insert({ a: 1 }) + d.insert({ a: 2 }) // returnUpdatedDocs set to false d.update({ a: 1 }, { $set: { b: 20 } }, {}, function (err, numAffected, affectedDocuments, upsert) { - assert.isNull(err); - numAffected.should.equal(1); - assert.isUndefined(affectedDocuments); - assert.isUndefined(upsert); + assert.isNull(err) + numAffected.should.equal(1) + assert.isUndefined(affectedDocuments) + assert.isUndefined(upsert) // returnUpdatedDocs set to true d.update({ a: 1 }, { $set: { b: 21 } }, { returnUpdatedDocs: true }, function (err, numAffected, affectedDocuments, upsert) { - assert.isNull(err); - numAffected.should.equal(1); - affectedDocuments.a.should.equal(1); - affectedDocuments.b.should.equal(21); - assert.isUndefined(upsert); + assert.isNull(err) + numAffected.should.equal(1) + affectedDocuments.a.should.equal(1) + affectedDocuments.b.should.equal(21) + assert.isUndefined(upsert) - done(); - }); - }); - }); + done() + }) + }) + }) - it("Regular update, multi true", function (done) { - d.insert({ a: 1 }); - d.insert({ a: 2 }); + it('Regular update, multi true', function (done) { + d.insert({ a: 1 }) + d.insert({ a: 2 }) // returnUpdatedDocs set to false d.update({}, { $set: { b: 20 } }, { multi: true }, function (err, numAffected, affectedDocuments, upsert) { - assert.isNull(err); - numAffected.should.equal(2); - assert.isUndefined(affectedDocuments); - assert.isUndefined(upsert); + assert.isNull(err) + numAffected.should.equal(2) + assert.isUndefined(affectedDocuments) + assert.isUndefined(upsert) // returnUpdatedDocs set to true - d.update({}, { $set: { b: 21 } }, { multi: true, returnUpdatedDocs: true }, function (err, numAffected, affectedDocuments, upsert) { - assert.isNull(err); - numAffected.should.equal(2); - affectedDocuments.length.should.equal(2); - assert.isUndefined(upsert); - - done(); - }); - }); - }); + d.update({}, { $set: { b: 21 } }, { + multi: true, + returnUpdatedDocs: true + }, function (err, numAffected, affectedDocuments, upsert) { + assert.isNull(err) + numAffected.should.equal(2) + affectedDocuments.length.should.equal(2) + assert.isUndefined(upsert) + + done() + }) + }) + }) - it("Upsert", function (done) { - d.insert({ a: 1 }); - d.insert({ a: 2 }); + it('Upsert', function (done) { + d.insert({ a: 1 }) + d.insert({ a: 2 }) // Upsert flag not set d.update({ a: 3 }, { $set: { b: 20 } }, {}, function (err, numAffected, affectedDocuments, upsert) { - assert.isNull(err); - numAffected.should.equal(0); - assert.isUndefined(affectedDocuments); - assert.isUndefined(upsert); + assert.isNull(err) + numAffected.should.equal(0) + assert.isUndefined(affectedDocuments) + assert.isUndefined(upsert) // Upsert flag set d.update({ a: 3 }, { $set: { b: 21 } }, { upsert: true }, function (err, numAffected, affectedDocuments, upsert) { - assert.isNull(err); - numAffected.should.equal(1); - affectedDocuments.a.should.equal(3); - affectedDocuments.b.should.equal(21); - upsert.should.equal(true); + assert.isNull(err) + numAffected.should.equal(1) + affectedDocuments.a.should.equal(3) + affectedDocuments.b.should.equal(21) + upsert.should.equal(true) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(3); - done(); - }); - }); - }); - }); - - - }); // ==== End of 'Update - Callback signature' ==== // - - }); // ==== End of 'Update' ==== // - + docs.length.should.equal(3) + done() + }) + }) + }) + }) + }) // ==== End of 'Update - Callback signature' ==== // + }) // ==== End of 'Update' ==== // describe('Remove', function () { - it('Can remove multiple documents', function (done) { - var id1, id2, id3; + let id1 // Test DB status function testPostUpdateState (cb) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); + docs.length.should.equal(1) - Object.keys(docs[0]).length.should.equal(2); - docs[0]._id.should.equal(id1); - docs[0].somedata.should.equal('ok'); + Object.keys(docs[0]).length.should.equal(2) + docs[0]._id.should.equal(id1) + docs[0].somedata.should.equal('ok') - return cb(); - }); + return cb() + }) } // Actually launch the test async.waterfall([ - function (cb) { - d.insert({ somedata: 'ok' }, function (err, doc1) { - id1 = doc1._id; - d.insert({ somedata: 'again', plus: 'additional data' }, function (err, doc2) { - id2 = doc2._id; - d.insert({ somedata: 'again' }, function (err, doc3) { - id3 = doc3._id; - return cb(err); - }); - }); - }); - } - , function (cb) { // Test with query that doesn't match anything - d.remove({ somedata: 'again' }, { multi: true }, function (err, n) { - assert.isNull(err); - n.should.equal(2); - return cb(); - }); - } - , async.apply(testPostUpdateState) - , function (cb) { - d.loadDatabase(function (err) { return cb(err); }); - } - , async.apply(testPostUpdateState) - ], done); - }); + function (cb) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'ok' }, function (err, doc1) { + id1 = doc1._id + // eslint-disable-next-line node/handle-callback-err + d.insert({ somedata: 'again', plus: 'additional data' }, function (err, doc2) { + d.insert({ somedata: 'again' }, function (err, doc3) { + return cb(err) + }) + }) + }) + }, + function (cb) { // Test with query that doesn't match anything + d.remove({ somedata: 'again' }, { multi: true }, function (err, n) { + assert.isNull(err) + n.should.equal(2) + return cb() + }) + }, + async.apply(testPostUpdateState), + function (cb) { + d.loadDatabase(function (err) { return cb(err) }) + }, + async.apply(testPostUpdateState) + ], done) + }) // This tests concurrency issues it('Remove can be called multiple times in parallel and everything that needs to be removed will be', function (done) { d.insert({ planet: 'Earth' }, function () { d.insert({ planet: 'Mars' }, function () { d.insert({ planet: 'Saturn' }, function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(3); + docs.length.should.equal(3) // Remove two docs simultaneously - var toRemove = ['Mars', 'Saturn']; - async.each(toRemove, function(planet, cb) { - d.remove({ planet: planet }, function (err) { return cb(err); }); + const toRemove = ['Mars', 'Saturn'] + async.each(toRemove, function (planet, cb) { + d.remove({ planet: planet }, function (err) { return cb(err) }) + // eslint-disable-next-line node/handle-callback-err }, function (err) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); + docs.length.should.equal(1) - done(); - }); - }); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) + }) + }) it('Returns an error if the query is not well formed', function (done) { d.insert({ hello: 'world' }, function () { d.remove({ $or: { hello: 'world' } }, {}, function (err, nr, upsert) { - assert.isDefined(err); - assert.isUndefined(nr); - assert.isUndefined(upsert); + assert.isDefined(err) + assert.isUndefined(nr) + assert.isUndefined(upsert) - done(); - }); - }); - }); + done() + }) + }) + }) it('Non-multi removes are persistent', function (done) { - d.insert({ a:1, hello: 'world' }, function (err, doc1) { - d.insert({ a:2, hello: 'earth' }, function (err, doc2) { - d.insert({ a:3, hello: 'moto' }, function (err, doc3) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 1, hello: 'world' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 2, hello: 'earth' }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 3, hello: 'moto' }, function (err, doc3) { d.remove({ a: 2 }, {}, function (err) { - assert.isNull(err); + assert.isNull(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.sort(function (a, b) { return a.a - b.a; }); - docs.length.should.equal(2); - _.isEqual(docs[0], { _id: doc1._id, a:1, hello: 'world' }).should.equal(true); - _.isEqual(docs[1], { _id: doc3._id, a:3, hello: 'moto' }).should.equal(true); + docs.sort(function (a, b) { return a.a - b.a }) + docs.length.should.equal(2) + _.isEqual(docs[0], { _id: doc1._id, a: 1, hello: 'world' }).should.equal(true) + _.isEqual(docs[1], { _id: doc3._id, a: 3, hello: 'moto' }).should.equal(true) // Even after a reload the database state hasn't changed d.loadDatabase(function (err) { - assert.isNull(err); + assert.isNull(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.sort(function (a, b) { return a.a - b.a; }); - docs.length.should.equal(2); - _.isEqual(docs[0], { _id: doc1._id, a:1, hello: 'world' }).should.equal(true); - _.isEqual(docs[1], { _id: doc3._id, a:3, hello: 'moto' }).should.equal(true); - - done(); - }); - }); - }); - }); - }); - }); - }); - }); + docs.sort(function (a, b) { return a.a - b.a }) + docs.length.should.equal(2) + _.isEqual(docs[0], { _id: doc1._id, a: 1, hello: 'world' }).should.equal(true) + _.isEqual(docs[1], { _id: doc3._id, a: 3, hello: 'moto' }).should.equal(true) + + done() + }) + }) + }) + }) + }) + }) + }) + }) it('Multi removes are persistent', function (done) { - d.insert({ a:1, hello: 'world' }, function (err, doc1) { - d.insert({ a:2, hello: 'earth' }, function (err, doc2) { - d.insert({ a:3, hello: 'moto' }, function (err, doc3) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 1, hello: 'world' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 2, hello: 'earth' }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 3, hello: 'moto' }, function (err, doc3) { d.remove({ a: { $in: [1, 3] } }, { multi: true }, function (err) { - assert.isNull(err); + assert.isNull(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - _.isEqual(docs[0], { _id: doc2._id, a:2, hello: 'earth' }).should.equal(true); + docs.length.should.equal(1) + _.isEqual(docs[0], { _id: doc2._id, a: 2, hello: 'earth' }).should.equal(true) // Even after a reload the database state hasn't changed d.loadDatabase(function (err) { - assert.isNull(err); + assert.isNull(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - _.isEqual(docs[0], { _id: doc2._id, a:2, hello: 'earth' }).should.equal(true); - - done(); - }); - }); - }); - }); - }); - }); - }); - }); - + docs.length.should.equal(1) + _.isEqual(docs[0], { _id: doc2._id, a: 2, hello: 'earth' }).should.equal(true) + + done() + }) + }) + }) + }) + }) + }) + }) + }) + it('Can remove without the options arg (will use defaults then)', function (done) { - d.insert({ a:1, hello: 'world' }, function (err, doc1) { - d.insert({ a:2, hello: 'earth' }, function (err, doc2) { - d.insert({ a:5, hello: 'pluton' }, function (err, doc3) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 1, hello: 'world' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 2, hello: 'earth' }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ a: 5, hello: 'pluton' }, function (err, doc3) { d.remove({ a: 2 }, function (err, nr) { - assert.isNull(err); - nr.should.equal(1); + assert.isNull(err) + nr.should.equal(1) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - var d1 = _.find(docs, function (doc) { return doc._id === doc1._id }) - , d2 = _.find(docs, function (doc) { return doc._id === doc2._id }) - , d3 = _.find(docs, function (doc) { return doc._id === doc3._id }) - ; - - d1.a.should.equal(1); - assert.isUndefined(d2); - d3.a.should.equal(5); - - done(); - }); - }); - }); - }); - }); - }); - - }); // ==== End of 'Remove' ==== // + const d1 = _.find(docs, function (doc) { return doc._id === doc1._id }) + const d2 = _.find(docs, function (doc) { return doc._id === doc2._id }) + const d3 = _.find(docs, function (doc) { return doc._id === doc3._id }) + d1.a.should.equal(1) + assert.isUndefined(d2) + d3.a.should.equal(5) - describe('Using indexes', function () { + done() + }) + }) + }) + }) + }) + }) + }) // ==== End of 'Remove' ==== // + describe('Using indexes', function () { describe('ensureIndex and index initialization in database loading', function () { - it('ensureIndex can be called right after a loadDatabase and be initialized and filled correctly', function (done) { - var now = new Date() - , rawData = model.serialize({ _id: "aaa", z: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "bbb", z: "2", hello: 'world' }) + '\n' + - model.serialize({ _id: "ccc", z: "3", nested: { today: now } }) - ; + const now = new Date() + const rawData = model.serialize({ _id: 'aaa', z: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: 'bbb', z: '2', hello: 'world' }) + '\n' + + model.serialize({ _id: 'ccc', z: '3', nested: { today: now } }) - d.getAllData().length.should.equal(0); + d.getAllData().length.should.equal(0) fs.writeFile(testDb, rawData, 'utf8', function () { d.loadDatabase(function () { - d.getAllData().length.should.equal(3); - - assert.deepEqual(Object.keys(d.indexes), ['_id']); - - d.ensureIndex({ fieldName: 'z' }); - d.indexes.z.fieldName.should.equal('z'); - d.indexes.z.unique.should.equal(false); - d.indexes.z.sparse.should.equal(false); - d.indexes.z.tree.getNumberOfKeys().should.equal(3); - d.indexes.z.tree.search('1')[0].should.equal(d.getAllData()[0]); - d.indexes.z.tree.search('2')[0].should.equal(d.getAllData()[1]); - d.indexes.z.tree.search('3')[0].should.equal(d.getAllData()[2]); - - done(); - }); - }); - }); - + d.getAllData().length.should.equal(3) + + assert.deepEqual(Object.keys(d.indexes), ['_id']) + + d.ensureIndex({ fieldName: 'z' }) + d.indexes.z.fieldName.should.equal('z') + d.indexes.z.unique.should.equal(false) + d.indexes.z.sparse.should.equal(false) + d.indexes.z.tree.getNumberOfKeys().should.equal(3) + d.indexes.z.tree.search('1')[0].should.equal(d.getAllData()[0]) + d.indexes.z.tree.search('2')[0].should.equal(d.getAllData()[1]) + d.indexes.z.tree.search('3')[0].should.equal(d.getAllData()[2]) + + done() + }) + }) + }) + it('ensureIndex can be called twice on the same field, the second call will ahve no effect', function (done) { - Object.keys(d.indexes).length.should.equal(1); - Object.keys(d.indexes)[0].should.equal("_id"); - - d.insert({ planet: "Earth" }, function () { - d.insert({ planet: "Mars" }, function () { + Object.keys(d.indexes).length.should.equal(1) + Object.keys(d.indexes)[0].should.equal('_id') + + d.insert({ planet: 'Earth' }, function () { + d.insert({ planet: 'Mars' }, function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(2); - - d.ensureIndex({ fieldName: "planet" }, function (err) { - assert.isNull(err); - Object.keys(d.indexes).length.should.equal(2); - Object.keys(d.indexes)[0].should.equal("_id"); - Object.keys(d.indexes)[1].should.equal("planet"); - - d.indexes.planet.getAll().length.should.equal(2); - + docs.length.should.equal(2) + + d.ensureIndex({ fieldName: 'planet' }, function (err) { + assert.isNull(err) + Object.keys(d.indexes).length.should.equal(2) + Object.keys(d.indexes)[0].should.equal('_id') + Object.keys(d.indexes)[1].should.equal('planet') + + d.indexes.planet.getAll().length.should.equal(2) + // This second call has no effect, documents don't get inserted twice in the index - d.ensureIndex({ fieldName: "planet" }, function (err) { - assert.isNull(err); - Object.keys(d.indexes).length.should.equal(2); - Object.keys(d.indexes)[0].should.equal("_id"); - Object.keys(d.indexes)[1].should.equal("planet"); - - d.indexes.planet.getAll().length.should.equal(2); - - done(); - }); - }); - }); - }); - }); - }); + d.ensureIndex({ fieldName: 'planet' }, function (err) { + assert.isNull(err) + Object.keys(d.indexes).length.should.equal(2) + Object.keys(d.indexes)[0].should.equal('_id') + Object.keys(d.indexes)[1].should.equal('planet') + + d.indexes.planet.getAll().length.should.equal(2) + + done() + }) + }) + }) + }) + }) + }) it('ensureIndex can be called after the data set was modified and the index still be correct', function (done) { - var rawData = model.serialize({ _id: "aaa", z: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "bbb", z: "2", hello: 'world' }) - ; + const rawData = model.serialize({ _id: 'aaa', z: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: 'bbb', z: '2', hello: 'world' }) - d.getAllData().length.should.equal(0); + d.getAllData().length.should.equal(0) fs.writeFile(testDb, rawData, 'utf8', function () { d.loadDatabase(function () { - d.getAllData().length.should.equal(2); + d.getAllData().length.should.equal(2) - assert.deepEqual(Object.keys(d.indexes), ['_id']); + assert.deepEqual(Object.keys(d.indexes), ['_id']) - d.insert({ z: "12", yes: 'yes' }, function (err, newDoc1) { - d.insert({ z: "14", nope: 'nope' }, function (err, newDoc2) { - d.remove({ z: "2" }, {}, function () { - d.update({ z: "1" }, { $set: { 'yes': 'yep' } }, {}, function () { - assert.deepEqual(Object.keys(d.indexes), ['_id']); + // eslint-disable-next-line node/handle-callback-err + d.insert({ z: '12', yes: 'yes' }, function (err, newDoc1) { + // eslint-disable-next-line node/handle-callback-err + d.insert({ z: '14', nope: 'nope' }, function (err, newDoc2) { + d.remove({ z: '2' }, {}, function () { + d.update({ z: '1' }, { $set: { yes: 'yep' } }, {}, function () { + assert.deepEqual(Object.keys(d.indexes), ['_id']) - d.ensureIndex({ fieldName: 'z' }); - d.indexes.z.fieldName.should.equal('z'); - d.indexes.z.unique.should.equal(false); - d.indexes.z.sparse.should.equal(false); - d.indexes.z.tree.getNumberOfKeys().should.equal(3); + d.ensureIndex({ fieldName: 'z' }) + d.indexes.z.fieldName.should.equal('z') + d.indexes.z.unique.should.equal(false) + d.indexes.z.sparse.should.equal(false) + d.indexes.z.tree.getNumberOfKeys().should.equal(3) // The pointers in the _id and z indexes are the same - d.indexes.z.tree.search('1')[0].should.equal(d.indexes._id.getMatching('aaa')[0]); - d.indexes.z.tree.search('12')[0].should.equal(d.indexes._id.getMatching(newDoc1._id)[0]); - d.indexes.z.tree.search('14')[0].should.equal(d.indexes._id.getMatching(newDoc2._id)[0]); + d.indexes.z.tree.search('1')[0].should.equal(d.indexes._id.getMatching('aaa')[0]) + d.indexes.z.tree.search('12')[0].should.equal(d.indexes._id.getMatching(newDoc1._id)[0]) + d.indexes.z.tree.search('14')[0].should.equal(d.indexes._id.getMatching(newDoc2._id)[0]) // The data in the z index is correct + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - var doc0 = _.find(docs, function (doc) { return doc._id === 'aaa'; }) - , doc1 = _.find(docs, function (doc) { return doc._id === newDoc1._id; }) - , doc2 = _.find(docs, function (doc) { return doc._id === newDoc2._id; }) - ; - - docs.length.should.equal(3); - - assert.deepEqual(doc0, { _id: "aaa", z: "1", a: 2, ages: [1, 5, 12], yes: 'yep' }); - assert.deepEqual(doc1, { _id: newDoc1._id, z: "12", yes: 'yes' }); - assert.deepEqual(doc2, { _id: newDoc2._id, z: "14", nope: 'nope' }); - - done(); - }); - }); - }); - }); - }); - }); - }); - }); + const doc0 = _.find(docs, function (doc) { return doc._id === 'aaa' }) + const doc1 = _.find(docs, function (doc) { return doc._id === newDoc1._id }) + const doc2 = _.find(docs, function (doc) { return doc._id === newDoc2._id }) + + docs.length.should.equal(3) + + assert.deepEqual(doc0, { _id: 'aaa', z: '1', a: 2, ages: [1, 5, 12], yes: 'yep' }) + assert.deepEqual(doc1, { _id: newDoc1._id, z: '12', yes: 'yes' }) + assert.deepEqual(doc2, { _id: newDoc2._id, z: '14', nope: 'nope' }) + + done() + }) + }) + }) + }) + }) + }) + }) + }) it('ensureIndex can be called before a loadDatabase and still be initialized and filled correctly', function (done) { - var now = new Date() - , rawData = model.serialize({ _id: "aaa", z: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "bbb", z: "2", hello: 'world' }) + '\n' + - model.serialize({ _id: "ccc", z: "3", nested: { today: now } }) - ; + const now = new Date() + const rawData = model.serialize({ _id: 'aaa', z: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: 'bbb', z: '2', hello: 'world' }) + '\n' + + model.serialize({ _id: 'ccc', z: '3', nested: { today: now } }) - d.getAllData().length.should.equal(0); + d.getAllData().length.should.equal(0) - d.ensureIndex({ fieldName: 'z' }); - d.indexes.z.fieldName.should.equal('z'); - d.indexes.z.unique.should.equal(false); - d.indexes.z.sparse.should.equal(false); - d.indexes.z.tree.getNumberOfKeys().should.equal(0); + d.ensureIndex({ fieldName: 'z' }) + d.indexes.z.fieldName.should.equal('z') + d.indexes.z.unique.should.equal(false) + d.indexes.z.sparse.should.equal(false) + d.indexes.z.tree.getNumberOfKeys().should.equal(0) fs.writeFile(testDb, rawData, 'utf8', function () { d.loadDatabase(function () { - var doc1 = _.find(d.getAllData(), function (doc) { return doc.z === "1"; }) - , doc2 = _.find(d.getAllData(), function (doc) { return doc.z === "2"; }) - , doc3 = _.find(d.getAllData(), function (doc) { return doc.z === "3"; }) - ; + const doc1 = _.find(d.getAllData(), function (doc) { return doc.z === '1' }) + const doc2 = _.find(d.getAllData(), function (doc) { return doc.z === '2' }) + const doc3 = _.find(d.getAllData(), function (doc) { return doc.z === '3' }) - d.getAllData().length.should.equal(3); + d.getAllData().length.should.equal(3) - d.indexes.z.tree.getNumberOfKeys().should.equal(3); - d.indexes.z.tree.search('1')[0].should.equal(doc1); - d.indexes.z.tree.search('2')[0].should.equal(doc2); - d.indexes.z.tree.search('3')[0].should.equal(doc3); + d.indexes.z.tree.getNumberOfKeys().should.equal(3) + d.indexes.z.tree.search('1')[0].should.equal(doc1) + d.indexes.z.tree.search('2')[0].should.equal(doc2) + d.indexes.z.tree.search('3')[0].should.equal(doc3) - done(); - }); - }); - }); + done() + }) + }) + }) it('Can initialize multiple indexes on a database load', function (done) { - var now = new Date() - , rawData = model.serialize({ _id: "aaa", z: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "bbb", z: "2", a: 'world' }) + '\n' + - model.serialize({ _id: "ccc", z: "3", a: { today: now } }) - ; + const now = new Date() + const rawData = model.serialize({ _id: 'aaa', z: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: 'bbb', z: '2', a: 'world' }) + '\n' + + model.serialize({ _id: 'ccc', z: '3', a: { today: now } }) - d.getAllData().length.should.equal(0); + d.getAllData().length.should.equal(0) d.ensureIndex({ fieldName: 'z' }, function () { d.ensureIndex({ fieldName: 'a' }, function () { - d.indexes.a.tree.getNumberOfKeys().should.equal(0); - d.indexes.z.tree.getNumberOfKeys().should.equal(0); + d.indexes.a.tree.getNumberOfKeys().should.equal(0) + d.indexes.z.tree.getNumberOfKeys().should.equal(0) fs.writeFile(testDb, rawData, 'utf8', function () { d.loadDatabase(function (err) { - var doc1 = _.find(d.getAllData(), function (doc) { return doc.z === "1"; }) - , doc2 = _.find(d.getAllData(), function (doc) { return doc.z === "2"; }) - , doc3 = _.find(d.getAllData(), function (doc) { return doc.z === "3"; }) - ; - - assert.isNull(err); - d.getAllData().length.should.equal(3); - - d.indexes.z.tree.getNumberOfKeys().should.equal(3); - d.indexes.z.tree.search('1')[0].should.equal(doc1); - d.indexes.z.tree.search('2')[0].should.equal(doc2); - d.indexes.z.tree.search('3')[0].should.equal(doc3); - - d.indexes.a.tree.getNumberOfKeys().should.equal(3); - d.indexes.a.tree.search(2)[0].should.equal(doc1); - d.indexes.a.tree.search('world')[0].should.equal(doc2); - d.indexes.a.tree.search({ today: now })[0].should.equal(doc3); - - done(); - }); - }); - }); - - }); - }); + const doc1 = _.find(d.getAllData(), function (doc) { return doc.z === '1' }) + const doc2 = _.find(d.getAllData(), function (doc) { return doc.z === '2' }) + const doc3 = _.find(d.getAllData(), function (doc) { return doc.z === '3' }) + + assert.isNull(err) + d.getAllData().length.should.equal(3) + + d.indexes.z.tree.getNumberOfKeys().should.equal(3) + d.indexes.z.tree.search('1')[0].should.equal(doc1) + d.indexes.z.tree.search('2')[0].should.equal(doc2) + d.indexes.z.tree.search('3')[0].should.equal(doc3) + + d.indexes.a.tree.getNumberOfKeys().should.equal(3) + d.indexes.a.tree.search(2)[0].should.equal(doc1) + d.indexes.a.tree.search('world')[0].should.equal(doc2) + d.indexes.a.tree.search({ today: now })[0].should.equal(doc3) + + done() + }) + }) + }) + }) + }) it('If a unique constraint is not respected, database loading will not work and no data will be inserted', function (done) { - var now = new Date() - , rawData = model.serialize({ _id: "aaa", z: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "bbb", z: "2", a: 'world' }) + '\n' + - model.serialize({ _id: "ccc", z: "1", a: { today: now } }) - ; + const now = new Date() + const rawData = model.serialize({ _id: 'aaa', z: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: 'bbb', z: '2', a: 'world' }) + '\n' + + model.serialize({ _id: 'ccc', z: '1', a: { today: now } }) - d.getAllData().length.should.equal(0); + d.getAllData().length.should.equal(0) - d.ensureIndex({ fieldName: 'z', unique: true }); - d.indexes.z.tree.getNumberOfKeys().should.equal(0); + d.ensureIndex({ fieldName: 'z', unique: true }) + d.indexes.z.tree.getNumberOfKeys().should.equal(0) fs.writeFile(testDb, rawData, 'utf8', function () { d.loadDatabase(function (err) { - err.errorType.should.equal('uniqueViolated'); - err.key.should.equal("1"); - d.getAllData().length.should.equal(0); - d.indexes.z.tree.getNumberOfKeys().should.equal(0); + err.errorType.should.equal('uniqueViolated') + err.key.should.equal('1') + d.getAllData().length.should.equal(0) + d.indexes.z.tree.getNumberOfKeys().should.equal(0) - done(); - }); - }); - }); + done() + }) + }) + }) it('If a unique constraint is not respected, ensureIndex will return an error and not create an index', function (done) { d.insert({ a: 1, b: 4 }, function () { d.insert({ a: 2, b: 45 }, function () { d.insert({ a: 1, b: 3 }, function () { d.ensureIndex({ fieldName: 'b' }, function (err) { - assert.isNull(err); + assert.isNull(err) d.ensureIndex({ fieldName: 'a', unique: true }, function (err) { - err.errorType.should.equal('uniqueViolated'); - assert.deepEqual(Object.keys(d.indexes), ['_id', 'b']); - - done(); - }); - }); - }); - }); - }); - }); - + err.errorType.should.equal('uniqueViolated') + assert.deepEqual(Object.keys(d.indexes), ['_id', 'b']) + + done() + }) + }) + }) + }) + }) + }) + it('Can remove an index', function (done) { d.ensureIndex({ fieldName: 'e' }, function (err) { - assert.isNull(err); - - Object.keys(d.indexes).length.should.equal(2); - assert.isNotNull(d.indexes.e); - - d.removeIndex("e", function (err) { - assert.isNull(err); - Object.keys(d.indexes).length.should.equal(1); - assert.isUndefined(d.indexes.e); - - done(); - }); - }); - }); - - }); // ==== End of 'ensureIndex and index initialization in database loading' ==== // - - - describe('Indexing newly inserted documents', function () { + assert.isNull(err) + + Object.keys(d.indexes).length.should.equal(2) + assert.isNotNull(d.indexes.e) + d.removeIndex('e', function (err) { + assert.isNull(err) + Object.keys(d.indexes).length.should.equal(1) + assert.isUndefined(d.indexes.e) + + done() + }) + }) + }) + }) // ==== End of 'ensureIndex and index initialization in database loading' ==== // + + describe('Indexing newly inserted documents', function () { it('Newly inserted documents are indexed', function (done) { - d.ensureIndex({ fieldName: 'z' }); - d.indexes.z.tree.getNumberOfKeys().should.equal(0); + d.ensureIndex({ fieldName: 'z' }) + d.indexes.z.tree.getNumberOfKeys().should.equal(0) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, z: 'yes' }, function (err, newDoc) { - d.indexes.z.tree.getNumberOfKeys().should.equal(1); - assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]); + d.indexes.z.tree.getNumberOfKeys().should.equal(1) + assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 5, z: 'nope' }, function (err, newDoc) { - d.indexes.z.tree.getNumberOfKeys().should.equal(2); - assert.deepEqual(d.indexes.z.getMatching('nope'), [newDoc]); + d.indexes.z.tree.getNumberOfKeys().should.equal(2) + assert.deepEqual(d.indexes.z.getMatching('nope'), [newDoc]) - done(); - }); - }); - }); + done() + }) + }) + }) it('If multiple indexes are defined, the document is inserted in all of them', function (done) { - d.ensureIndex({ fieldName: 'z' }); - d.ensureIndex({ fieldName: 'ya' }); - d.indexes.z.tree.getNumberOfKeys().should.equal(0); + d.ensureIndex({ fieldName: 'z' }) + d.ensureIndex({ fieldName: 'ya' }) + d.indexes.z.tree.getNumberOfKeys().should.equal(0) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, z: 'yes', ya: 'indeed' }, function (err, newDoc) { - d.indexes.z.tree.getNumberOfKeys().should.equal(1); - d.indexes.ya.tree.getNumberOfKeys().should.equal(1); - assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]); - assert.deepEqual(d.indexes.ya.getMatching('indeed'), [newDoc]); + d.indexes.z.tree.getNumberOfKeys().should.equal(1) + d.indexes.ya.tree.getNumberOfKeys().should.equal(1) + assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]) + assert.deepEqual(d.indexes.ya.getMatching('indeed'), [newDoc]) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 5, z: 'nope', ya: 'sure' }, function (err, newDoc2) { - d.indexes.z.tree.getNumberOfKeys().should.equal(2); - d.indexes.ya.tree.getNumberOfKeys().should.equal(2); - assert.deepEqual(d.indexes.z.getMatching('nope'), [newDoc2]); - assert.deepEqual(d.indexes.ya.getMatching('sure'), [newDoc2]); + d.indexes.z.tree.getNumberOfKeys().should.equal(2) + d.indexes.ya.tree.getNumberOfKeys().should.equal(2) + assert.deepEqual(d.indexes.z.getMatching('nope'), [newDoc2]) + assert.deepEqual(d.indexes.ya.getMatching('sure'), [newDoc2]) - done(); - }); - }); - }); + done() + }) + }) + }) it('Can insert two docs at the same key for a non unique index', function (done) { - d.ensureIndex({ fieldName: 'z' }); - d.indexes.z.tree.getNumberOfKeys().should.equal(0); + d.ensureIndex({ fieldName: 'z' }) + d.indexes.z.tree.getNumberOfKeys().should.equal(0) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, z: 'yes' }, function (err, newDoc) { - d.indexes.z.tree.getNumberOfKeys().should.equal(1); - assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]); + d.indexes.z.tree.getNumberOfKeys().should.equal(1) + assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 5, z: 'yes' }, function (err, newDoc2) { - d.indexes.z.tree.getNumberOfKeys().should.equal(1); - assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc, newDoc2]); + d.indexes.z.tree.getNumberOfKeys().should.equal(1) + assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc, newDoc2]) - done(); - }); - }); - }); + done() + }) + }) + }) it('If the index has a unique constraint, an error is thrown if it is violated and the data is not modified', function (done) { - d.ensureIndex({ fieldName: 'z', unique: true }); - d.indexes.z.tree.getNumberOfKeys().should.equal(0); + d.ensureIndex({ fieldName: 'z', unique: true }) + d.indexes.z.tree.getNumberOfKeys().should.equal(0) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, z: 'yes' }, function (err, newDoc) { - d.indexes.z.tree.getNumberOfKeys().should.equal(1); - assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]); + d.indexes.z.tree.getNumberOfKeys().should.equal(1) + assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]) d.insert({ a: 5, z: 'yes' }, function (err) { - err.errorType.should.equal('uniqueViolated'); - err.key.should.equal('yes'); + err.errorType.should.equal('uniqueViolated') + err.key.should.equal('yes') // Index didn't change - d.indexes.z.tree.getNumberOfKeys().should.equal(1); - assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]); + d.indexes.z.tree.getNumberOfKeys().should.equal(1) + assert.deepEqual(d.indexes.z.getMatching('yes'), [newDoc]) // Data didn't change - assert.deepEqual(d.getAllData(), [newDoc]); + assert.deepEqual(d.getAllData(), [newDoc]) d.loadDatabase(function () { - d.getAllData().length.should.equal(1); - assert.deepEqual(d.getAllData()[0], newDoc); + d.getAllData().length.should.equal(1) + assert.deepEqual(d.getAllData()[0], newDoc) - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('If an index has a unique constraint, other indexes cannot be modified when it raises an error', function (done) { - d.ensureIndex({ fieldName: 'nonu1' }); - d.ensureIndex({ fieldName: 'uni', unique: true }); - d.ensureIndex({ fieldName: 'nonu2' }); + d.ensureIndex({ fieldName: 'nonu1' }) + d.ensureIndex({ fieldName: 'uni', unique: true }) + d.ensureIndex({ fieldName: 'nonu2' }) d.insert({ nonu1: 'yes', nonu2: 'yes2', uni: 'willfail' }, function (err, newDoc) { - assert.isNull(err); - d.indexes.nonu1.tree.getNumberOfKeys().should.equal(1); - d.indexes.uni.tree.getNumberOfKeys().should.equal(1); - d.indexes.nonu2.tree.getNumberOfKeys().should.equal(1); + assert.isNull(err) + d.indexes.nonu1.tree.getNumberOfKeys().should.equal(1) + d.indexes.uni.tree.getNumberOfKeys().should.equal(1) + d.indexes.nonu2.tree.getNumberOfKeys().should.equal(1) d.insert({ nonu1: 'no', nonu2: 'no2', uni: 'willfail' }, function (err) { - err.errorType.should.equal('uniqueViolated'); + err.errorType.should.equal('uniqueViolated') // No index was modified - d.indexes.nonu1.tree.getNumberOfKeys().should.equal(1); - d.indexes.uni.tree.getNumberOfKeys().should.equal(1); - d.indexes.nonu2.tree.getNumberOfKeys().should.equal(1); + d.indexes.nonu1.tree.getNumberOfKeys().should.equal(1) + d.indexes.uni.tree.getNumberOfKeys().should.equal(1) + d.indexes.nonu2.tree.getNumberOfKeys().should.equal(1) - assert.deepEqual(d.indexes.nonu1.getMatching('yes'), [newDoc]); - assert.deepEqual(d.indexes.uni.getMatching('willfail'), [newDoc]); - assert.deepEqual(d.indexes.nonu2.getMatching('yes2'), [newDoc]); + assert.deepEqual(d.indexes.nonu1.getMatching('yes'), [newDoc]) + assert.deepEqual(d.indexes.uni.getMatching('willfail'), [newDoc]) + assert.deepEqual(d.indexes.nonu2.getMatching('yes2'), [newDoc]) - done(); - }); - }); - }); + done() + }) + }) + }) it('Unique indexes prevent you from inserting two docs where the field is undefined except if theyre sparse', function (done) { - d.ensureIndex({ fieldName: 'zzz', unique: true }); - d.indexes.zzz.tree.getNumberOfKeys().should.equal(0); + d.ensureIndex({ fieldName: 'zzz', unique: true }) + d.indexes.zzz.tree.getNumberOfKeys().should.equal(0) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, z: 'yes' }, function (err, newDoc) { - d.indexes.zzz.tree.getNumberOfKeys().should.equal(1); - assert.deepEqual(d.indexes.zzz.getMatching(undefined), [newDoc]); + d.indexes.zzz.tree.getNumberOfKeys().should.equal(1) + assert.deepEqual(d.indexes.zzz.getMatching(undefined), [newDoc]) d.insert({ a: 5, z: 'other' }, function (err) { - err.errorType.should.equal('uniqueViolated'); - assert.isUndefined(err.key); + err.errorType.should.equal('uniqueViolated') + assert.isUndefined(err.key) - d.ensureIndex({ fieldName: 'yyy', unique: true, sparse: true }); + d.ensureIndex({ fieldName: 'yyy', unique: true, sparse: true }) d.insert({ a: 5, z: 'other', zzz: 'set' }, function (err) { - assert.isNull(err); - d.indexes.yyy.getAll().length.should.equal(0); // Nothing indexed - d.indexes.zzz.getAll().length.should.equal(2); + assert.isNull(err) + d.indexes.yyy.getAll().length.should.equal(0) // Nothing indexed + d.indexes.zzz.getAll().length.should.equal(2) - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('Insertion still works as before with indexing', function (done) { - d.ensureIndex({ fieldName: 'a' }); - d.ensureIndex({ fieldName: 'b' }); + d.ensureIndex({ fieldName: 'a' }) + d.ensureIndex({ fieldName: 'b' }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 1, b: 'hello' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, b: 'si' }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - assert.deepEqual(doc1, _.find(docs, function (d) { return d._id === doc1._id; })); - assert.deepEqual(doc2, _.find(docs, function (d) { return d._id === doc2._id; })); + assert.deepEqual(doc1, _.find(docs, function (d) { return d._id === doc1._id })) + assert.deepEqual(doc2, _.find(docs, function (d) { return d._id === doc2._id })) - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('All indexes point to the same data as the main index on _id', function (done) { - d.ensureIndex({ fieldName: 'a' }); + d.ensureIndex({ fieldName: 'a' }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 1, b: 'hello' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, b: 'si' }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(2); - d.getAllData().length.should.equal(2); + docs.length.should.equal(2) + d.getAllData().length.should.equal(2) - d.indexes._id.getMatching(doc1._id).length.should.equal(1); - d.indexes.a.getMatching(1).length.should.equal(1); - d.indexes._id.getMatching(doc1._id)[0].should.equal(d.indexes.a.getMatching(1)[0]); + d.indexes._id.getMatching(doc1._id).length.should.equal(1) + d.indexes.a.getMatching(1).length.should.equal(1) + d.indexes._id.getMatching(doc1._id)[0].should.equal(d.indexes.a.getMatching(1)[0]) - d.indexes._id.getMatching(doc2._id).length.should.equal(1); - d.indexes.a.getMatching(2).length.should.equal(1); - d.indexes._id.getMatching(doc2._id)[0].should.equal(d.indexes.a.getMatching(2)[0]); + d.indexes._id.getMatching(doc2._id).length.should.equal(1) + d.indexes.a.getMatching(2).length.should.equal(1) + d.indexes._id.getMatching(doc2._id)[0].should.equal(d.indexes.a.getMatching(2)[0]) - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('If a unique constraint is violated, no index is changed, including the main one', function (done) { - d.ensureIndex({ fieldName: 'a', unique: true }); + d.ensureIndex({ fieldName: 'a', unique: true }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 1, b: 'hello' }, function (err, doc1) { d.insert({ a: 1, b: 'si' }, function (err) { - assert.isDefined(err); + assert.isDefined(err) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - d.getAllData().length.should.equal(1); + docs.length.should.equal(1) + d.getAllData().length.should.equal(1) - d.indexes._id.getMatching(doc1._id).length.should.equal(1); - d.indexes.a.getMatching(1).length.should.equal(1); - d.indexes._id.getMatching(doc1._id)[0].should.equal(d.indexes.a.getMatching(1)[0]); + d.indexes._id.getMatching(doc1._id).length.should.equal(1) + d.indexes.a.getMatching(1).length.should.equal(1) + d.indexes._id.getMatching(doc1._id)[0].should.equal(d.indexes.a.getMatching(1)[0]) - d.indexes.a.getMatching(2).length.should.equal(0); + d.indexes.a.getMatching(2).length.should.equal(0) - done(); - }); - }); - }); - }); - - }); // ==== End of 'Indexing newly inserted documents' ==== // + done() + }) + }) + }) + }) + }) // ==== End of 'Indexing newly inserted documents' ==== // describe('Updating indexes upon document update', function () { - it('Updating docs still works as before with indexing', function (done) { - d.ensureIndex({ fieldName: 'a' }); + d.ensureIndex({ fieldName: 'a' }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 1, b: 'hello' }, function (err, _doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, b: 'si' }, function (err, _doc2) { d.update({ a: 1 }, { $set: { a: 456, b: 'no' } }, {}, function (err, nr) { - var data = d.getAllData() - , doc1 = _.find(data, function (doc) { return doc._id === _doc1._id; }) - , doc2 = _.find(data, function (doc) { return doc._id === _doc2._id; }) - ; + const data = d.getAllData() + const doc1 = _.find(data, function (doc) { return doc._id === _doc1._id }) + const doc2 = _.find(data, function (doc) { return doc._id === _doc2._id }) - assert.isNull(err); - nr.should.equal(1); + assert.isNull(err) + nr.should.equal(1) - data.length.should.equal(2); - assert.deepEqual(doc1, { a: 456, b: 'no', _id: _doc1._id }); - assert.deepEqual(doc2, { a: 2, b: 'si', _id: _doc2._id }); + data.length.should.equal(2) + assert.deepEqual(doc1, { a: 456, b: 'no', _id: _doc1._id }) + assert.deepEqual(doc2, { a: 2, b: 'si', _id: _doc2._id }) d.update({}, { $inc: { a: 10 }, $set: { b: 'same' } }, { multi: true }, function (err, nr) { - var data = d.getAllData() - , doc1 = _.find(data, function (doc) { return doc._id === _doc1._id; }) - , doc2 = _.find(data, function (doc) { return doc._id === _doc2._id; }) - ; + const data = d.getAllData() + const doc1 = _.find(data, function (doc) { return doc._id === _doc1._id }) + const doc2 = _.find(data, function (doc) { return doc._id === _doc2._id }) - assert.isNull(err); - nr.should.equal(2); + assert.isNull(err) + nr.should.equal(2) - data.length.should.equal(2); - assert.deepEqual(doc1, { a: 466, b: 'same', _id: _doc1._id }); - assert.deepEqual(doc2, { a: 12, b: 'same', _id: _doc2._id }); + data.length.should.equal(2) + assert.deepEqual(doc1, { a: 466, b: 'same', _id: _doc1._id }) + assert.deepEqual(doc2, { a: 12, b: 'same', _id: _doc2._id }) - done(); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) it('Indexes get updated when a document (or multiple documents) is updated', function (done) { - d.ensureIndex({ fieldName: 'a' }); - d.ensureIndex({ fieldName: 'b' }); + d.ensureIndex({ fieldName: 'a' }) + d.ensureIndex({ fieldName: 'b' }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 1, b: 'hello' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, b: 'si' }, function (err, doc2) { // Simple update d.update({ a: 1 }, { $set: { a: 456, b: 'no' } }, {}, function (err, nr) { - assert.isNull(err); - nr.should.equal(1); + assert.isNull(err) + nr.should.equal(1) - d.indexes.a.tree.getNumberOfKeys().should.equal(2); - d.indexes.a.getMatching(456)[0]._id.should.equal(doc1._id); - d.indexes.a.getMatching(2)[0]._id.should.equal(doc2._id); + d.indexes.a.tree.getNumberOfKeys().should.equal(2) + d.indexes.a.getMatching(456)[0]._id.should.equal(doc1._id) + d.indexes.a.getMatching(2)[0]._id.should.equal(doc2._id) - d.indexes.b.tree.getNumberOfKeys().should.equal(2); - d.indexes.b.getMatching('no')[0]._id.should.equal(doc1._id); - d.indexes.b.getMatching('si')[0]._id.should.equal(doc2._id); + d.indexes.b.tree.getNumberOfKeys().should.equal(2) + d.indexes.b.getMatching('no')[0]._id.should.equal(doc1._id) + d.indexes.b.getMatching('si')[0]._id.should.equal(doc2._id) // The same pointers are shared between all indexes - d.indexes.a.tree.getNumberOfKeys().should.equal(2); - d.indexes.b.tree.getNumberOfKeys().should.equal(2); - d.indexes._id.tree.getNumberOfKeys().should.equal(2); - d.indexes.a.getMatching(456)[0].should.equal(d.indexes._id.getMatching(doc1._id)[0]); - d.indexes.b.getMatching('no')[0].should.equal(d.indexes._id.getMatching(doc1._id)[0]); - d.indexes.a.getMatching(2)[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]); - d.indexes.b.getMatching('si')[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]); + d.indexes.a.tree.getNumberOfKeys().should.equal(2) + d.indexes.b.tree.getNumberOfKeys().should.equal(2) + d.indexes._id.tree.getNumberOfKeys().should.equal(2) + d.indexes.a.getMatching(456)[0].should.equal(d.indexes._id.getMatching(doc1._id)[0]) + d.indexes.b.getMatching('no')[0].should.equal(d.indexes._id.getMatching(doc1._id)[0]) + d.indexes.a.getMatching(2)[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]) + d.indexes.b.getMatching('si')[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]) // Multi update d.update({}, { $inc: { a: 10 }, $set: { b: 'same' } }, { multi: true }, function (err, nr) { - assert.isNull(err); - nr.should.equal(2); + assert.isNull(err) + nr.should.equal(2) - d.indexes.a.tree.getNumberOfKeys().should.equal(2); - d.indexes.a.getMatching(466)[0]._id.should.equal(doc1._id); - d.indexes.a.getMatching(12)[0]._id.should.equal(doc2._id); + d.indexes.a.tree.getNumberOfKeys().should.equal(2) + d.indexes.a.getMatching(466)[0]._id.should.equal(doc1._id) + d.indexes.a.getMatching(12)[0]._id.should.equal(doc2._id) - d.indexes.b.tree.getNumberOfKeys().should.equal(1); - d.indexes.b.getMatching('same').length.should.equal(2); - _.pluck(d.indexes.b.getMatching('same'), '_id').should.contain(doc1._id); - _.pluck(d.indexes.b.getMatching('same'), '_id').should.contain(doc2._id); + d.indexes.b.tree.getNumberOfKeys().should.equal(1) + d.indexes.b.getMatching('same').length.should.equal(2) + _.pluck(d.indexes.b.getMatching('same'), '_id').should.contain(doc1._id) + _.pluck(d.indexes.b.getMatching('same'), '_id').should.contain(doc2._id) // The same pointers are shared between all indexes - d.indexes.a.tree.getNumberOfKeys().should.equal(2); - d.indexes.b.tree.getNumberOfKeys().should.equal(1); - d.indexes.b.getAll().length.should.equal(2); - d.indexes._id.tree.getNumberOfKeys().should.equal(2); - d.indexes.a.getMatching(466)[0].should.equal(d.indexes._id.getMatching(doc1._id)[0]); - d.indexes.a.getMatching(12)[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]); + d.indexes.a.tree.getNumberOfKeys().should.equal(2) + d.indexes.b.tree.getNumberOfKeys().should.equal(1) + d.indexes.b.getAll().length.should.equal(2) + d.indexes._id.tree.getNumberOfKeys().should.equal(2) + d.indexes.a.getMatching(466)[0].should.equal(d.indexes._id.getMatching(doc1._id)[0]) + d.indexes.a.getMatching(12)[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]) // Can't test the pointers in b as their order is randomized, but it is the same as with a - done(); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) it('If a simple update violates a contraint, all changes are rolled back and an error is thrown', function (done) { - d.ensureIndex({ fieldName: 'a', unique: true }); - d.ensureIndex({ fieldName: 'b', unique: true }); - d.ensureIndex({ fieldName: 'c', unique: true }); + d.ensureIndex({ fieldName: 'a', unique: true }) + d.ensureIndex({ fieldName: 'b', unique: true }) + d.ensureIndex({ fieldName: 'c', unique: true }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 1, b: 10, c: 100 }, function (err, _doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, b: 20, c: 200 }, function (err, _doc2) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 3, b: 30, c: 300 }, function (err, _doc3) { // Will conflict with doc3 d.update({ a: 2 }, { $inc: { a: 10, c: 1000 }, $set: { b: 30 } }, {}, function (err) { - var data = d.getAllData() - , doc1 = _.find(data, function (doc) { return doc._id === _doc1._id; }) - , doc2 = _.find(data, function (doc) { return doc._id === _doc2._id; }) - , doc3 = _.find(data, function (doc) { return doc._id === _doc3._id; }) - ; + const data = d.getAllData() + const doc1 = _.find(data, function (doc) { return doc._id === _doc1._id }) + const doc2 = _.find(data, function (doc) { return doc._id === _doc2._id }) + const doc3 = _.find(data, function (doc) { return doc._id === _doc3._id }) - err.errorType.should.equal('uniqueViolated'); + err.errorType.should.equal('uniqueViolated') // Data left unchanged - data.length.should.equal(3); - assert.deepEqual(doc1, { a: 1, b: 10, c: 100, _id: _doc1._id }); - assert.deepEqual(doc2, { a: 2, b: 20, c: 200, _id: _doc2._id }); - assert.deepEqual(doc3, { a: 3, b: 30, c: 300, _id: _doc3._id }); + data.length.should.equal(3) + assert.deepEqual(doc1, { a: 1, b: 10, c: 100, _id: _doc1._id }) + assert.deepEqual(doc2, { a: 2, b: 20, c: 200, _id: _doc2._id }) + assert.deepEqual(doc3, { a: 3, b: 30, c: 300, _id: _doc3._id }) // All indexes left unchanged and pointing to the same docs - d.indexes.a.tree.getNumberOfKeys().should.equal(3); - d.indexes.a.getMatching(1)[0].should.equal(doc1); - d.indexes.a.getMatching(2)[0].should.equal(doc2); - d.indexes.a.getMatching(3)[0].should.equal(doc3); - - d.indexes.b.tree.getNumberOfKeys().should.equal(3); - d.indexes.b.getMatching(10)[0].should.equal(doc1); - d.indexes.b.getMatching(20)[0].should.equal(doc2); - d.indexes.b.getMatching(30)[0].should.equal(doc3); - - d.indexes.c.tree.getNumberOfKeys().should.equal(3); - d.indexes.c.getMatching(100)[0].should.equal(doc1); - d.indexes.c.getMatching(200)[0].should.equal(doc2); - d.indexes.c.getMatching(300)[0].should.equal(doc3); - - done(); - }); - }); - }); - }); - }); + d.indexes.a.tree.getNumberOfKeys().should.equal(3) + d.indexes.a.getMatching(1)[0].should.equal(doc1) + d.indexes.a.getMatching(2)[0].should.equal(doc2) + d.indexes.a.getMatching(3)[0].should.equal(doc3) + + d.indexes.b.tree.getNumberOfKeys().should.equal(3) + d.indexes.b.getMatching(10)[0].should.equal(doc1) + d.indexes.b.getMatching(20)[0].should.equal(doc2) + d.indexes.b.getMatching(30)[0].should.equal(doc3) + + d.indexes.c.tree.getNumberOfKeys().should.equal(3) + d.indexes.c.getMatching(100)[0].should.equal(doc1) + d.indexes.c.getMatching(200)[0].should.equal(doc2) + d.indexes.c.getMatching(300)[0].should.equal(doc3) + + done() + }) + }) + }) + }) + }) it('If a multi update violates a contraint, all changes are rolled back and an error is thrown', function (done) { - d.ensureIndex({ fieldName: 'a', unique: true }); - d.ensureIndex({ fieldName: 'b', unique: true }); - d.ensureIndex({ fieldName: 'c', unique: true }); + d.ensureIndex({ fieldName: 'a', unique: true }) + d.ensureIndex({ fieldName: 'b', unique: true }) + d.ensureIndex({ fieldName: 'c', unique: true }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 1, b: 10, c: 100 }, function (err, _doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, b: 20, c: 200 }, function (err, _doc2) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 3, b: 30, c: 300 }, function (err, _doc3) { // Will conflict with doc3 - d.update({ a: { $in: [1, 2] } }, { $inc: { a: 10, c: 1000 }, $set: { b: 30 } }, { multi: true }, function (err) { - var data = d.getAllData() - , doc1 = _.find(data, function (doc) { return doc._id === _doc1._id; }) - , doc2 = _.find(data, function (doc) { return doc._id === _doc2._id; }) - , doc3 = _.find(data, function (doc) { return doc._id === _doc3._id; }) - ; + d.update({ a: { $in: [1, 2] } }, { + $inc: { a: 10, c: 1000 }, + $set: { b: 30 } + }, { multi: true }, function (err) { + const data = d.getAllData() + const doc1 = _.find(data, function (doc) { return doc._id === _doc1._id }) + const doc2 = _.find(data, function (doc) { return doc._id === _doc2._id }) + const doc3 = _.find(data, function (doc) { return doc._id === _doc3._id }) - err.errorType.should.equal('uniqueViolated'); + err.errorType.should.equal('uniqueViolated') // Data left unchanged - data.length.should.equal(3); - assert.deepEqual(doc1, { a: 1, b: 10, c: 100, _id: _doc1._id }); - assert.deepEqual(doc2, { a: 2, b: 20, c: 200, _id: _doc2._id }); - assert.deepEqual(doc3, { a: 3, b: 30, c: 300, _id: _doc3._id }); + data.length.should.equal(3) + assert.deepEqual(doc1, { a: 1, b: 10, c: 100, _id: _doc1._id }) + assert.deepEqual(doc2, { a: 2, b: 20, c: 200, _id: _doc2._id }) + assert.deepEqual(doc3, { a: 3, b: 30, c: 300, _id: _doc3._id }) // All indexes left unchanged and pointing to the same docs - d.indexes.a.tree.getNumberOfKeys().should.equal(3); - d.indexes.a.getMatching(1)[0].should.equal(doc1); - d.indexes.a.getMatching(2)[0].should.equal(doc2); - d.indexes.a.getMatching(3)[0].should.equal(doc3); - - d.indexes.b.tree.getNumberOfKeys().should.equal(3); - d.indexes.b.getMatching(10)[0].should.equal(doc1); - d.indexes.b.getMatching(20)[0].should.equal(doc2); - d.indexes.b.getMatching(30)[0].should.equal(doc3); - - d.indexes.c.tree.getNumberOfKeys().should.equal(3); - d.indexes.c.getMatching(100)[0].should.equal(doc1); - d.indexes.c.getMatching(200)[0].should.equal(doc2); - d.indexes.c.getMatching(300)[0].should.equal(doc3); - - done(); - }); - }); - }); - }); - }); - - }); // ==== End of 'Updating indexes upon document update' ==== // + d.indexes.a.tree.getNumberOfKeys().should.equal(3) + d.indexes.a.getMatching(1)[0].should.equal(doc1) + d.indexes.a.getMatching(2)[0].should.equal(doc2) + d.indexes.a.getMatching(3)[0].should.equal(doc3) + + d.indexes.b.tree.getNumberOfKeys().should.equal(3) + d.indexes.b.getMatching(10)[0].should.equal(doc1) + d.indexes.b.getMatching(20)[0].should.equal(doc2) + d.indexes.b.getMatching(30)[0].should.equal(doc3) + + d.indexes.c.tree.getNumberOfKeys().should.equal(3) + d.indexes.c.getMatching(100)[0].should.equal(doc1) + d.indexes.c.getMatching(200)[0].should.equal(doc2) + d.indexes.c.getMatching(300)[0].should.equal(doc3) + + done() + }) + }) + }) + }) + }) + }) // ==== End of 'Updating indexes upon document update' ==== // describe('Updating indexes upon document remove', function () { - it('Removing docs still works as before with indexing', function (done) { - d.ensureIndex({ fieldName: 'a' }); + d.ensureIndex({ fieldName: 'a' }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 1, b: 'hello' }, function (err, _doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, b: 'si' }, function (err, _doc2) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 3, b: 'coin' }, function (err, _doc3) { d.remove({ a: 1 }, {}, function (err, nr) { - var data = d.getAllData() - , doc2 = _.find(data, function (doc) { return doc._id === _doc2._id; }) - , doc3 = _.find(data, function (doc) { return doc._id === _doc3._id; }) - ; + const data = d.getAllData() + const doc2 = _.find(data, function (doc) { return doc._id === _doc2._id }) + const doc3 = _.find(data, function (doc) { return doc._id === _doc3._id }) - assert.isNull(err); - nr.should.equal(1); + assert.isNull(err) + nr.should.equal(1) - data.length.should.equal(2); - assert.deepEqual(doc2, { a: 2, b: 'si', _id: _doc2._id }); - assert.deepEqual(doc3, { a: 3, b: 'coin', _id: _doc3._id }); + data.length.should.equal(2) + assert.deepEqual(doc2, { a: 2, b: 'si', _id: _doc2._id }) + assert.deepEqual(doc3, { a: 3, b: 'coin', _id: _doc3._id }) d.remove({ a: { $in: [2, 3] } }, { multi: true }, function (err, nr) { - var data = d.getAllData() - ; + const data = d.getAllData() - assert.isNull(err); - nr.should.equal(2); - data.length.should.equal(0); + assert.isNull(err) + nr.should.equal(2) + data.length.should.equal(0) - done(); - }); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) + }) it('Indexes get updated when a document (or multiple documents) is removed', function (done) { - d.ensureIndex({ fieldName: 'a' }); - d.ensureIndex({ fieldName: 'b' }); + d.ensureIndex({ fieldName: 'a' }) + d.ensureIndex({ fieldName: 'b' }) + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 1, b: 'hello' }, function (err, doc1) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 2, b: 'si' }, function (err, doc2) { + // eslint-disable-next-line node/handle-callback-err d.insert({ a: 3, b: 'coin' }, function (err, doc3) { // Simple remove d.remove({ a: 1 }, {}, function (err, nr) { - assert.isNull(err); - nr.should.equal(1); + assert.isNull(err) + nr.should.equal(1) - d.indexes.a.tree.getNumberOfKeys().should.equal(2); - d.indexes.a.getMatching(2)[0]._id.should.equal(doc2._id); - d.indexes.a.getMatching(3)[0]._id.should.equal(doc3._id); + d.indexes.a.tree.getNumberOfKeys().should.equal(2) + d.indexes.a.getMatching(2)[0]._id.should.equal(doc2._id) + d.indexes.a.getMatching(3)[0]._id.should.equal(doc3._id) - d.indexes.b.tree.getNumberOfKeys().should.equal(2); - d.indexes.b.getMatching('si')[0]._id.should.equal(doc2._id); - d.indexes.b.getMatching('coin')[0]._id.should.equal(doc3._id); + d.indexes.b.tree.getNumberOfKeys().should.equal(2) + d.indexes.b.getMatching('si')[0]._id.should.equal(doc2._id) + d.indexes.b.getMatching('coin')[0]._id.should.equal(doc3._id) // The same pointers are shared between all indexes - d.indexes.a.tree.getNumberOfKeys().should.equal(2); - d.indexes.b.tree.getNumberOfKeys().should.equal(2); - d.indexes._id.tree.getNumberOfKeys().should.equal(2); - d.indexes.a.getMatching(2)[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]); - d.indexes.b.getMatching('si')[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]); - d.indexes.a.getMatching(3)[0].should.equal(d.indexes._id.getMatching(doc3._id)[0]); - d.indexes.b.getMatching('coin')[0].should.equal(d.indexes._id.getMatching(doc3._id)[0]); + d.indexes.a.tree.getNumberOfKeys().should.equal(2) + d.indexes.b.tree.getNumberOfKeys().should.equal(2) + d.indexes._id.tree.getNumberOfKeys().should.equal(2) + d.indexes.a.getMatching(2)[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]) + d.indexes.b.getMatching('si')[0].should.equal(d.indexes._id.getMatching(doc2._id)[0]) + d.indexes.a.getMatching(3)[0].should.equal(d.indexes._id.getMatching(doc3._id)[0]) + d.indexes.b.getMatching('coin')[0].should.equal(d.indexes._id.getMatching(doc3._id)[0]) // Multi remove d.remove({}, { multi: true }, function (err, nr) { - assert.isNull(err); - nr.should.equal(2); + assert.isNull(err) + nr.should.equal(2) - d.indexes.a.tree.getNumberOfKeys().should.equal(0); - d.indexes.b.tree.getNumberOfKeys().should.equal(0); - d.indexes._id.tree.getNumberOfKeys().should.equal(0); - - done(); - }); - }); - }); - }); - }); - }); - - }); // ==== End of 'Updating indexes upon document remove' ==== // + d.indexes.a.tree.getNumberOfKeys().should.equal(0) + d.indexes.b.tree.getNumberOfKeys().should.equal(0) + d.indexes._id.tree.getNumberOfKeys().should.equal(0) + done() + }) + }) + }) + }) + }) + }) + }) // ==== End of 'Updating indexes upon document remove' ==== // describe('Persisting indexes', function () { - it('Indexes are persisted to a separate file and recreated upon reload', function (done) { - var persDb = "workspace/persistIndexes.db" - , db - ; + const persDb = 'workspace/persistIndexes.db' + let db - if (fs.existsSync(persDb)) { fs.writeFileSync(persDb, '', 'utf8'); } - db = new Datastore({ filename: persDb, autoload: true }); + if (fs.existsSync(persDb)) { fs.writeFileSync(persDb, '', 'utf8') } + db = new Datastore({ filename: persDb, autoload: true }) - Object.keys(db.indexes).length.should.equal(1); - Object.keys(db.indexes)[0].should.equal("_id"); + Object.keys(db.indexes).length.should.equal(1) + Object.keys(db.indexes)[0].should.equal('_id') - db.insert({ planet: "Earth" }, function (err) { - assert.isNull(err); - db.insert({ planet: "Mars" }, function (err) { - assert.isNull(err); + db.insert({ planet: 'Earth' }, function (err) { + assert.isNull(err) + db.insert({ planet: 'Mars' }, function (err) { + assert.isNull(err) - db.ensureIndex({ fieldName: "planet" }, function (err) { - Object.keys(db.indexes).length.should.equal(2); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("planet"); - db.indexes._id.getAll().length.should.equal(2); - db.indexes.planet.getAll().length.should.equal(2); - db.indexes.planet.fieldName.should.equal("planet"); + // eslint-disable-next-line node/handle-callback-err + db.ensureIndex({ fieldName: 'planet' }, function (err) { + Object.keys(db.indexes).length.should.equal(2) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('planet') + db.indexes._id.getAll().length.should.equal(2) + db.indexes.planet.getAll().length.should.equal(2) + db.indexes.planet.fieldName.should.equal('planet') // After a reload the indexes are recreated - db = new Datastore({ filename: persDb }); + db = new Datastore({ filename: persDb }) db.loadDatabase(function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(2); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("planet"); - db.indexes._id.getAll().length.should.equal(2); - db.indexes.planet.getAll().length.should.equal(2); - db.indexes.planet.fieldName.should.equal("planet"); + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(2) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('planet') + db.indexes._id.getAll().length.should.equal(2) + db.indexes.planet.getAll().length.should.equal(2) + db.indexes.planet.fieldName.should.equal('planet') // After another reload the indexes are still there (i.e. they are preserved during autocompaction) - db = new Datastore({ filename: persDb }); + db = new Datastore({ filename: persDb }) db.loadDatabase(function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(2); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("planet"); - db.indexes._id.getAll().length.should.equal(2); - db.indexes.planet.getAll().length.should.equal(2); - db.indexes.planet.fieldName.should.equal("planet"); - - done(); - }); - }); - }); - }); - }); - }); + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(2) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('planet') + db.indexes._id.getAll().length.should.equal(2) + db.indexes.planet.getAll().length.should.equal(2) + db.indexes.planet.fieldName.should.equal('planet') + + done() + }) + }) + }) + }) + }) + }) it('Indexes are persisted with their options and recreated even if some db operation happen between loads', function (done) { - var persDb = "workspace/persistIndexes.db" - , db - ; + const persDb = 'workspace/persistIndexes.db' + let db - if (fs.existsSync(persDb)) { fs.writeFileSync(persDb, '', 'utf8'); } - db = new Datastore({ filename: persDb, autoload: true }); + if (fs.existsSync(persDb)) { fs.writeFileSync(persDb, '', 'utf8') } + db = new Datastore({ filename: persDb, autoload: true }) - Object.keys(db.indexes).length.should.equal(1); - Object.keys(db.indexes)[0].should.equal("_id"); + Object.keys(db.indexes).length.should.equal(1) + Object.keys(db.indexes)[0].should.equal('_id') - db.insert({ planet: "Earth" }, function (err) { - assert.isNull(err); - db.insert({ planet: "Mars" }, function (err) { - assert.isNull(err); + db.insert({ planet: 'Earth' }, function (err) { + assert.isNull(err) + db.insert({ planet: 'Mars' }, function (err) { + assert.isNull(err) - db.ensureIndex({ fieldName: "planet", unique: true, sparse: false }, function (err) { - Object.keys(db.indexes).length.should.equal(2); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("planet"); - db.indexes._id.getAll().length.should.equal(2); - db.indexes.planet.getAll().length.should.equal(2); - db.indexes.planet.unique.should.equal(true); - db.indexes.planet.sparse.should.equal(false); + // eslint-disable-next-line node/handle-callback-err + db.ensureIndex({ fieldName: 'planet', unique: true, sparse: false }, function (err) { + Object.keys(db.indexes).length.should.equal(2) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('planet') + db.indexes._id.getAll().length.should.equal(2) + db.indexes.planet.getAll().length.should.equal(2) + db.indexes.planet.unique.should.equal(true) + db.indexes.planet.sparse.should.equal(false) - db.insert({ planet: "Jupiter" }, function (err) { - assert.isNull(err); + db.insert({ planet: 'Jupiter' }, function (err) { + assert.isNull(err) // After a reload the indexes are recreated - db = new Datastore({ filename: persDb }); + db = new Datastore({ filename: persDb }) db.loadDatabase(function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(2); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("planet"); - db.indexes._id.getAll().length.should.equal(3); - db.indexes.planet.getAll().length.should.equal(3); - db.indexes.planet.unique.should.equal(true); - db.indexes.planet.sparse.should.equal(false); + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(2) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('planet') + db.indexes._id.getAll().length.should.equal(3) + db.indexes.planet.getAll().length.should.equal(3) + db.indexes.planet.unique.should.equal(true) + db.indexes.planet.sparse.should.equal(false) db.ensureIndex({ fieldName: 'bloup', unique: false, sparse: true }, function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(3); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("planet"); - Object.keys(db.indexes)[2].should.equal("bloup"); - db.indexes._id.getAll().length.should.equal(3); - db.indexes.planet.getAll().length.should.equal(3); - db.indexes.bloup.getAll().length.should.equal(0); - db.indexes.planet.unique.should.equal(true); - db.indexes.planet.sparse.should.equal(false); - db.indexes.bloup.unique.should.equal(false); - db.indexes.bloup.sparse.should.equal(true); + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(3) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('planet') + Object.keys(db.indexes)[2].should.equal('bloup') + db.indexes._id.getAll().length.should.equal(3) + db.indexes.planet.getAll().length.should.equal(3) + db.indexes.bloup.getAll().length.should.equal(0) + db.indexes.planet.unique.should.equal(true) + db.indexes.planet.sparse.should.equal(false) + db.indexes.bloup.unique.should.equal(false) + db.indexes.bloup.sparse.should.equal(true) // After another reload the indexes are still there (i.e. they are preserved during autocompaction) - db = new Datastore({ filename: persDb }); + db = new Datastore({ filename: persDb }) db.loadDatabase(function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(3); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("planet"); - Object.keys(db.indexes)[2].should.equal("bloup"); - db.indexes._id.getAll().length.should.equal(3); - db.indexes.planet.getAll().length.should.equal(3); - db.indexes.bloup.getAll().length.should.equal(0); - db.indexes.planet.unique.should.equal(true); - db.indexes.planet.sparse.should.equal(false); - db.indexes.bloup.unique.should.equal(false); - db.indexes.bloup.sparse.should.equal(true); - - done(); - }); - }); - }); - }); - }); - }); - }); - }); + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(3) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('planet') + Object.keys(db.indexes)[2].should.equal('bloup') + db.indexes._id.getAll().length.should.equal(3) + db.indexes.planet.getAll().length.should.equal(3) + db.indexes.bloup.getAll().length.should.equal(0) + db.indexes.planet.unique.should.equal(true) + db.indexes.planet.sparse.should.equal(false) + db.indexes.bloup.unique.should.equal(false) + db.indexes.bloup.sparse.should.equal(true) + + done() + }) + }) + }) + }) + }) + }) + }) + }) it('Indexes can also be removed and the remove persisted', function (done) { - var persDb = "workspace/persistIndexes.db" - , db - ; - - if (fs.existsSync(persDb)) { fs.writeFileSync(persDb, '', 'utf8'); } - db = new Datastore({ filename: persDb, autoload: true }); - - Object.keys(db.indexes).length.should.equal(1); - Object.keys(db.indexes)[0].should.equal("_id"); - - db.insert({ planet: "Earth" }, function (err) { - assert.isNull(err); - db.insert({ planet: "Mars" }, function (err) { - assert.isNull(err); - - db.ensureIndex({ fieldName: "planet" }, function (err) { - assert.isNull(err); - db.ensureIndex({ fieldName: "another" }, function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(3); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("planet"); - Object.keys(db.indexes)[2].should.equal("another"); - db.indexes._id.getAll().length.should.equal(2); - db.indexes.planet.getAll().length.should.equal(2); - db.indexes.planet.fieldName.should.equal("planet"); + const persDb = 'workspace/persistIndexes.db' + let db + + if (fs.existsSync(persDb)) { fs.writeFileSync(persDb, '', 'utf8') } + db = new Datastore({ filename: persDb, autoload: true }) + + Object.keys(db.indexes).length.should.equal(1) + Object.keys(db.indexes)[0].should.equal('_id') + + db.insert({ planet: 'Earth' }, function (err) { + assert.isNull(err) + db.insert({ planet: 'Mars' }, function (err) { + assert.isNull(err) + + db.ensureIndex({ fieldName: 'planet' }, function (err) { + assert.isNull(err) + db.ensureIndex({ fieldName: 'another' }, function (err) { + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(3) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('planet') + Object.keys(db.indexes)[2].should.equal('another') + db.indexes._id.getAll().length.should.equal(2) + db.indexes.planet.getAll().length.should.equal(2) + db.indexes.planet.fieldName.should.equal('planet') // After a reload the indexes are recreated - db = new Datastore({ filename: persDb }); + db = new Datastore({ filename: persDb }) db.loadDatabase(function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(3); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("planet"); - Object.keys(db.indexes)[2].should.equal("another"); - db.indexes._id.getAll().length.should.equal(2); - db.indexes.planet.getAll().length.should.equal(2); - db.indexes.planet.fieldName.should.equal("planet"); + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(3) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('planet') + Object.keys(db.indexes)[2].should.equal('another') + db.indexes._id.getAll().length.should.equal(2) + db.indexes.planet.getAll().length.should.equal(2) + db.indexes.planet.fieldName.should.equal('planet') // Index is removed - db.removeIndex("planet", function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(2); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("another"); - db.indexes._id.getAll().length.should.equal(2); + db.removeIndex('planet', function (err) { + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(2) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('another') + db.indexes._id.getAll().length.should.equal(2) // After a reload indexes are preserved - db = new Datastore({ filename: persDb }); + db = new Datastore({ filename: persDb }) db.loadDatabase(function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(2); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("another"); - db.indexes._id.getAll().length.should.equal(2); + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(2) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('another') + db.indexes._id.getAll().length.should.equal(2) // After another reload the indexes are still there (i.e. they are preserved during autocompaction) - db = new Datastore({ filename: persDb }); + db = new Datastore({ filename: persDb }) db.loadDatabase(function (err) { - assert.isNull(err); - Object.keys(db.indexes).length.should.equal(2); - Object.keys(db.indexes)[0].should.equal("_id"); - Object.keys(db.indexes)[1].should.equal("another"); - db.indexes._id.getAll().length.should.equal(2); - - done(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - - }); // ==== End of 'Persisting indexes' ==== + assert.isNull(err) + Object.keys(db.indexes).length.should.equal(2) + Object.keys(db.indexes)[0].should.equal('_id') + Object.keys(db.indexes)[1].should.equal('another') + db.indexes._id.getAll().length.should.equal(2) + + done() + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) // ==== End of 'Persisting indexes' ==== it('Results of getMatching should never contain duplicates', function (done) { - d.ensureIndex({ fieldName: 'bad' }); + d.ensureIndex({ fieldName: 'bad' }) d.insert({ bad: ['a', 'b'] }, function () { + // eslint-disable-next-line node/handle-callback-err d.getCandidates({ bad: { $in: ['a', 'b'] } }, function (err, res) { - res.length.should.equal(1); - done(); - }); - }); - }); - - }); // ==== End of 'Using indexes' ==== // - - -}); + res.length.should.equal(1) + done() + }) + }) + }) + }) // ==== End of 'Using indexes' ==== // +}) diff --git a/test/executor.test.js b/test/executor.test.js index ffea474..13c6606 100755 --- a/test/executor.test.js +++ b/test/executor.test.js @@ -1,213 +1,215 @@ -var should = require('chai').should() - , assert = require('chai').assert - , testDb = 'workspace/test.db' - , fs = require('fs') - , path = require('path') - , _ = require('underscore') - , async = require('async') - , model = require('../lib/model') - , Datastore = require('../lib/datastore') - , Persistence = require('../lib/persistence') - ; - +/* eslint-env mocha */ +const chai = require('chai') +const testDb = 'workspace/test.db' +const fs = require('fs') +const path = require('path') +const async = require('async') +const Datastore = require('../lib/datastore') +const Persistence = require('../lib/persistence') + +const { assert } = chai +chai.should() // Test that even if a callback throws an exception, the next DB operations will still be executed // 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 function testThrowInCallback (d, done) { - var currentUncaughtExceptionHandlers = process.listeners('uncaughtException'); + const currentUncaughtExceptionHandlers = process.listeners('uncaughtException') - process.removeAllListeners('uncaughtException'); + process.removeAllListeners('uncaughtException') + // eslint-disable-next-line node/handle-callback-err process.on('uncaughtException', function (err) { // Do nothing with the error which is only there to test we stay on track - }); + }) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err) { process.nextTick(function () { + // eslint-disable-next-line node/handle-callback-err d.insert({ bar: 1 }, function (err) { - process.removeAllListeners('uncaughtException'); - for (var i = 0; i < currentUncaughtExceptionHandlers.length; i += 1) { - process.on('uncaughtException', currentUncaughtExceptionHandlers[i]); + process.removeAllListeners('uncaughtException') + for (let i = 0; i < currentUncaughtExceptionHandlers.length; i += 1) { + process.on('uncaughtException', currentUncaughtExceptionHandlers[i]) } - done(); - }); - }); + done() + }) + }) - throw new Error('Some error'); - }); + throw new Error('Some error') + }) } // Test that if the callback is falsy, the next DB operations will still be executed function testFalsyCallback (d, done) { - d.insert({ a: 1 }, null); + d.insert({ a: 1 }, null) process.nextTick(function () { - d.update({ a: 1 }, { a: 2 }, {}, null); + d.update({ a: 1 }, { a: 2 }, {}, null) process.nextTick(function () { - d.update({ a: 2 }, { a: 1 }, null); + d.update({ a: 2 }, { a: 1 }, null) process.nextTick(function () { - d.remove({ a: 2 }, {}, null); + d.remove({ a: 2 }, {}, null) process.nextTick(function () { - d.remove({ a: 2 }, null); + d.remove({ a: 2 }, null) process.nextTick(function () { - d.find({}, done); - }); - }); - }); - }); - }); + d.find({}, done) + }) + }) + }) + }) + }) } // Test that operations are executed in the right order // 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 function testRightOrder (d, done) { - var currentUncaughtExceptionHandlers = process.listeners('uncaughtException'); + const currentUncaughtExceptionHandlers = process.listeners('uncaughtException') - process.removeAllListeners('uncaughtException'); + process.removeAllListeners('uncaughtException') + // eslint-disable-next-line node/handle-callback-err process.on('uncaughtException', function (err) { // Do nothing with the error which is only there to test we stay on track - }); + }) + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(0); + docs.length.should.equal(0) d.insert({ a: 1 }, function () { d.update({ a: 1 }, { a: 2 }, {}, function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs[0].a.should.equal(2); + docs[0].a.should.equal(2) process.nextTick(function () { d.update({ a: 2 }, { a: 3 }, {}, function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs[0].a.should.equal(3); + docs[0].a.should.equal(3) - process.removeAllListeners('uncaughtException'); - for (var i = 0; i < currentUncaughtExceptionHandlers.length; i += 1) { - process.on('uncaughtException', currentUncaughtExceptionHandlers[i]); + process.removeAllListeners('uncaughtException') + for (let i = 0; i < currentUncaughtExceptionHandlers.length; i += 1) { + process.on('uncaughtException', currentUncaughtExceptionHandlers[i]) } - done(); - }); - }); - }); + done() + }) + }) + }) - throw new Error('Some error'); - }); - }); - }); - }); + throw new Error('Some error') + }) + }) + }) + }) } // Note: The following test does not have any assertion because it // is meant to address the deprecation warning: // (node) warning: Recursive process.nextTick detected. This will break in the next version of node. Please use setImmediate for recursive deferral. // see -var testEventLoopStarvation = function(d, done){ - var times = 1001; - var i = 0; - while ( i b ? -1 : 1; }).should.equal(-1); - }); - - }); // ==== End of 'Comparing things' ==== // - + model.compareThings('hello', 'bloup', function (a, b) { return a < b ? -1 : 1 }).should.equal(1) + model.compareThings('hello', 'bloup', function (a, b) { return a > b ? -1 : 1 }).should.equal(-1) + }) + }) // ==== End of 'Comparing things' ==== // describe('Querying', function () { - describe('Comparing things', function () { - it('Two things of different types cannot be equal, two identical native things are equal', function () { - var toTest = [null, 'somestring', 42, true, new Date(72998322), { hello: 'world' }] - , toTestAgainst = [null, 'somestring', 42, true, new Date(72998322), { hello: 'world' }] // Use another array so that we don't test pointer equality - , i, j - ; + const toTest = [null, 'somestring', 42, true, new Date(72998322), { hello: 'world' }] + const toTestAgainst = [null, 'somestring', 42, true, new Date(72998322), { hello: 'world' }] + let i + let j for (i = 0; i < toTest.length; i += 1) { for (j = 0; j < toTestAgainst.length; j += 1) { - model.areThingsEqual(toTest[i], toTestAgainst[j]).should.equal(i === j); + model.areThingsEqual(toTest[i], toTestAgainst[j]).should.equal(i === j) } } - }); + }) it('Can test native types null undefined string number boolean date equality', function () { - var toTest = [null, undefined, 'somestring', 42, true, new Date(72998322), { hello: 'world' }] - , toTestAgainst = [undefined, null, 'someotherstring', 5, false, new Date(111111), { hello: 'mars' }] - , i - ; + const toTest = [null, undefined, 'somestring', 42, true, new Date(72998322), { hello: 'world' }] + const toTestAgainst = [undefined, null, 'someotherstring', 5, false, new Date(111111), { hello: 'mars' }] + let i for (i = 0; i < toTest.length; i += 1) { - model.areThingsEqual(toTest[i], toTestAgainst[i]).should.equal(false); + model.areThingsEqual(toTest[i], toTestAgainst[i]).should.equal(false) } - }); + }) it('If one side is an array or undefined, comparison fails', function () { - var toTestAgainst = [null, undefined, 'somestring', 42, true, new Date(72998322), { hello: 'world' }] - , i - ; + const toTestAgainst = [null, undefined, 'somestring', 42, true, new Date(72998322), { hello: 'world' }] + let i for (i = 0; i < toTestAgainst.length; i += 1) { - model.areThingsEqual([1, 2, 3], toTestAgainst[i]).should.equal(false); - model.areThingsEqual(toTestAgainst[i], []).should.equal(false); + model.areThingsEqual([1, 2, 3], toTestAgainst[i]).should.equal(false) + model.areThingsEqual(toTestAgainst[i], []).should.equal(false) - model.areThingsEqual(undefined, toTestAgainst[i]).should.equal(false); - model.areThingsEqual(toTestAgainst[i], undefined).should.equal(false); + model.areThingsEqual(undefined, toTestAgainst[i]).should.equal(false) + model.areThingsEqual(toTestAgainst[i], undefined).should.equal(false) } - }); + }) it('Can test objects equality', function () { - model.areThingsEqual({ hello: 'world' }, {}).should.equal(false); - model.areThingsEqual({ hello: 'world' }, { hello: 'mars' }).should.equal(false); - model.areThingsEqual({ hello: 'world' }, { hello: 'world', temperature: 42 }).should.equal(false); - model.areThingsEqual({ hello: 'world', other: { temperature: 42 }}, { hello: 'world', other: { temperature: 42 }}).should.equal(true); - }); - - }); - + model.areThingsEqual({ hello: 'world' }, {}).should.equal(false) + model.areThingsEqual({ hello: 'world' }, { hello: 'mars' }).should.equal(false) + model.areThingsEqual({ hello: 'world' }, { hello: 'world', temperature: 42 }).should.equal(false) + model.areThingsEqual({ hello: 'world', other: { temperature: 42 } }, { + hello: 'world', + other: { temperature: 42 } + }).should.equal(true) + }) + }) describe('Getting a fields value in dot notation', function () { - it('Return first-level and nested values', function () { - model.getDotValue({ hello: 'world' }, 'hello').should.equal('world'); - model.getDotValue({ hello: 'world', type: { planet: true, blue: true } }, 'type.planet').should.equal(true); - }); + model.getDotValue({ hello: 'world' }, 'hello').should.equal('world') + model.getDotValue({ hello: 'world', type: { planet: true, blue: true } }, 'type.planet').should.equal(true) + }) it('Return undefined if the field cannot be found in the object', function () { - assert.isUndefined(model.getDotValue({ hello: 'world' }, 'helloo')); - assert.isUndefined(model.getDotValue({ hello: 'world', type: { planet: true } }, 'type.plane')); - }); - - it("Can navigate inside arrays with dot notation, and return the array of values in that case", function () { - var dv; - + assert.isUndefined(model.getDotValue({ hello: 'world' }, 'helloo')) + assert.isUndefined(model.getDotValue({ hello: 'world', type: { planet: true } }, 'type.plane')) + }) + + it('Can navigate inside arrays with dot notation, and return the array of values in that case', function () { + let dv + // Simple array of subdocuments - dv = model.getDotValue({ planets: [ { name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 } ] }, 'planets.name'); - assert.deepEqual(dv, ['Earth', 'Mars', 'Pluton']); - + dv = model.getDotValue({ + planets: [{ name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { + name: 'Pluton', + number: 9 + }] + }, 'planets.name') + assert.deepEqual(dv, ['Earth', 'Mars', 'Pluton']) + // Nested array of subdocuments - dv = model.getDotValue({ nedb: true, data: { planets: [ { name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 } ] } }, 'data.planets.number'); - assert.deepEqual(dv, [3, 2, 9]); - + dv = model.getDotValue({ + nedb: true, + data: { planets: [{ name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 }] } + }, 'data.planets.number') + assert.deepEqual(dv, [3, 2, 9]) + // Nested array in a subdocument of an array (yay, inception!) // TODO: make sure MongoDB doesn't flatten the array (it wouldn't make sense) - dv = model.getDotValue({ nedb: true, data: { planets: [ { name: 'Earth', numbers: [ 1, 3 ] }, { name: 'Mars', numbers: [ 7 ] }, { name: 'Pluton', numbers: [ 9, 5, 1 ] } ] } }, 'data.planets.numbers'); - assert.deepEqual(dv, [[ 1, 3 ], [ 7 ], [ 9, 5, 1 ]]); - }); - - it("Can get a single value out of an array using its index", function () { - var dv; - + dv = model.getDotValue({ + nedb: true, + data: { + planets: [{ name: 'Earth', numbers: [1, 3] }, { name: 'Mars', numbers: [7] }, { + name: 'Pluton', + numbers: [9, 5, 1] + }] + } + }, 'data.planets.numbers') + assert.deepEqual(dv, [[1, 3], [7], [9, 5, 1]]) + }) + + it('Can get a single value out of an array using its index', function () { + let dv + // Simple index in dot notation - dv = model.getDotValue({ planets: [ { name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 } ] }, 'planets.1'); - assert.deepEqual(dv, { name: 'Mars', number: 2 }); + dv = model.getDotValue({ + planets: [{ name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { + name: 'Pluton', + number: 9 + }] + }, 'planets.1') + assert.deepEqual(dv, { name: 'Mars', number: 2 }) // Out of bounds index - dv = model.getDotValue({ planets: [ { name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 } ] }, 'planets.3'); - assert.isUndefined(dv); + dv = model.getDotValue({ + planets: [{ name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { + name: 'Pluton', + number: 9 + }] + }, 'planets.3') + assert.isUndefined(dv) // Index in nested array - dv = model.getDotValue({ nedb: true, data: { planets: [ { name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 } ] } }, 'data.planets.2'); - assert.deepEqual(dv, { name: 'Pluton', number: 9 }); - - // Dot notation with index in the middle - dv = model.getDotValue({ nedb: true, data: { planets: [ { name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 } ] } }, 'data.planets.0.name'); - dv.should.equal('Earth'); - }); - - }); + dv = model.getDotValue({ + nedb: true, + data: { planets: [{ name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 }] } + }, 'data.planets.2') + assert.deepEqual(dv, { name: 'Pluton', number: 9 }) + // Dot notation with index in the middle + dv = model.getDotValue({ + nedb: true, + data: { planets: [{ name: 'Earth', number: 3 }, { name: 'Mars', number: 2 }, { name: 'Pluton', number: 9 }] } + }, 'data.planets.0.name') + dv.should.equal('Earth') + }) + }) describe('Field equality', function () { - it('Can find documents with simple fields', function () { - model.match({ test: 'yeah' }, { test: 'yea' }).should.equal(false); - model.match({ test: 'yeah' }, { test: 'yeahh' }).should.equal(false); - model.match({ test: 'yeah' }, { test: 'yeah' }).should.equal(true); - }); + model.match({ test: 'yeah' }, { test: 'yea' }).should.equal(false) + model.match({ test: 'yeah' }, { test: 'yeahh' }).should.equal(false) + model.match({ test: 'yeah' }, { test: 'yeah' }).should.equal(true) + }) it('Can find documents with the dot-notation', function () { - model.match({ test: { ooo: 'yeah' } }, { "test.ooo": 'yea' }).should.equal(false); - model.match({ test: { ooo: 'yeah' } }, { "test.oo": 'yeah' }).should.equal(false); - model.match({ test: { ooo: 'yeah' } }, { "tst.ooo": 'yeah' }).should.equal(false); - model.match({ test: { ooo: 'yeah' } }, { "test.ooo": 'yeah' }).should.equal(true); - }); + model.match({ test: { ooo: 'yeah' } }, { 'test.ooo': 'yea' }).should.equal(false) + model.match({ test: { ooo: 'yeah' } }, { 'test.oo': 'yeah' }).should.equal(false) + model.match({ test: { ooo: 'yeah' } }, { 'tst.ooo': 'yeah' }).should.equal(false) + model.match({ test: { ooo: 'yeah' } }, { 'test.ooo': 'yeah' }).should.equal(true) + }) it('Cannot find undefined', function () { - model.match({ test: undefined }, { test: undefined }).should.equal(false); - model.match({ test: { pp: undefined } }, { "test.pp": undefined }).should.equal(false); - }); + model.match({ test: undefined }, { test: undefined }).should.equal(false) + model.match({ test: { pp: undefined } }, { 'test.pp': undefined }).should.equal(false) + }) it('Nested objects are deep-equality matched and not treated as sub-queries', function () { - model.match({ a: { b: 5 } }, { a: { b: 5 } }).should.equal(true); - model.match({ a: { b: 5, c: 3 } }, { a: { b: 5 } }).should.equal(false); + model.match({ a: { b: 5 } }, { a: { b: 5 } }).should.equal(true) + model.match({ a: { b: 5, c: 3 } }, { a: { b: 5 } }).should.equal(false) model.match({ a: { b: 5 } }, { a: { b: { $lt: 10 } } }).should.equal(false); - (function () { model.match({ a: { b: 5 } }, { a: { $or: [ { b: 10 }, { b: 5 } ] } }) }).should.throw(); - }); - - it("Can match for field equality inside an array with the dot notation", function () { - model.match({ a: true, b: [ 'node', 'embedded', 'database' ] }, { 'b.1': 'node' }).should.equal(false); - model.match({ a: true, b: [ 'node', 'embedded', 'database' ] }, { 'b.1': 'embedded' }).should.equal(true); - model.match({ a: true, b: [ 'node', 'embedded', 'database' ] }, { 'b.1': 'database' }).should.equal(false); + (function () { model.match({ a: { b: 5 } }, { a: { $or: [{ b: 10 }, { b: 5 }] } }) }).should.throw() }) - }); - + it('Can match for field equality inside an array with the dot notation', function () { + model.match({ a: true, b: ['node', 'embedded', 'database'] }, { 'b.1': 'node' }).should.equal(false) + model.match({ a: true, b: ['node', 'embedded', 'database'] }, { 'b.1': 'embedded' }).should.equal(true) + model.match({ a: true, b: ['node', 'embedded', 'database'] }, { 'b.1': 'database' }).should.equal(false) + }) + }) describe('Regular expression matching', function () { - it('Matching a non-string to a regular expression always yields false', function () { - var d = new Date() - , r = new RegExp(d.getTime()); + const d = new Date() + const r = new RegExp(d.getTime()) - model.match({ test: true }, { test: /true/ }).should.equal(false); - model.match({ test: null }, { test: /null/ }).should.equal(false); - model.match({ test: 42 }, { test: /42/ }).should.equal(false); - model.match({ test: d }, { test: r }).should.equal(false); - }); + model.match({ test: true }, { test: /true/ }).should.equal(false) + model.match({ test: null }, { test: /null/ }).should.equal(false) + model.match({ test: 42 }, { test: /42/ }).should.equal(false) + model.match({ test: d }, { test: r }).should.equal(false) + }) it('Can match strings using basic querying', function () { - model.match({ test: 'true' }, { test: /true/ }).should.equal(true); - model.match({ test: 'babaaaar' }, { test: /aba+r/ }).should.equal(true); - model.match({ test: 'babaaaar' }, { test: /^aba+r/ }).should.equal(false); - model.match({ test: 'true' }, { test: /t[ru]e/ }).should.equal(false); - }); + model.match({ test: 'true' }, { test: /true/ }).should.equal(true) + model.match({ test: 'babaaaar' }, { test: /aba+r/ }).should.equal(true) + model.match({ test: 'babaaaar' }, { test: /^aba+r/ }).should.equal(false) + model.match({ test: 'true' }, { test: /t[ru]e/ }).should.equal(false) + }) it('Can match strings using the $regex operator', function () { - model.match({ test: 'true' }, { test: { $regex: /true/ } }).should.equal(true); - model.match({ test: 'babaaaar' }, { test: { $regex: /aba+r/ } }).should.equal(true); - model.match({ test: 'babaaaar' }, { test: { $regex: /^aba+r/ } }).should.equal(false); - model.match({ test: 'true' }, { test: { $regex: /t[ru]e/ } }).should.equal(false); - }); + model.match({ test: 'true' }, { test: { $regex: /true/ } }).should.equal(true) + model.match({ test: 'babaaaar' }, { test: { $regex: /aba+r/ } }).should.equal(true) + model.match({ test: 'babaaaar' }, { test: { $regex: /^aba+r/ } }).should.equal(false) + model.match({ test: 'true' }, { test: { $regex: /t[ru]e/ } }).should.equal(false) + }) it('Will throw if $regex operator is used with a non regex value', function () { (function () { @@ -1125,351 +1115,583 @@ describe('Model', function () { (function () { model.match({ test: 'true' }, { test: { $regex: 'true' } }) - }).should.throw(); - }); + }).should.throw() + }) it('Can use the $regex operator in cunjunction with other operators', function () { - model.match({ test: 'helLo' }, { test: { $regex: /ll/i, $nin: ['helL', 'helLop'] } }).should.equal(true); - model.match({ test: 'helLo' }, { test: { $regex: /ll/i, $nin: ['helLo', 'helLop'] } }).should.equal(false); - }); + model.match({ test: 'helLo' }, { test: { $regex: /ll/i, $nin: ['helL', 'helLop'] } }).should.equal(true) + model.match({ test: 'helLo' }, { test: { $regex: /ll/i, $nin: ['helLo', 'helLop'] } }).should.equal(false) + }) it('Can use dot-notation', function () { - model.match({ test: { nested: 'true' } }, { 'test.nested': /true/ }).should.equal(true); - model.match({ test: { nested: 'babaaaar' } }, { 'test.nested': /^aba+r/ }).should.equal(false); - - model.match({ test: { nested: 'true' } }, { 'test.nested': { $regex: /true/ } }).should.equal(true); - model.match({ test: { nested: 'babaaaar' } }, { 'test.nested': { $regex: /^aba+r/ } }).should.equal(false); - }); - - }); + model.match({ test: { nested: 'true' } }, { 'test.nested': /true/ }).should.equal(true) + model.match({ test: { nested: 'babaaaar' } }, { 'test.nested': /^aba+r/ }).should.equal(false) + model.match({ test: { nested: 'true' } }, { 'test.nested': { $regex: /true/ } }).should.equal(true) + model.match({ test: { nested: 'babaaaar' } }, { 'test.nested': { $regex: /^aba+r/ } }).should.equal(false) + }) + }) describe('$lt', function () { - it('Cannot compare a field to an object, an array, null or a boolean, it will return false', function () { - model.match({ a: 5 }, { a: { $lt: { a: 6 } } }).should.equal(false); - model.match({ a: 5 }, { a: { $lt: [6, 7] } }).should.equal(false); - model.match({ a: 5 }, { a: { $lt: null } }).should.equal(false); - model.match({ a: 5 }, { a: { $lt: true } }).should.equal(false); - }); + model.match({ a: 5 }, { a: { $lt: { a: 6 } } }).should.equal(false) + model.match({ a: 5 }, { a: { $lt: [6, 7] } }).should.equal(false) + model.match({ a: 5 }, { a: { $lt: null } }).should.equal(false) + model.match({ a: 5 }, { a: { $lt: true } }).should.equal(false) + }) it('Can compare numbers, with or without dot notation', function () { - model.match({ a: 5 }, { a: { $lt: 6 } }).should.equal(true); - model.match({ a: 5 }, { a: { $lt: 5 } }).should.equal(false); - model.match({ a: 5 }, { a: { $lt: 4 } }).should.equal(false); + model.match({ a: 5 }, { a: { $lt: 6 } }).should.equal(true) + model.match({ a: 5 }, { a: { $lt: 5 } }).should.equal(false) + model.match({ a: 5 }, { a: { $lt: 4 } }).should.equal(false) - model.match({ a: { b: 5 } }, { "a.b": { $lt: 6 } }).should.equal(true); - model.match({ a: { b: 5 } }, { "a.b": { $lt: 3 } }).should.equal(false); - }); + model.match({ a: { b: 5 } }, { 'a.b': { $lt: 6 } }).should.equal(true) + model.match({ a: { b: 5 } }, { 'a.b': { $lt: 3 } }).should.equal(false) + }) it('Can compare strings, with or without dot notation', function () { - model.match({ a: "nedb" }, { a: { $lt: "nedc" } }).should.equal(true); - model.match({ a: "nedb" }, { a: { $lt: "neda" } }).should.equal(false); + model.match({ a: 'nedb' }, { a: { $lt: 'nedc' } }).should.equal(true) + model.match({ a: 'nedb' }, { a: { $lt: 'neda' } }).should.equal(false) - model.match({ a: { b: "nedb" } }, { "a.b": { $lt: "nedc" } }).should.equal(true); - model.match({ a: { b: "nedb" } }, { "a.b": { $lt: "neda" } }).should.equal(false); - }); + model.match({ a: { b: 'nedb' } }, { 'a.b': { $lt: 'nedc' } }).should.equal(true) + model.match({ a: { b: 'nedb' } }, { 'a.b': { $lt: 'neda' } }).should.equal(false) + }) it('If field is an array field, a match means a match on at least one element', function () { - model.match({ a: [5, 10] }, { a: { $lt: 4 } }).should.equal(false); - model.match({ a: [5, 10] }, { a: { $lt: 6 } }).should.equal(true); - model.match({ a: [5, 10] }, { a: { $lt: 11 } }).should.equal(true); - }); + model.match({ a: [5, 10] }, { a: { $lt: 4 } }).should.equal(false) + model.match({ a: [5, 10] }, { a: { $lt: 6 } }).should.equal(true) + model.match({ a: [5, 10] }, { a: { $lt: 11 } }).should.equal(true) + }) it('Works with dates too', function () { - model.match({ a: new Date(1000) }, { a: { $gte: new Date(1001) } }).should.equal(false); - model.match({ a: new Date(1000) }, { a: { $lt: new Date(1001) } }).should.equal(true); - }); - - }); - + model.match({ a: new Date(1000) }, { a: { $gte: new Date(1001) } }).should.equal(false) + model.match({ a: new Date(1000) }, { a: { $lt: new Date(1001) } }).should.equal(true) + }) + }) // General behaviour is tested in the block about $lt. Here we just test operators work - describe('Other comparison operators: $lte, $gt, $gte, $ne, $in, $exists', function () { - + describe('Other comparison operators: $lte, $gt, $gte, $ne, $in, $stat', function () { it('$lte', function () { - model.match({ a: 5 }, { a: { $lte: 6 } }).should.equal(true); - model.match({ a: 5 }, { a: { $lte: 5 } }).should.equal(true); - model.match({ a: 5 }, { a: { $lte: 4 } }).should.equal(false); - }); + model.match({ a: 5 }, { a: { $lte: 6 } }).should.equal(true) + model.match({ a: 5 }, { a: { $lte: 5 } }).should.equal(true) + model.match({ a: 5 }, { a: { $lte: 4 } }).should.equal(false) + }) it('$gt', function () { - model.match({ a: 5 }, { a: { $gt: 6 } }).should.equal(false); - model.match({ a: 5 }, { a: { $gt: 5 } }).should.equal(false); - model.match({ a: 5 }, { a: { $gt: 4 } }).should.equal(true); - }); + model.match({ a: 5 }, { a: { $gt: 6 } }).should.equal(false) + model.match({ a: 5 }, { a: { $gt: 5 } }).should.equal(false) + model.match({ a: 5 }, { a: { $gt: 4 } }).should.equal(true) + }) it('$gte', function () { - model.match({ a: 5 }, { a: { $gte: 6 } }).should.equal(false); - model.match({ a: 5 }, { a: { $gte: 5 } }).should.equal(true); - model.match({ a: 5 }, { a: { $gte: 4 } }).should.equal(true); - }); + model.match({ a: 5 }, { a: { $gte: 6 } }).should.equal(false) + model.match({ a: 5 }, { a: { $gte: 5 } }).should.equal(true) + model.match({ a: 5 }, { a: { $gte: 4 } }).should.equal(true) + }) it('$ne', function () { - model.match({ a: 5 }, { a: { $ne: 4 } }).should.equal(true); - model.match({ a: 5 }, { a: { $ne: 5 } }).should.equal(false); - model.match({ a: 5 }, { b: { $ne: 5 } }).should.equal(true); - model.match({ a: false }, { a: { $ne: false } }).should.equal(false); - }); + model.match({ a: 5 }, { a: { $ne: 4 } }).should.equal(true) + model.match({ a: 5 }, { a: { $ne: 5 } }).should.equal(false) + model.match({ a: 5 }, { b: { $ne: 5 } }).should.equal(true) + model.match({ a: false }, { a: { $ne: false } }).should.equal(false) + }) it('$in', function () { - model.match({ a: 5 }, { a: { $in: [6, 8, 9] } }).should.equal(false); - model.match({ a: 6 }, { a: { $in: [6, 8, 9] } }).should.equal(true); - model.match({ a: 7 }, { a: { $in: [6, 8, 9] } }).should.equal(false); - model.match({ a: 8 }, { a: { $in: [6, 8, 9] } }).should.equal(true); + model.match({ a: 5 }, { a: { $in: [6, 8, 9] } }).should.equal(false) + model.match({ a: 6 }, { a: { $in: [6, 8, 9] } }).should.equal(true) + model.match({ a: 7 }, { a: { $in: [6, 8, 9] } }).should.equal(false) + model.match({ a: 8 }, { a: { $in: [6, 8, 9] } }).should.equal(true) model.match({ a: 9 }, { a: { $in: [6, 8, 9] } }).should.equal(true); - (function () { model.match({ a: 5 }, { a: { $in: 5 } }); }).should.throw(); - }); + (function () { model.match({ a: 5 }, { a: { $in: 5 } }) }).should.throw() + }) it('$nin', function () { - model.match({ a: 5 }, { a: { $nin: [6, 8, 9] } }).should.equal(true); - model.match({ a: 6 }, { a: { $nin: [6, 8, 9] } }).should.equal(false); - model.match({ a: 7 }, { a: { $nin: [6, 8, 9] } }).should.equal(true); - model.match({ a: 8 }, { a: { $nin: [6, 8, 9] } }).should.equal(false); - model.match({ a: 9 }, { a: { $nin: [6, 8, 9] } }).should.equal(false); + model.match({ a: 5 }, { a: { $nin: [6, 8, 9] } }).should.equal(true) + model.match({ a: 6 }, { a: { $nin: [6, 8, 9] } }).should.equal(false) + model.match({ a: 7 }, { a: { $nin: [6, 8, 9] } }).should.equal(true) + model.match({ a: 8 }, { a: { $nin: [6, 8, 9] } }).should.equal(false) + model.match({ a: 9 }, { a: { $nin: [6, 8, 9] } }).should.equal(false) // Matches if field doesn't exist model.match({ a: 9 }, { b: { $nin: [6, 8, 9] } }).should.equal(true); - (function () { model.match({ a: 5 }, { a: { $in: 5 } }); }).should.throw(); - }); + (function () { model.match({ a: 5 }, { a: { $in: 5 } }) }).should.throw() + }) it('$exists', function () { - model.match({ a: 5 }, { a: { $exists: 1 } }).should.equal(true); - model.match({ a: 5 }, { a: { $exists: true } }).should.equal(true); - model.match({ a: 5 }, { a: { $exists: new Date() } }).should.equal(true); - model.match({ a: 5 }, { a: { $exists: '' } }).should.equal(true); - model.match({ a: 5 }, { a: { $exists: [] } }).should.equal(true); - model.match({ a: 5 }, { a: { $exists: {} } }).should.equal(true); + model.match({ a: 5 }, { a: { $exists: 1 } }).should.equal(true) + model.match({ a: 5 }, { a: { $exists: true } }).should.equal(true) + model.match({ a: 5 }, { a: { $exists: new Date() } }).should.equal(true) + model.match({ a: 5 }, { a: { $exists: '' } }).should.equal(true) + model.match({ a: 5 }, { a: { $exists: [] } }).should.equal(true) + model.match({ a: 5 }, { a: { $exists: {} } }).should.equal(true) - model.match({ a: 5 }, { a: { $exists: 0 } }).should.equal(false); - model.match({ a: 5 }, { a: { $exists: false } }).should.equal(false); - model.match({ a: 5 }, { a: { $exists: null } }).should.equal(false); - model.match({ a: 5 }, { a: { $exists: undefined } }).should.equal(false); + model.match({ a: 5 }, { a: { $exists: 0 } }).should.equal(false) + model.match({ a: 5 }, { a: { $exists: false } }).should.equal(false) + model.match({ a: 5 }, { a: { $exists: null } }).should.equal(false) + model.match({ a: 5 }, { a: { $exists: undefined } }).should.equal(false) - model.match({ a: 5 }, { b: { $exists: true } }).should.equal(false); - - model.match({ a: 5 }, { b: { $exists: false } }).should.equal(true); - }); - - }); + model.match({ a: 5 }, { b: { $exists: true } }).should.equal(false) + model.match({ a: 5 }, { b: { $exists: false } }).should.equal(true) + }) + }) describe('Comparing on arrays', function () { - - it("Can perform a direct array match", function () { - model.match({ planets: ['Earth', 'Mars', 'Pluto'], something: 'else' }, { planets: ['Earth', 'Mars'] }).should.equal(false); - model.match({ planets: ['Earth', 'Mars', 'Pluto'], something: 'else' }, { planets: ['Earth', 'Mars', 'Pluto'] }).should.equal(true); - model.match({ planets: ['Earth', 'Mars', 'Pluto'], something: 'else' }, { planets: ['Earth', 'Pluto', 'Mars'] }).should.equal(false); - }); + it('Can perform a direct array match', function () { + model.match({ + planets: ['Earth', 'Mars', 'Pluto'], + something: 'else' + }, { planets: ['Earth', 'Mars'] }).should.equal(false) + model.match({ + planets: ['Earth', 'Mars', 'Pluto'], + something: 'else' + }, { planets: ['Earth', 'Mars', 'Pluto'] }).should.equal(true) + model.match({ + planets: ['Earth', 'Mars', 'Pluto'], + something: 'else' + }, { planets: ['Earth', 'Pluto', 'Mars'] }).should.equal(false) + }) it('Can query on the size of an array field', function () { // Non nested documents - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $size: 0 } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $size: 1 } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $size: 2 } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $size: 3 } }).should.equal(true); + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $size: 0 } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $size: 1 } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $size: 2 } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $size: 3 } }).should.equal(true) // Nested documents - model.match({ hello: 'world', description: { satellites: ['Moon', 'Hubble'], diameter: 6300 } }, { "description.satellites": { $size: 0 } }).should.equal(false); - model.match({ hello: 'world', description: { satellites: ['Moon', 'Hubble'], diameter: 6300 } }, { "description.satellites": { $size: 1 } }).should.equal(false); - model.match({ hello: 'world', description: { satellites: ['Moon', 'Hubble'], diameter: 6300 } }, { "description.satellites": { $size: 2 } }).should.equal(true); - model.match({ hello: 'world', description: { satellites: ['Moon', 'Hubble'], diameter: 6300 } }, { "description.satellites": { $size: 3 } }).should.equal(false); + model.match({ + hello: 'world', + description: { satellites: ['Moon', 'Hubble'], diameter: 6300 } + }, { 'description.satellites': { $size: 0 } }).should.equal(false) + model.match({ + hello: 'world', + description: { satellites: ['Moon', 'Hubble'], diameter: 6300 } + }, { 'description.satellites': { $size: 1 } }).should.equal(false) + model.match({ + hello: 'world', + description: { satellites: ['Moon', 'Hubble'], diameter: 6300 } + }, { 'description.satellites': { $size: 2 } }).should.equal(true) + model.match({ + hello: 'world', + description: { satellites: ['Moon', 'Hubble'], diameter: 6300 } + }, { 'description.satellites': { $size: 3 } }).should.equal(false) // Using a projected array - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.names": { $size: 0 } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.names": { $size: 1 } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.names": { $size: 2 } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.names": { $size: 3 } }).should.equal(true); - }); + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.names': { $size: 0 } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.names': { $size: 1 } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.names': { $size: 2 } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.names': { $size: 3 } }).should.equal(true) + }) it('$size operator works with empty arrays', function () { - model.match({ childrens: [] }, { "childrens": { $size: 0 } }).should.equal(true); - model.match({ childrens: [] }, { "childrens": { $size: 2 } }).should.equal(false); - model.match({ childrens: [] }, { "childrens": { $size: 3 } }).should.equal(false); - }); + model.match({ childrens: [] }, { childrens: { $size: 0 } }).should.equal(true) + model.match({ childrens: [] }, { childrens: { $size: 2 } }).should.equal(false) + model.match({ childrens: [] }, { childrens: { $size: 3 } }).should.equal(false) + }) it('Should throw an error if a query operator is used without comparing to an integer', function () { - (function () { model.match({ a: [1, 5] }, { a: { $size: 1.4 } }); }).should.throw(); - (function () { model.match({ a: [1, 5] }, { a: { $size: 'fdf' } }); }).should.throw(); - (function () { model.match({ a: [1, 5] }, { a: { $size: { $lt: 5 } } }); }).should.throw(); - }); + (function () { model.match({ a: [1, 5] }, { a: { $size: 1.4 } }) }).should.throw(); + (function () { model.match({ a: [1, 5] }, { a: { $size: 'fdf' } }) }).should.throw(); + (function () { model.match({ a: [1, 5] }, { a: { $size: { $lt: 5 } } }) }).should.throw() + }) it('Using $size operator on a non-array field should prevent match but not throw', function () { - model.match({ a: 5 }, { a: { $size: 1 } }).should.equal(false); - }); + model.match({ a: 5 }, { a: { $size: 1 } }).should.equal(false) + }) it('Can use $size several times in the same matcher', function () { - model.match({ childrens: [ 'Riri', 'Fifi', 'Loulou' ] }, { "childrens": { $size: 3, $size: 3 } }).should.equal(true); - model.match({ childrens: [ 'Riri', 'Fifi', 'Loulou' ] }, { "childrens": { $size: 3, $size: 4 } }).should.equal(false); // Of course this can never be true - }); + model.match({ childrens: ['Riri', 'Fifi', 'Loulou'] }, { + childrens: { + $size: 3, + // eslint-disable-next-line no-dupe-keys + $size: 3 + } + }).should.equal(true) + model.match({ childrens: ['Riri', 'Fifi', 'Loulou'] }, { + childrens: { + $size: 3, + // eslint-disable-next-line no-dupe-keys + $size: 4 + } + }).should.equal(false) // Of course this can never be true + }) it('Can query array documents with multiple simultaneous conditions', function () { // Non nested documents - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $elemMatch: { name: "Dewey", age: 7 } } }).should.equal(true); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $elemMatch: { name: "Dewey", age: 12 } } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $elemMatch: { name: "Louie", age: 3 } } }).should.equal(false); + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $elemMatch: { name: 'Dewey', age: 7 } } }).should.equal(true) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $elemMatch: { name: 'Dewey', age: 12 } } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $elemMatch: { name: 'Louie', age: 3 } } }).should.equal(false) // Nested documents - model.match({ outer: { childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] } }, { "outer.childrens": { $elemMatch: { name: "Dewey", age: 7 } } }).should.equal(true); - model.match({ outer: { childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] } }, { "outer.childrens": { $elemMatch: { name: "Dewey", age: 12 } } }).should.equal(false); - model.match({ outer: { childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] } }, { "outer.childrens": { $elemMatch: { name: "Louie", age: 3 } } }).should.equal(false); - - }); + model.match({ + outer: { + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + } + }, { 'outer.childrens': { $elemMatch: { name: 'Dewey', age: 7 } } }).should.equal(true) + model.match({ + outer: { + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + } + }, { 'outer.childrens': { $elemMatch: { name: 'Dewey', age: 12 } } }).should.equal(false) + model.match({ + outer: { + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + } + }, { 'outer.childrens': { $elemMatch: { name: 'Louie', age: 3 } } }).should.equal(false) + }) it('$elemMatch operator works with empty arrays', function () { - model.match({ childrens: [] }, { "childrens": { $elemMatch: { name: "Mitsos" } } }).should.equal(false); - model.match({ childrens: [] }, { "childrens": { $elemMatch: {} } }).should.equal(false); - }); + model.match({ childrens: [] }, { childrens: { $elemMatch: { name: 'Mitsos' } } }).should.equal(false) + model.match({ childrens: [] }, { childrens: { $elemMatch: {} } }).should.equal(false) + }) it('Can use more complex comparisons inside nested query documents', function () { - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $elemMatch: { name: "Dewey", age: { $gt: 6, $lt: 8 } } } }).should.equal(true); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $elemMatch: { name: "Dewey", age: { $in: [ 6, 7, 8 ] } } } } ).should.equal(true); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $elemMatch: { name: "Dewey", age: { $gt: 6, $lt: 7 } } } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens": { $elemMatch: { name: "Louie", age: { $gt: 6, $lte: 7 } } } }).should.equal(false); - }); - }); - + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $elemMatch: { name: 'Dewey', age: { $gt: 6, $lt: 8 } } } }).should.equal(true) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $elemMatch: { name: 'Dewey', age: { $in: [6, 7, 8] } } } }).should.equal(true) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $elemMatch: { name: 'Dewey', age: { $gt: 6, $lt: 7 } } } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { childrens: { $elemMatch: { name: 'Louie', age: { $gt: 6, $lte: 7 } } } }).should.equal(false) + }) + }) describe('Logical operators $or, $and, $not', function () { - it('Any of the subqueries should match for an $or to match', function () { - model.match({ hello: 'world' }, { $or: [ { hello: 'pluton' }, { hello: 'world' } ] }).should.equal(true); - model.match({ hello: 'pluton' }, { $or: [ { hello: 'pluton' }, { hello: 'world' } ] }).should.equal(true); - model.match({ hello: 'nope' }, { $or: [ { hello: 'pluton' }, { hello: 'world' } ] }).should.equal(false); - model.match({ hello: 'world', age: 15 }, { $or: [ { hello: 'pluton' }, { age: { $lt: 20 } } ] }).should.equal(true); - model.match({ hello: 'world', age: 15 }, { $or: [ { hello: 'pluton' }, { age: { $lt: 10 } } ] }).should.equal(false); - }); + model.match({ hello: 'world' }, { $or: [{ hello: 'pluton' }, { hello: 'world' }] }).should.equal(true) + model.match({ hello: 'pluton' }, { $or: [{ hello: 'pluton' }, { hello: 'world' }] }).should.equal(true) + model.match({ hello: 'nope' }, { $or: [{ hello: 'pluton' }, { hello: 'world' }] }).should.equal(false) + model.match({ + hello: 'world', + age: 15 + }, { $or: [{ hello: 'pluton' }, { age: { $lt: 20 } }] }).should.equal(true) + model.match({ + hello: 'world', + age: 15 + }, { $or: [{ hello: 'pluton' }, { age: { $lt: 10 } }] }).should.equal(false) + }) it('All of the subqueries should match for an $and to match', function () { - model.match({ hello: 'world', age: 15 }, { $and: [ { age: 15 }, { hello: 'world' } ] }).should.equal(true); - model.match({ hello: 'world', age: 15 }, { $and: [ { age: 16 }, { hello: 'world' } ] }).should.equal(false); - model.match({ hello: 'world', age: 15 }, { $and: [ { hello: 'world' }, { age: { $lt: 20 } } ] }).should.equal(true); - model.match({ hello: 'world', age: 15 }, { $and: [ { hello: 'pluton' }, { age: { $lt: 20 } } ] }).should.equal(false); - }); + model.match({ hello: 'world', age: 15 }, { $and: [{ age: 15 }, { hello: 'world' }] }).should.equal(true) + model.match({ hello: 'world', age: 15 }, { $and: [{ age: 16 }, { hello: 'world' }] }).should.equal(false) + model.match({ + hello: 'world', + age: 15 + }, { $and: [{ hello: 'world' }, { age: { $lt: 20 } }] }).should.equal(true) + model.match({ + hello: 'world', + age: 15 + }, { $and: [{ hello: 'pluton' }, { age: { $lt: 20 } }] }).should.equal(false) + }) it('Subquery should not match for a $not to match', function () { - model.match({ a: 5, b: 10 }, { a: 5 }).should.equal(true); - model.match({ a: 5, b: 10 }, { $not: { a: 5 } }).should.equal(false); - }); + model.match({ a: 5, b: 10 }, { a: 5 }).should.equal(true) + model.match({ a: 5, b: 10 }, { $not: { a: 5 } }).should.equal(false) + }) it('Logical operators are all top-level, only other logical operators can be above', function () { - (function () { model.match({ a: { b: 7 } }, { a: { $or: [ { b: 5 }, { b: 7 } ] } })}).should.throw(); - model.match({ a: { b: 7 } }, { $or: [ { "a.b": 5 }, { "a.b": 7 } ] }).should.equal(true); - }); + (function () { model.match({ a: { b: 7 } }, { a: { $or: [{ b: 5 }, { b: 7 }] } }) }).should.throw() + model.match({ a: { b: 7 } }, { $or: [{ 'a.b': 5 }, { 'a.b': 7 }] }).should.equal(true) + }) it('Logical operators can be combined as long as they are on top of the decision tree', function () { - model.match({ a: 5, b: 7, c: 12 }, { $or: [ { $and: [ { a: 5 }, { b: 8 } ] }, { $and: [{ a: 5 }, { c : { $lt: 40 } }] } ] }).should.equal(true); - model.match({ a: 5, b: 7, c: 12 }, { $or: [ { $and: [ { a: 5 }, { b: 8 } ] }, { $and: [{ a: 5 }, { c : { $lt: 10 } }] } ] }).should.equal(false); - }); + model.match({ + a: 5, + b: 7, + c: 12 + }, { $or: [{ $and: [{ a: 5 }, { b: 8 }] }, { $and: [{ a: 5 }, { c: { $lt: 40 } }] }] }).should.equal(true) + model.match({ + a: 5, + b: 7, + c: 12 + }, { $or: [{ $and: [{ a: 5 }, { b: 8 }] }, { $and: [{ a: 5 }, { c: { $lt: 10 } }] }] }).should.equal(false) + }) it('Should throw an error if a logical operator is used without an array or if an unknown logical operator is used', function () { - (function () { model.match({ a: 5 }, { $or: { a: 5, a: 6 } }); }).should.throw(); - (function () { model.match({ a: 5 }, { $and: { a: 5, a: 6 } }); }).should.throw(); - (function () { model.match({ a: 5 }, { $unknown: [ { a: 5 } ] }); }).should.throw(); - }); - - }); - + // eslint-disable-next-line no-dupe-keys + (function () { model.match({ a: 5 }, { $or: { a: 5, a: 6 } }) }).should.throw(); + // eslint-disable-next-line no-dupe-keys + (function () { model.match({ a: 5 }, { $and: { a: 5, a: 6 } }) }).should.throw(); + (function () { model.match({ a: 5 }, { $unknown: [{ a: 5 }] }) }).should.throw() + }) + }) describe('Comparison operator $where', function () { - it('Function should match and not match correctly', function () { - model.match({ a: 4}, { $where: function () { return this.a === 4; } }).should.equal(true); - model.match({ a: 4}, { $where: function () { return this.a === 5; } }).should.equal(false); - }); + model.match({ a: 4 }, { $where: function () { return this.a === 4 } }).should.equal(true) + model.match({ a: 4 }, { $where: function () { return this.a === 5 } }).should.equal(false) + }) it('Should throw an error if the $where function is not, in fact, a function', function () { - (function () { model.match({ a: 4 }, { $where: 'not a function' }); }).should.throw(); - }); + (function () { model.match({ a: 4 }, { $where: 'not a function' }) }).should.throw() + }) it('Should throw an error if the $where function returns a non-boolean', function () { - (function () { model.match({ a: 4 }, { $where: function () { return 'not a boolean'; } }); }).should.throw(); - }); - - it('Should be able to do the complex matching it must be used for', function () { - var checkEmail = function() { - if (!this.firstName || !this.lastName) { return false; } - return this.firstName.toLowerCase() + "." + this.lastName.toLowerCase() + "@gmail.com" === this.email; - }; - model.match({ firstName: "John", lastName: "Doe", email: "john.doe@gmail.com" }, { $where: checkEmail }).should.equal(true); - model.match({ firstName: "john", lastName: "doe", email: "john.doe@gmail.com" }, { $where: checkEmail }).should.equal(true); - model.match({ firstName: "Jane", lastName: "Doe", email: "john.doe@gmail.com" }, { $where: checkEmail }).should.equal(false); - model.match({ firstName: "John", lastName: "Deere", email: "john.doe@gmail.com" }, { $where: checkEmail }).should.equal(false); - model.match({ lastName: "Doe", email: "john.doe@gmail.com" }, { $where: checkEmail }).should.equal(false); - }); - - }); + (function () { model.match({ a: 4 }, { $where: function () { return 'not a boolean' } }) }).should.throw() + }) + it('Should be able to do the complex matching it must be used for', function () { + const checkEmail = function () { + if (!this.firstName || !this.lastName) { return false } + return this.firstName.toLowerCase() + '.' + this.lastName.toLowerCase() + '@gmail.com' === this.email + } + model.match({ + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@gmail.com' + }, { $where: checkEmail }).should.equal(true) + model.match({ + firstName: 'john', + lastName: 'doe', + email: 'john.doe@gmail.com' + }, { $where: checkEmail }).should.equal(true) + model.match({ + firstName: 'Jane', + lastName: 'Doe', + email: 'john.doe@gmail.com' + }, { $where: checkEmail }).should.equal(false) + model.match({ + firstName: 'John', + lastName: 'Deere', + email: 'john.doe@gmail.com' + }, { $where: checkEmail }).should.equal(false) + model.match({ lastName: 'Doe', email: 'john.doe@gmail.com' }, { $where: checkEmail }).should.equal(false) + }) + }) describe('Array fields', function () { - it('Field equality', function () { - model.match({ tags: ['node', 'js', 'db'] }, { tags: 'python' }).should.equal(false); - model.match({ tags: ['node', 'js', 'db'] }, { tagss: 'js' }).should.equal(false); - model.match({ tags: ['node', 'js', 'db'] }, { tags: 'js' }).should.equal(true); - model.match({ tags: ['node', 'js', 'db'] }, { tags: 'js', tags: 'node' }).should.equal(true); + model.match({ tags: ['node', 'js', 'db'] }, { tags: 'python' }).should.equal(false) + model.match({ tags: ['node', 'js', 'db'] }, { tagss: 'js' }).should.equal(false) + model.match({ tags: ['node', 'js', 'db'] }, { tags: 'js' }).should.equal(true) + // eslint-disable-next-line no-dupe-keys + model.match({ tags: ['node', 'js', 'db'] }, { tags: 'js', tags: 'node' }).should.equal(true) // Mixed matching with array and non array - model.match({ tags: ['node', 'js', 'db'], nedb: true }, { tags: 'js', nedb: true }).should.equal(true); + model.match({ tags: ['node', 'js', 'db'], nedb: true }, { tags: 'js', nedb: true }).should.equal(true) // Nested matching - model.match({ number: 5, data: { tags: ['node', 'js', 'db'] } }, { "data.tags": 'js' }).should.equal(true); - model.match({ number: 5, data: { tags: ['node', 'js', 'db'] } }, { "data.tags": 'j' }).should.equal(false); - }); + model.match({ number: 5, data: { tags: ['node', 'js', 'db'] } }, { 'data.tags': 'js' }).should.equal(true) + model.match({ number: 5, data: { tags: ['node', 'js', 'db'] } }, { 'data.tags': 'j' }).should.equal(false) + }) it('With one comparison operator', function () { - model.match({ ages: [3, 7, 12] }, { ages: { $lt: 2 } }).should.equal(false); - model.match({ ages: [3, 7, 12] }, { ages: { $lt: 3 } }).should.equal(false); - model.match({ ages: [3, 7, 12] }, { ages: { $lt: 4 } }).should.equal(true); - model.match({ ages: [3, 7, 12] }, { ages: { $lt: 8 } }).should.equal(true); - model.match({ ages: [3, 7, 12] }, { ages: { $lt: 13 } }).should.equal(true); - }); + model.match({ ages: [3, 7, 12] }, { ages: { $lt: 2 } }).should.equal(false) + model.match({ ages: [3, 7, 12] }, { ages: { $lt: 3 } }).should.equal(false) + model.match({ ages: [3, 7, 12] }, { ages: { $lt: 4 } }).should.equal(true) + model.match({ ages: [3, 7, 12] }, { ages: { $lt: 8 } }).should.equal(true) + model.match({ ages: [3, 7, 12] }, { ages: { $lt: 13 } }).should.equal(true) + }) it('Works with arrays that are in subdocuments', function () { - model.match({ children: { ages: [3, 7, 12] } }, { "children.ages": { $lt: 2 } }).should.equal(false); - model.match({ children: { ages: [3, 7, 12] } }, { "children.ages": { $lt: 3 } }).should.equal(false); - model.match({ children: { ages: [3, 7, 12] } }, { "children.ages": { $lt: 4 } }).should.equal(true); - model.match({ children: { ages: [3, 7, 12] } }, { "children.ages": { $lt: 8 } }).should.equal(true); - model.match({ children: { ages: [3, 7, 12] } }, { "children.ages": { $lt: 13 } }).should.equal(true); - }); + model.match({ children: { ages: [3, 7, 12] } }, { 'children.ages': { $lt: 2 } }).should.equal(false) + model.match({ children: { ages: [3, 7, 12] } }, { 'children.ages': { $lt: 3 } }).should.equal(false) + model.match({ children: { ages: [3, 7, 12] } }, { 'children.ages': { $lt: 4 } }).should.equal(true) + model.match({ children: { ages: [3, 7, 12] } }, { 'children.ages': { $lt: 8 } }).should.equal(true) + model.match({ children: { ages: [3, 7, 12] } }, { 'children.ages': { $lt: 13 } }).should.equal(true) + }) it('Can query inside arrays thanks to dot notation', function () { - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.age": { $lt: 2 } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.age": { $lt: 3 } }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.age": { $lt: 4 } }).should.equal(true); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.age": { $lt: 8 } }).should.equal(true); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.age": { $lt: 13 } }).should.equal(true); - - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.name": 'Louis' }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.name": 'Louie' }).should.equal(true); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.name": 'Lewi' }).should.equal(false); - }); - - it('Can query for a specific element inside arrays thanks to dot notation', function () { - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.0.name": 'Louie' }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.1.name": 'Louie' }).should.equal(false); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.2.name": 'Louie' }).should.equal(true); - model.match({ childrens: [ { name: "Huey", age: 3 }, { name: "Dewey", age: 7 }, { name: "Louie", age: 12 } ] }, { "childrens.3.name": 'Louie' }).should.equal(false); - }); - - it('A single array-specific operator and the query is treated as array specific', function () { - (function () { model.match({ childrens: [ 'Riri', 'Fifi', 'Loulou' ] }, { "childrens": { "Fifi": true, $size: 3 } })}).should.throw(); - }); - - it('Can mix queries on array fields and non array filds with array specific operators', function () { - model.match({ uncle: 'Donald', nephews: [ 'Riri', 'Fifi', 'Loulou' ] }, { nephews: { $size: 2 }, uncle: 'Donald' }).should.equal(false); - model.match({ uncle: 'Donald', nephews: [ 'Riri', 'Fifi', 'Loulou' ] }, { nephews: { $size: 3 }, uncle: 'Donald' }).should.equal(true); - model.match({ uncle: 'Donald', nephews: [ 'Riri', 'Fifi', 'Loulou' ] }, { nephews: { $size: 4 }, uncle: 'Donald' }).should.equal(false); + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.age': { $lt: 2 } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.age': { $lt: 3 } }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.age': { $lt: 4 } }).should.equal(true) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.age': { $lt: 8 } }).should.equal(true) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.age': { $lt: 13 } }).should.equal(true) + + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.name': 'Louis' }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.name': 'Louie' }).should.equal(true) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.name': 'Lewi' }).should.equal(false) + }) - model.match({ uncle: 'Donals', nephews: [ 'Riri', 'Fifi', 'Loulou' ] }, { nephews: { $size: 3 }, uncle: 'Picsou' }).should.equal(false); - model.match({ uncle: 'Donald', nephews: [ 'Riri', 'Fifi', 'Loulou' ] }, { nephews: { $size: 3 }, uncle: 'Donald' }).should.equal(true); - model.match({ uncle: 'Donald', nephews: [ 'Riri', 'Fifi', 'Loulou' ] }, { nephews: { $size: 3 }, uncle: 'Daisy' }).should.equal(false); - }); - - }); + it('Can query for a specific element inside arrays thanks to dot notation', function () { + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.0.name': 'Louie' }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.1.name': 'Louie' }).should.equal(false) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.2.name': 'Louie' }).should.equal(true) + model.match({ + childrens: [{ name: 'Huey', age: 3 }, { name: 'Dewey', age: 7 }, { + name: 'Louie', + age: 12 + }] + }, { 'childrens.3.name': 'Louie' }).should.equal(false) + }) - }); // ==== End of 'Querying' ==== // + it('A single array-specific operator and the query is treated as array specific', function () { + (function () { + model.match({ childrens: ['Riri', 'Fifi', 'Loulou'] }, { + childrens: { + Fifi: true, + $size: 3 + } + }) + }).should.throw() + }) -}); + it('Can mix queries on array fields and non array filds with array specific operators', function () { + model.match({ uncle: 'Donald', nephews: ['Riri', 'Fifi', 'Loulou'] }, { + nephews: { $size: 2 }, + uncle: 'Donald' + }).should.equal(false) + model.match({ uncle: 'Donald', nephews: ['Riri', 'Fifi', 'Loulou'] }, { + nephews: { $size: 3 }, + uncle: 'Donald' + }).should.equal(true) + model.match({ uncle: 'Donald', nephews: ['Riri', 'Fifi', 'Loulou'] }, { + nephews: { $size: 4 }, + uncle: 'Donald' + }).should.equal(false) + + model.match({ uncle: 'Donals', nephews: ['Riri', 'Fifi', 'Loulou'] }, { + nephews: { $size: 3 }, + uncle: 'Picsou' + }).should.equal(false) + model.match({ uncle: 'Donald', nephews: ['Riri', 'Fifi', 'Loulou'] }, { + nephews: { $size: 3 }, + uncle: 'Donald' + }).should.equal(true) + model.match({ uncle: 'Donald', nephews: ['Riri', 'Fifi', 'Loulou'] }, { + nephews: { $size: 3 }, + uncle: 'Daisy' + }).should.equal(false) + }) + }) + }) // ==== End of 'Querying' ==== // +}) diff --git a/test/persistence.test.js b/test/persistence.test.js index ab1869a..34de0d3 100755 --- a/test/persistence.test.js +++ b/test/persistence.test.js @@ -1,926 +1,921 @@ -var should = require('chai').should() - , assert = require('chai').assert - , testDb = 'workspace/test.db' - , fs = require('fs') - , path = require('path') - , _ = require('underscore') - , async = require('async') - , model = require('../lib/model') - , customUtils = require('../lib/customUtils') - , Datastore = require('../lib/datastore') - , Persistence = require('../lib/persistence') - , storage = require('../lib/storage') - , child_process = require('child_process') -; - +/* eslint-env mocha */ +const chai = require('chai') +const testDb = 'workspace/test.db' +const fs = require('fs') +const path = require('path') +const _ = require('underscore') +const async = require('async') +const model = require('../lib/model') +const Datastore = require('../lib/datastore') +const Persistence = require('../lib/persistence') +const storage = require('../lib/storage') +const { execFile, fork } = require('child_process') + +const { assert } = chai +chai.should() describe('Persistence', function () { - var d; + let d beforeEach(function (done) { - d = new Datastore({ filename: testDb }); - d.filename.should.equal(testDb); - d.inMemoryOnly.should.equal(false); + d = new Datastore({ filename: testDb }) + d.filename.should.equal(testDb) + d.inMemoryOnly.should.equal(false) async.waterfall([ function (cb) { Persistence.ensureDirectoryExists(path.dirname(testDb), function () { - fs.exists(testDb, function (exists) { - if (exists) { - fs.unlink(testDb, cb); - } else { return cb(); } - }); - }); + fs.access(testDb, fs.constants.FS_OK, function (err) { + if (!err) { + fs.unlink(testDb, cb) + } else { return cb() } + }) + }) + }, + function (cb) { + d.loadDatabase(function (err) { + assert.isNull(err) + d.getAllData().length.should.equal(0) + return cb() + }) } - , function (cb) { - d.loadDatabase(function (err) { - assert.isNull(err); - d.getAllData().length.should.equal(0); - return cb(); - }); - } - ], done); - }); + ], done) + }) it('Every line represents a document', function () { - var now = new Date() - , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "2", hello: 'world' }) + '\n' + - model.serialize({ _id: "3", nested: { today: now } }) - , treatedData = d.persistence.treatRawData(rawData).data - ; - - treatedData.sort(function (a, b) { return a._id - b._id; }); - treatedData.length.should.equal(3); - _.isEqual(treatedData[0], { _id: "1", a: 2, ages: [1, 5, 12] }).should.equal(true); - _.isEqual(treatedData[1], { _id: "2", hello: 'world' }).should.equal(true); - _.isEqual(treatedData[2], { _id: "3", nested: { today: now } }).should.equal(true); - }); + const now = new Date() + const rawData = model.serialize({ _id: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: '2', hello: 'world' }) + '\n' + + model.serialize({ _id: '3', nested: { today: now } }) + const treatedData = d.persistence.treatRawData(rawData).data + + treatedData.sort(function (a, b) { return a._id - b._id }) + treatedData.length.should.equal(3) + _.isEqual(treatedData[0], { _id: '1', a: 2, ages: [1, 5, 12] }).should.equal(true) + _.isEqual(treatedData[1], { _id: '2', hello: 'world' }).should.equal(true) + _.isEqual(treatedData[2], { _id: '3', nested: { today: now } }).should.equal(true) + }) it('Badly formatted lines have no impact on the treated data', function () { - var now = new Date() - , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - 'garbage\n' + - model.serialize({ _id: "3", nested: { today: now } }) - , treatedData = d.persistence.treatRawData(rawData).data - ; - - treatedData.sort(function (a, b) { return a._id - b._id; }); - treatedData.length.should.equal(2); - _.isEqual(treatedData[0], { _id: "1", a: 2, ages: [1, 5, 12] }).should.equal(true); - _.isEqual(treatedData[1], { _id: "3", nested: { today: now } }).should.equal(true); - }); + const now = new Date() + const rawData = model.serialize({ _id: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + 'garbage\n' + + model.serialize({ _id: '3', nested: { today: now } }) + const treatedData = d.persistence.treatRawData(rawData).data + + treatedData.sort(function (a, b) { return a._id - b._id }) + treatedData.length.should.equal(2) + _.isEqual(treatedData[0], { _id: '1', a: 2, ages: [1, 5, 12] }).should.equal(true) + _.isEqual(treatedData[1], { _id: '3', nested: { today: now } }).should.equal(true) + }) it('Well formatted lines that have no _id are not included in the data', function () { - var now = new Date() - , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "2", hello: 'world' }) + '\n' + - model.serialize({ nested: { today: now } }) - , treatedData = d.persistence.treatRawData(rawData).data - ; - - treatedData.sort(function (a, b) { return a._id - b._id; }); - treatedData.length.should.equal(2); - _.isEqual(treatedData[0], { _id: "1", a: 2, ages: [1, 5, 12] }).should.equal(true); - _.isEqual(treatedData[1], { _id: "2", hello: 'world' }).should.equal(true); - }); + const now = new Date() + const rawData = model.serialize({ _id: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: '2', hello: 'world' }) + '\n' + + model.serialize({ nested: { today: now } }) + const treatedData = d.persistence.treatRawData(rawData).data + + treatedData.sort(function (a, b) { return a._id - b._id }) + treatedData.length.should.equal(2) + _.isEqual(treatedData[0], { _id: '1', a: 2, ages: [1, 5, 12] }).should.equal(true) + _.isEqual(treatedData[1], { _id: '2', hello: 'world' }).should.equal(true) + }) it('If two lines concern the same doc (= same _id), the last one is the good version', function () { - var now = new Date() - , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "2", hello: 'world' }) + '\n' + - model.serialize({ _id: "1", nested: { today: now } }) - , treatedData = d.persistence.treatRawData(rawData).data - ; - - treatedData.sort(function (a, b) { return a._id - b._id; }); - treatedData.length.should.equal(2); - _.isEqual(treatedData[0], { _id: "1", nested: { today: now } }).should.equal(true); - _.isEqual(treatedData[1], { _id: "2", hello: 'world' }).should.equal(true); - }); + const now = new Date() + const rawData = model.serialize({ _id: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: '2', hello: 'world' }) + '\n' + + model.serialize({ _id: '1', nested: { today: now } }) + const treatedData = d.persistence.treatRawData(rawData).data + + treatedData.sort(function (a, b) { return a._id - b._id }) + treatedData.length.should.equal(2) + _.isEqual(treatedData[0], { _id: '1', nested: { today: now } }).should.equal(true) + _.isEqual(treatedData[1], { _id: '2', hello: 'world' }).should.equal(true) + }) it('If a doc contains $$deleted: true, that means we need to remove it from the data', function () { - var now = new Date() - , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "2", hello: 'world' }) + '\n' + - model.serialize({ _id: "1", $$deleted: true }) + '\n' + - model.serialize({ _id: "3", today: now }) - , treatedData = d.persistence.treatRawData(rawData).data - ; - - treatedData.sort(function (a, b) { return a._id - b._id; }); - treatedData.length.should.equal(2); - _.isEqual(treatedData[0], { _id: "2", hello: 'world' }).should.equal(true); - _.isEqual(treatedData[1], { _id: "3", today: now }).should.equal(true); - }); + const now = new Date() + const rawData = model.serialize({ _id: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: '2', hello: 'world' }) + '\n' + + model.serialize({ _id: '1', $$deleted: true }) + '\n' + + model.serialize({ _id: '3', today: now }) + const treatedData = d.persistence.treatRawData(rawData).data + + treatedData.sort(function (a, b) { return a._id - b._id }) + treatedData.length.should.equal(2) + _.isEqual(treatedData[0], { _id: '2', hello: 'world' }).should.equal(true) + _.isEqual(treatedData[1], { _id: '3', today: now }).should.equal(true) + }) it('If a doc contains $$deleted: true, no error is thrown if the doc wasnt in the list before', function () { - var now = new Date() - , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ _id: "2", $$deleted: true }) + '\n' + - model.serialize({ _id: "3", today: now }) - , treatedData = d.persistence.treatRawData(rawData).data - ; - - treatedData.sort(function (a, b) { return a._id - b._id; }); - treatedData.length.should.equal(2); - _.isEqual(treatedData[0], { _id: "1", a: 2, ages: [1, 5, 12] }).should.equal(true); - _.isEqual(treatedData[1], { _id: "3", today: now }).should.equal(true); - }); + const now = new Date() + const rawData = model.serialize({ _id: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ _id: '2', $$deleted: true }) + '\n' + + model.serialize({ _id: '3', today: now }) + const treatedData = d.persistence.treatRawData(rawData).data + + treatedData.sort(function (a, b) { return a._id - b._id }) + treatedData.length.should.equal(2) + _.isEqual(treatedData[0], { _id: '1', a: 2, ages: [1, 5, 12] }).should.equal(true) + _.isEqual(treatedData[1], { _id: '3', today: now }).should.equal(true) + }) it('If a doc contains $$indexCreated, no error is thrown during treatRawData and we can get the index options', function () { - var now = new Date() - , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + - model.serialize({ $$indexCreated: { fieldName: "test", unique: true } }) + '\n' + - model.serialize({ _id: "3", today: now }) - , treatedData = d.persistence.treatRawData(rawData).data - , indexes = d.persistence.treatRawData(rawData).indexes - ; - - Object.keys(indexes).length.should.equal(1); - assert.deepEqual(indexes.test, { fieldName: "test", unique: true }); - - treatedData.sort(function (a, b) { return a._id - b._id; }); - treatedData.length.should.equal(2); - _.isEqual(treatedData[0], { _id: "1", a: 2, ages: [1, 5, 12] }).should.equal(true); - _.isEqual(treatedData[1], { _id: "3", today: now }).should.equal(true); - }); + const now = new Date() + const rawData = model.serialize({ _id: '1', a: 2, ages: [1, 5, 12] }) + '\n' + + model.serialize({ $$indexCreated: { fieldName: 'test', unique: true } }) + '\n' + + model.serialize({ _id: '3', today: now }) + const treatedData = d.persistence.treatRawData(rawData).data + const indexes = d.persistence.treatRawData(rawData).indexes + + Object.keys(indexes).length.should.equal(1) + assert.deepEqual(indexes.test, { fieldName: 'test', unique: true }) + + treatedData.sort(function (a, b) { return a._id - b._id }) + treatedData.length.should.equal(2) + _.isEqual(treatedData[0], { _id: '1', a: 2, ages: [1, 5, 12] }).should.equal(true) + _.isEqual(treatedData[1], { _id: '3', today: now }).should.equal(true) + }) it('Compact database on load', function (done) { d.insert({ a: 2 }, function () { d.insert({ a: 4 }, function () { d.remove({ a: 2 }, {}, function () { // Here, the underlying file is 3 lines long for only one document - var data = fs.readFileSync(d.filename, 'utf8').split('\n') - , filledCount = 0; + const data = fs.readFileSync(d.filename, 'utf8').split('\n') + let filledCount = 0 - data.forEach(function (item) { if (item.length > 0) { filledCount += 1; } }); - filledCount.should.equal(3); + data.forEach(function (item) { if (item.length > 0) { filledCount += 1 } }) + filledCount.should.equal(3) d.loadDatabase(function (err) { - assert.isNull(err); + assert.isNull(err) // Now, the file has been compacted and is only 1 line long - var data = fs.readFileSync(d.filename, 'utf8').split('\n') - , filledCount = 0; + const data = fs.readFileSync(d.filename, 'utf8').split('\n') + let filledCount = 0 - data.forEach(function (item) { if (item.length > 0) { filledCount += 1; } }); - filledCount.should.equal(1); + data.forEach(function (item) { if (item.length > 0) { filledCount += 1 } }) + filledCount.should.equal(1) - done(); - }); + done() + }) }) - }); - }); - }); + }) + }) + }) it('Calling loadDatabase after the data was modified doesnt change its contents', function (done) { d.loadDatabase(function () { d.insert({ a: 1 }, function (err) { - assert.isNull(err); + assert.isNull(err) d.insert({ a: 2 }, function (err) { - var data = d.getAllData() - , doc1 = _.find(data, function (doc) { return doc.a === 1; }) - , doc2 = _.find(data, function (doc) { return doc.a === 2; }) - ; - assert.isNull(err); - data.length.should.equal(2); - doc1.a.should.equal(1); - doc2.a.should.equal(2); + const data = d.getAllData() + const doc1 = _.find(data, function (doc) { return doc.a === 1 }) + const doc2 = _.find(data, function (doc) { return doc.a === 2 }) + assert.isNull(err) + data.length.should.equal(2) + doc1.a.should.equal(1) + doc2.a.should.equal(2) d.loadDatabase(function (err) { - var data = d.getAllData() - , doc1 = _.find(data, function (doc) { return doc.a === 1; }) - , doc2 = _.find(data, function (doc) { return doc.a === 2; }) - ; - assert.isNull(err); - data.length.should.equal(2); - doc1.a.should.equal(1); - doc2.a.should.equal(2); - - done(); - }); - }); - }); - }); - }); + const data = d.getAllData() + const doc1 = _.find(data, function (doc) { return doc.a === 1 }) + const doc2 = _.find(data, function (doc) { return doc.a === 2 }) + assert.isNull(err) + data.length.should.equal(2) + doc1.a.should.equal(1) + doc2.a.should.equal(2) + + done() + }) + }) + }) + }) + }) it('Calling loadDatabase after the datafile was removed will reset the database', function (done) { d.loadDatabase(function () { d.insert({ a: 1 }, function (err) { - assert.isNull(err); + assert.isNull(err) d.insert({ a: 2 }, function (err) { - var data = d.getAllData() - , doc1 = _.find(data, function (doc) { return doc.a === 1; }) - , doc2 = _.find(data, function (doc) { return doc.a === 2; }) - ; - assert.isNull(err); - data.length.should.equal(2); - doc1.a.should.equal(1); - doc2.a.should.equal(2); + const data = d.getAllData() + const doc1 = _.find(data, function (doc) { return doc.a === 1 }) + const doc2 = _.find(data, function (doc) { return doc.a === 2 }) + assert.isNull(err) + data.length.should.equal(2) + doc1.a.should.equal(1) + doc2.a.should.equal(2) fs.unlink(testDb, function (err) { - assert.isNull(err); + assert.isNull(err) d.loadDatabase(function (err) { - assert.isNull(err); - d.getAllData().length.should.equal(0); + assert.isNull(err) + d.getAllData().length.should.equal(0) - done(); - }); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) + }) it('Calling loadDatabase after the datafile was modified loads the new data', function (done) { d.loadDatabase(function () { d.insert({ a: 1 }, function (err) { - assert.isNull(err); + assert.isNull(err) d.insert({ a: 2 }, function (err) { - var data = d.getAllData() - , doc1 = _.find(data, function (doc) { return doc.a === 1; }) - , doc2 = _.find(data, function (doc) { return doc.a === 2; }) - ; - assert.isNull(err); - data.length.should.equal(2); - doc1.a.should.equal(1); - doc2.a.should.equal(2); + const data = d.getAllData() + const doc1 = _.find(data, function (doc) { return doc.a === 1 }) + const doc2 = _.find(data, function (doc) { return doc.a === 2 }) + assert.isNull(err) + data.length.should.equal(2) + doc1.a.should.equal(1) + doc2.a.should.equal(2) fs.writeFile(testDb, '{"a":3,"_id":"aaa"}', 'utf8', function (err) { - assert.isNull(err); + assert.isNull(err) d.loadDatabase(function (err) { - var data = d.getAllData() - , doc1 = _.find(data, function (doc) { return doc.a === 1; }) - , doc2 = _.find(data, function (doc) { return doc.a === 2; }) - , doc3 = _.find(data, function (doc) { return doc.a === 3; }) - ; - assert.isNull(err); - data.length.should.equal(1); - doc3.a.should.equal(3); - assert.isUndefined(doc1); - assert.isUndefined(doc2); - - done(); - }); - }); - }); - }); - }); - }); - - it("When treating raw data, refuse to proceed if too much data is corrupt, to avoid data loss", function (done) { - var corruptTestFilename = 'workspace/corruptTest.db' - , fakeData = '{"_id":"one","hello":"world"}\n' + 'Some corrupt data\n' + '{"_id":"two","hello":"earth"}\n' + '{"_id":"three","hello":"you"}\n' - , d - ; - fs.writeFileSync(corruptTestFilename, fakeData, "utf8"); + const data = d.getAllData() + const doc1 = _.find(data, function (doc) { return doc.a === 1 }) + const doc2 = _.find(data, function (doc) { return doc.a === 2 }) + const doc3 = _.find(data, function (doc) { return doc.a === 3 }) + assert.isNull(err) + data.length.should.equal(1) + doc3.a.should.equal(3) + assert.isUndefined(doc1) + assert.isUndefined(doc2) + + done() + }) + }) + }) + }) + }) + }) + + it('When treating raw data, refuse to proceed if too much data is corrupt, to avoid data loss', function (done) { + const corruptTestFilename = 'workspace/corruptTest.db' + const fakeData = '{"_id":"one","hello":"world"}\n' + 'Some corrupt data\n' + '{"_id":"two","hello":"earth"}\n' + '{"_id":"three","hello":"you"}\n' + let d + fs.writeFileSync(corruptTestFilename, fakeData, 'utf8') // Default corruptAlertThreshold - d = new Datastore({ filename: corruptTestFilename }); + d = new Datastore({ filename: corruptTestFilename }) d.loadDatabase(function (err) { - assert.isDefined(err); - assert.isNotNull(err); + assert.isDefined(err) + assert.isNotNull(err) - fs.writeFileSync(corruptTestFilename, fakeData, "utf8"); - d = new Datastore({ filename: corruptTestFilename, corruptAlertThreshold: 1 }); + fs.writeFileSync(corruptTestFilename, fakeData, 'utf8') + d = new Datastore({ filename: corruptTestFilename, corruptAlertThreshold: 1 }) d.loadDatabase(function (err) { - assert.isNull(err); + assert.isNull(err) - fs.writeFileSync(corruptTestFilename, fakeData, "utf8"); - d = new Datastore({ filename: corruptTestFilename, corruptAlertThreshold: 0 }); + fs.writeFileSync(corruptTestFilename, fakeData, 'utf8') + d = new Datastore({ filename: corruptTestFilename, corruptAlertThreshold: 0 }) d.loadDatabase(function (err) { - assert.isDefined(err); - assert.isNotNull(err); + assert.isDefined(err) + assert.isNotNull(err) - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) - it("Can listen to compaction events", function (done) { + it('Can listen to compaction events', function (done) { d.on('compaction.done', function () { - d.removeAllListeners('compaction.done'); // Tidy up for next tests - done(); - }); - - d.persistence.compactDatafile(); - }); + d.removeAllListeners('compaction.done') // Tidy up for next tests + done() + }) + d.persistence.compactDatafile() + }) describe('Serialization hooks', function () { - var as = function (s) { return "before_" + s + "_after"; } - , bd = function (s) { return s.substring(7, s.length - 6); } + const as = function (s) { return 'before_' + s + '_after' } + const bd = function (s) { return s.substring(7, s.length - 6) } - it("Declaring only one hook will throw an exception to prevent data loss", function (done) { - var hookTestFilename = 'workspace/hookTest.db' + it('Declaring only one hook will throw an exception to prevent data loss', function (done) { + const hookTestFilename = 'workspace/hookTest.db' storage.ensureFileDoesntExist(hookTestFilename, function () { - fs.writeFileSync(hookTestFilename, "Some content", "utf8"); + fs.writeFileSync(hookTestFilename, 'Some content', 'utf8'); (function () { - new Datastore({ filename: hookTestFilename, autoload: true - , afterSerialization: as - }); - }).should.throw(); + // eslint-disable-next-line no-new + new Datastore({ + filename: hookTestFilename, + autoload: true, + afterSerialization: as + }) + }).should.throw() // Data file left untouched - fs.readFileSync(hookTestFilename, "utf8").should.equal("Some content"); + fs.readFileSync(hookTestFilename, 'utf8').should.equal('Some content'); (function () { - new Datastore({ filename: hookTestFilename, autoload: true - , beforeDeserialization: bd - }); - }).should.throw(); + // eslint-disable-next-line no-new + new Datastore({ + filename: hookTestFilename, + autoload: true, + beforeDeserialization: bd + }) + }).should.throw() // Data file left untouched - fs.readFileSync(hookTestFilename, "utf8").should.equal("Some content"); + fs.readFileSync(hookTestFilename, 'utf8').should.equal('Some content') - done(); - }); - }); + done() + }) + }) - it("Declaring two hooks that are not reverse of one another will cause an exception to prevent data loss", function (done) { - var hookTestFilename = 'workspace/hookTest.db' + it('Declaring two hooks that are not reverse of one another will cause an exception to prevent data loss', function (done) { + const hookTestFilename = 'workspace/hookTest.db' storage.ensureFileDoesntExist(hookTestFilename, function () { - fs.writeFileSync(hookTestFilename, "Some content", "utf8"); + fs.writeFileSync(hookTestFilename, 'Some content', 'utf8'); (function () { - new Datastore({ filename: hookTestFilename, autoload: true - , afterSerialization: as - , beforeDeserialization: function (s) { return s; } - }); - }).should.throw(); + // eslint-disable-next-line no-new + new Datastore({ + filename: hookTestFilename, + autoload: true, + afterSerialization: as, + beforeDeserialization: function (s) { return s } + }) + }).should.throw() // Data file left untouched - fs.readFileSync(hookTestFilename, "utf8").should.equal("Some content"); + fs.readFileSync(hookTestFilename, 'utf8').should.equal('Some content') - done(); - }); - }); + done() + }) + }) - it("A serialization hook can be used to transform data before writing new state to disk", function (done) { - var hookTestFilename = 'workspace/hookTest.db' + it('A serialization hook can be used to transform data before writing new state to disk', function (done) { + const hookTestFilename = 'workspace/hookTest.db' storage.ensureFileDoesntExist(hookTestFilename, function () { - var d = new Datastore({ filename: hookTestFilename, autoload: true - , afterSerialization: as - , beforeDeserialization: bd + const d = new Datastore({ + filename: hookTestFilename, + autoload: true, + afterSerialization: as, + beforeDeserialization: bd }) - ; - d.insert({ hello: "world" }, function () { - var _data = fs.readFileSync(hookTestFilename, 'utf8') - , data = _data.split('\n') - , doc0 = bd(data[0]) - ; + d.insert({ hello: 'world' }, function () { + const _data = fs.readFileSync(hookTestFilename, 'utf8') + const data = _data.split('\n') + let doc0 = bd(data[0]) - data.length.should.equal(2); + data.length.should.equal(2) - data[0].substring(0, 7).should.equal('before_'); - data[0].substring(data[0].length - 6).should.equal('_after'); + data[0].substring(0, 7).should.equal('before_') + data[0].substring(data[0].length - 6).should.equal('_after') - doc0 = model.deserialize(doc0); - Object.keys(doc0).length.should.equal(2); - doc0.hello.should.equal('world'); + doc0 = model.deserialize(doc0) + Object.keys(doc0).length.should.equal(2) + doc0.hello.should.equal('world') d.insert({ p: 'Mars' }, function () { - var _data = fs.readFileSync(hookTestFilename, 'utf8') - , data = _data.split('\n') - , doc0 = bd(data[0]) - , doc1 = bd(data[1]) - ; + const _data = fs.readFileSync(hookTestFilename, 'utf8') + const data = _data.split('\n') + let doc0 = bd(data[0]) + let doc1 = bd(data[1]) - data.length.should.equal(3); + data.length.should.equal(3) - data[0].substring(0, 7).should.equal('before_'); - data[0].substring(data[0].length - 6).should.equal('_after'); - data[1].substring(0, 7).should.equal('before_'); - data[1].substring(data[1].length - 6).should.equal('_after'); + data[0].substring(0, 7).should.equal('before_') + data[0].substring(data[0].length - 6).should.equal('_after') + data[1].substring(0, 7).should.equal('before_') + data[1].substring(data[1].length - 6).should.equal('_after') - doc0 = model.deserialize(doc0); - Object.keys(doc0).length.should.equal(2); - doc0.hello.should.equal('world'); + doc0 = model.deserialize(doc0) + Object.keys(doc0).length.should.equal(2) + doc0.hello.should.equal('world') - doc1 = model.deserialize(doc1); - Object.keys(doc1).length.should.equal(2); - doc1.p.should.equal('Mars'); + doc1 = model.deserialize(doc1) + Object.keys(doc1).length.should.equal(2) + doc1.p.should.equal('Mars') d.ensureIndex({ fieldName: 'idefix' }, function () { - var _data = fs.readFileSync(hookTestFilename, 'utf8') - , data = _data.split('\n') - , doc0 = bd(data[0]) - , doc1 = bd(data[1]) - , idx = bd(data[2]) - ; - - data.length.should.equal(4); - - data[0].substring(0, 7).should.equal('before_'); - data[0].substring(data[0].length - 6).should.equal('_after'); - data[1].substring(0, 7).should.equal('before_'); - data[1].substring(data[1].length - 6).should.equal('_after'); - - doc0 = model.deserialize(doc0); - Object.keys(doc0).length.should.equal(2); - doc0.hello.should.equal('world'); - - doc1 = model.deserialize(doc1); - Object.keys(doc1).length.should.equal(2); - doc1.p.should.equal('Mars'); - - idx = model.deserialize(idx); - assert.deepEqual(idx, { '$$indexCreated': { fieldName: 'idefix' } }); - - done(); - }); - }); - }); - }); - }); - - it("Use serialization hook when persisting cached database or compacting", function (done) { - var hookTestFilename = 'workspace/hookTest.db' + const _data = fs.readFileSync(hookTestFilename, 'utf8') + const data = _data.split('\n') + let doc0 = bd(data[0]) + let doc1 = bd(data[1]) + let idx = bd(data[2]) + + data.length.should.equal(4) + + data[0].substring(0, 7).should.equal('before_') + data[0].substring(data[0].length - 6).should.equal('_after') + data[1].substring(0, 7).should.equal('before_') + data[1].substring(data[1].length - 6).should.equal('_after') + + doc0 = model.deserialize(doc0) + Object.keys(doc0).length.should.equal(2) + doc0.hello.should.equal('world') + + doc1 = model.deserialize(doc1) + Object.keys(doc1).length.should.equal(2) + doc1.p.should.equal('Mars') + + idx = model.deserialize(idx) + assert.deepEqual(idx, { $$indexCreated: { fieldName: 'idefix' } }) + + done() + }) + }) + }) + }) + }) + + it('Use serialization hook when persisting cached database or compacting', function (done) { + const hookTestFilename = 'workspace/hookTest.db' storage.ensureFileDoesntExist(hookTestFilename, function () { - var d = new Datastore({ filename: hookTestFilename, autoload: true - , afterSerialization: as - , beforeDeserialization: bd + const d = new Datastore({ + filename: hookTestFilename, + autoload: true, + afterSerialization: as, + beforeDeserialization: bd }) - ; - d.insert({ hello: "world" }, function () { - d.update({ hello: "world" }, { $set: { hello: "earth" } }, {}, function () { + d.insert({ hello: 'world' }, function () { + d.update({ hello: 'world' }, { $set: { hello: 'earth' } }, {}, function () { d.ensureIndex({ fieldName: 'idefix' }, function () { - var _data = fs.readFileSync(hookTestFilename, 'utf8') - , data = _data.split('\n') - , doc0 = bd(data[0]) - , doc1 = bd(data[1]) - , idx = bd(data[2]) - , _id - ; + const _data = fs.readFileSync(hookTestFilename, 'utf8') + const data = _data.split('\n') + let doc0 = bd(data[0]) + let doc1 = bd(data[1]) + let idx = bd(data[2]) - data.length.should.equal(4); + data.length.should.equal(4) - doc0 = model.deserialize(doc0); - Object.keys(doc0).length.should.equal(2); - doc0.hello.should.equal('world'); + doc0 = model.deserialize(doc0) + Object.keys(doc0).length.should.equal(2) + doc0.hello.should.equal('world') - doc1 = model.deserialize(doc1); - Object.keys(doc1).length.should.equal(2); - doc1.hello.should.equal('earth'); + doc1 = model.deserialize(doc1) + Object.keys(doc1).length.should.equal(2) + doc1.hello.should.equal('earth') - doc0._id.should.equal(doc1._id); - _id = doc0._id; + doc0._id.should.equal(doc1._id) + const _id = doc0._id - idx = model.deserialize(idx); - assert.deepEqual(idx, { '$$indexCreated': { fieldName: 'idefix' } }); + idx = model.deserialize(idx) + assert.deepEqual(idx, { $$indexCreated: { fieldName: 'idefix' } }) d.persistence.persistCachedDatabase(function () { - var _data = fs.readFileSync(hookTestFilename, 'utf8') - , data = _data.split('\n') - , doc0 = bd(data[0]) - , idx = bd(data[1]) - ; + const _data = fs.readFileSync(hookTestFilename, 'utf8') + const data = _data.split('\n') + let doc0 = bd(data[0]) + let idx = bd(data[1]) - data.length.should.equal(3); + data.length.should.equal(3) - doc0 = model.deserialize(doc0); - Object.keys(doc0).length.should.equal(2); - doc0.hello.should.equal('earth'); + doc0 = model.deserialize(doc0) + Object.keys(doc0).length.should.equal(2) + doc0.hello.should.equal('earth') - doc0._id.should.equal(_id); + doc0._id.should.equal(_id) - idx = model.deserialize(idx); - assert.deepEqual(idx, { '$$indexCreated': { fieldName: 'idefix', unique: false, sparse: false } }); + idx = model.deserialize(idx) + assert.deepEqual(idx, { $$indexCreated: { fieldName: 'idefix', unique: false, sparse: false } }) - done(); - }); - }); - }); - }); - }); - }); + done() + }) + }) + }) + }) + }) + }) - it("Deserialization hook is correctly used when loading data", function (done) { - var hookTestFilename = 'workspace/hookTest.db' + it('Deserialization hook is correctly used when loading data', function (done) { + const hookTestFilename = 'workspace/hookTest.db' storage.ensureFileDoesntExist(hookTestFilename, function () { - var d = new Datastore({ filename: hookTestFilename, autoload: true - , afterSerialization: as - , beforeDeserialization: bd + const d = new Datastore({ + filename: hookTestFilename, + autoload: true, + afterSerialization: as, + beforeDeserialization: bd }) - ; - d.insert({ hello: "world" }, function (err, doc) { - var _id = doc._id; - d.insert({ yo: "ya" }, function () { - d.update({ hello: "world" }, { $set: { hello: "earth" } }, {}, function () { - d.remove({ yo: "ya" }, {}, function () { + // eslint-disable-next-line node/handle-callback-err + d.insert({ hello: 'world' }, function (err, doc) { + const _id = doc._id + d.insert({ yo: 'ya' }, function () { + d.update({ hello: 'world' }, { $set: { hello: 'earth' } }, {}, function () { + d.remove({ yo: 'ya' }, {}, function () { d.ensureIndex({ fieldName: 'idefix' }, function () { - var _data = fs.readFileSync(hookTestFilename, 'utf8') - , data = _data.split('\n') - ; + const _data = fs.readFileSync(hookTestFilename, 'utf8') + const data = _data.split('\n') - data.length.should.equal(6); + data.length.should.equal(6) // Everything is deserialized correctly, including deletes and indexes - var d = new Datastore({ filename: hookTestFilename - , afterSerialization: as - , beforeDeserialization: bd + const d = new Datastore({ + filename: hookTestFilename, + afterSerialization: as, + beforeDeserialization: bd }) - ; d.loadDatabase(function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); - docs[0].hello.should.equal("earth"); - docs[0]._id.should.equal(_id); - - Object.keys(d.indexes).length.should.equal(2); - Object.keys(d.indexes).indexOf("idefix").should.not.equal(-1); - - done(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - - }); // ==== End of 'Serialization hooks' ==== // + docs.length.should.equal(1) + docs[0].hello.should.equal('earth') + docs[0]._id.should.equal(_id) - describe('Prevent dataloss when persisting data', function () { + Object.keys(d.indexes).length.should.equal(2) + Object.keys(d.indexes).indexOf('idefix').should.not.equal(-1) + done() + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) // ==== End of 'Serialization hooks' ==== // + + describe('Prevent dataloss when persisting data', function () { it('Creating a datastore with in memory as true and a bad filename wont cause an error', function () { - new Datastore({ filename: 'workspace/bad.db~', inMemoryOnly: true }); + // eslint-disable-next-line no-new + new Datastore({ filename: 'workspace/bad.db~', inMemoryOnly: true }) }) it('Creating a persistent datastore with a bad filename will cause an error', function () { - (function () { new Datastore({ filename: 'workspace/bad.db~' }); }).should.throw(); + // eslint-disable-next-line no-new + (function () { new Datastore({ filename: 'workspace/bad.db~' }) }).should.throw() }) - it('If no file exists, ensureDatafileIntegrity creates an empty datafile', function (done) { - var p = new Persistence({ db: { inMemoryOnly: false, filename: 'workspace/it.db' } }); + it('If no file stat, ensureDatafileIntegrity creates an empty datafile', function (done) { + const p = new Persistence({ db: { inMemoryOnly: false, filename: 'workspace/it.db' } }) - if (fs.existsSync('workspace/it.db')) { fs.unlinkSync('workspace/it.db'); } - if (fs.existsSync('workspace/it.db~')) { fs.unlinkSync('workspace/it.db~'); } + if (fs.existsSync('workspace/it.db')) { fs.unlinkSync('workspace/it.db') } + if (fs.existsSync('workspace/it.db~')) { fs.unlinkSync('workspace/it.db~') } - fs.existsSync('workspace/it.db').should.equal(false); - fs.existsSync('workspace/it.db~').should.equal(false); + fs.existsSync('workspace/it.db').should.equal(false) + fs.existsSync('workspace/it.db~').should.equal(false) storage.ensureDatafileIntegrity(p.filename, function (err) { - assert.isNull(err); + assert.isNull(err) - fs.existsSync('workspace/it.db').should.equal(true); - fs.existsSync('workspace/it.db~').should.equal(false); + fs.existsSync('workspace/it.db').should.equal(true) + fs.existsSync('workspace/it.db~').should.equal(false) - fs.readFileSync('workspace/it.db', 'utf8').should.equal(''); + fs.readFileSync('workspace/it.db', 'utf8').should.equal('') - done(); - }); - }); + done() + }) + }) - it('If only datafile exists, ensureDatafileIntegrity will use it', function (done) { - var p = new Persistence({ db: { inMemoryOnly: false, filename: 'workspace/it.db' } }); + it('If only datafile stat, ensureDatafileIntegrity will use it', function (done) { + const p = new Persistence({ db: { inMemoryOnly: false, filename: 'workspace/it.db' } }) - if (fs.existsSync('workspace/it.db')) { fs.unlinkSync('workspace/it.db'); } - if (fs.existsSync('workspace/it.db~')) { fs.unlinkSync('workspace/it.db~'); } + if (fs.existsSync('workspace/it.db')) { fs.unlinkSync('workspace/it.db') } + if (fs.existsSync('workspace/it.db~')) { fs.unlinkSync('workspace/it.db~') } - fs.writeFileSync('workspace/it.db', 'something', 'utf8'); + fs.writeFileSync('workspace/it.db', 'something', 'utf8') - fs.existsSync('workspace/it.db').should.equal(true); - fs.existsSync('workspace/it.db~').should.equal(false); + fs.existsSync('workspace/it.db').should.equal(true) + fs.existsSync('workspace/it.db~').should.equal(false) storage.ensureDatafileIntegrity(p.filename, function (err) { - assert.isNull(err); + assert.isNull(err) - fs.existsSync('workspace/it.db').should.equal(true); - fs.existsSync('workspace/it.db~').should.equal(false); + fs.existsSync('workspace/it.db').should.equal(true) + fs.existsSync('workspace/it.db~').should.equal(false) - fs.readFileSync('workspace/it.db', 'utf8').should.equal('something'); + fs.readFileSync('workspace/it.db', 'utf8').should.equal('something') - done(); - }); - }); + done() + }) + }) - it('If temp datafile exists and datafile doesnt, ensureDatafileIntegrity will use it (cannot happen except upon first use)', function (done) { - var p = new Persistence({ db: { inMemoryOnly: false, filename: 'workspace/it.db' } }); + it('If temp datafile stat and datafile doesnt, ensureDatafileIntegrity will use it (cannot happen except upon first use)', function (done) { + const p = new Persistence({ db: { inMemoryOnly: false, filename: 'workspace/it.db' } }) - if (fs.existsSync('workspace/it.db')) { fs.unlinkSync('workspace/it.db'); } - if (fs.existsSync('workspace/it.db~')) { fs.unlinkSync('workspace/it.db~~'); } + if (fs.existsSync('workspace/it.db')) { fs.unlinkSync('workspace/it.db') } + if (fs.existsSync('workspace/it.db~')) { fs.unlinkSync('workspace/it.db~~') } - fs.writeFileSync('workspace/it.db~', 'something', 'utf8'); + fs.writeFileSync('workspace/it.db~', 'something', 'utf8') - fs.existsSync('workspace/it.db').should.equal(false); - fs.existsSync('workspace/it.db~').should.equal(true); + fs.existsSync('workspace/it.db').should.equal(false) + fs.existsSync('workspace/it.db~').should.equal(true) storage.ensureDatafileIntegrity(p.filename, function (err) { - assert.isNull(err); + assert.isNull(err) - fs.existsSync('workspace/it.db').should.equal(true); - fs.existsSync('workspace/it.db~').should.equal(false); + fs.existsSync('workspace/it.db').should.equal(true) + fs.existsSync('workspace/it.db~').should.equal(false) - fs.readFileSync('workspace/it.db', 'utf8').should.equal('something'); + fs.readFileSync('workspace/it.db', 'utf8').should.equal('something') - done(); - }); - }); + done() + }) + }) // Technically it could also mean the write was successful but the rename wasn't, but there is in any case no guarantee that the data in the temp file is whole so we have to discard the whole file it('If both temp and current datafiles exist, ensureDatafileIntegrity will use the datafile, as it means that the write of the temp file failed', function (done) { - var theDb = new Datastore({ filename: 'workspace/it.db' }); + const theDb = new Datastore({ filename: 'workspace/it.db' }) - if (fs.existsSync('workspace/it.db')) { fs.unlinkSync('workspace/it.db'); } - if (fs.existsSync('workspace/it.db~')) { fs.unlinkSync('workspace/it.db~'); } + if (fs.existsSync('workspace/it.db')) { fs.unlinkSync('workspace/it.db') } + if (fs.existsSync('workspace/it.db~')) { fs.unlinkSync('workspace/it.db~') } - fs.writeFileSync('workspace/it.db', '{"_id":"0","hello":"world"}', 'utf8'); - fs.writeFileSync('workspace/it.db~', '{"_id":"0","hello":"other"}', 'utf8'); + fs.writeFileSync('workspace/it.db', '{"_id":"0","hello":"world"}', 'utf8') + fs.writeFileSync('workspace/it.db~', '{"_id":"0","hello":"other"}', 'utf8') - fs.existsSync('workspace/it.db').should.equal(true); - fs.existsSync('workspace/it.db~').should.equal(true); + fs.existsSync('workspace/it.db').should.equal(true) + fs.existsSync('workspace/it.db~').should.equal(true) storage.ensureDatafileIntegrity(theDb.persistence.filename, function (err) { - assert.isNull(err); + assert.isNull(err) - fs.existsSync('workspace/it.db').should.equal(true); - fs.existsSync('workspace/it.db~').should.equal(true); + fs.existsSync('workspace/it.db').should.equal(true) + fs.existsSync('workspace/it.db~').should.equal(true) - fs.readFileSync('workspace/it.db', 'utf8').should.equal('{"_id":"0","hello":"world"}'); + fs.readFileSync('workspace/it.db', 'utf8').should.equal('{"_id":"0","hello":"world"}') theDb.loadDatabase(function (err) { - assert.isNull(err); + assert.isNull(err) theDb.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(1); - docs[0].hello.should.equal("world"); - fs.existsSync('workspace/it.db').should.equal(true); - fs.existsSync('workspace/it.db~').should.equal(false); - done(); - }); - }); - }); - }); + assert.isNull(err) + docs.length.should.equal(1) + docs[0].hello.should.equal('world') + fs.existsSync('workspace/it.db').should.equal(true) + fs.existsSync('workspace/it.db~').should.equal(false) + done() + }) + }) + }) + }) it('persistCachedDatabase should update the contents of the datafile and leave a clean state', function (done) { d.insert({ hello: 'world' }, function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); + docs.length.should.equal(1) - if (fs.existsSync(testDb)) { fs.unlinkSync(testDb); } - if (fs.existsSync(testDb + '~')) { fs.unlinkSync(testDb + '~'); } - fs.existsSync(testDb).should.equal(false); + if (fs.existsSync(testDb)) { fs.unlinkSync(testDb) } + if (fs.existsSync(testDb + '~')) { fs.unlinkSync(testDb + '~') } + fs.existsSync(testDb).should.equal(false) - fs.writeFileSync(testDb + '~', 'something', 'utf8'); - fs.existsSync(testDb + '~').should.equal(true); + fs.writeFileSync(testDb + '~', 'something', 'utf8') + fs.existsSync(testDb + '~').should.equal(true) d.persistence.persistCachedDatabase(function (err) { - var contents = fs.readFileSync(testDb, 'utf8'); - assert.isNull(err); - fs.existsSync(testDb).should.equal(true); - fs.existsSync(testDb + '~').should.equal(false); + const contents = fs.readFileSync(testDb, 'utf8') + assert.isNull(err) + fs.existsSync(testDb).should.equal(true) + fs.existsSync(testDb + '~').should.equal(false) if (!contents.match(/^{"hello":"world","_id":"[0-9a-zA-Z]{16}"}\n$/)) { - throw new Error("Datafile contents not as expected"); + throw new Error('Datafile contents not as expected') } - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('After a persistCachedDatabase, there should be no temp or old filename', function (done) { d.insert({ hello: 'world' }, function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); + docs.length.should.equal(1) - if (fs.existsSync(testDb)) { fs.unlinkSync(testDb); } - if (fs.existsSync(testDb + '~')) { fs.unlinkSync(testDb + '~'); } - fs.existsSync(testDb).should.equal(false); - fs.existsSync(testDb + '~').should.equal(false); + if (fs.existsSync(testDb)) { fs.unlinkSync(testDb) } + if (fs.existsSync(testDb + '~')) { fs.unlinkSync(testDb + '~') } + fs.existsSync(testDb).should.equal(false) + fs.existsSync(testDb + '~').should.equal(false) - fs.writeFileSync(testDb + '~', 'bloup', 'utf8'); - fs.existsSync(testDb + '~').should.equal(true); + fs.writeFileSync(testDb + '~', 'bloup', 'utf8') + fs.existsSync(testDb + '~').should.equal(true) d.persistence.persistCachedDatabase(function (err) { - var contents = fs.readFileSync(testDb, 'utf8'); - assert.isNull(err); - fs.existsSync(testDb).should.equal(true); - fs.existsSync(testDb + '~').should.equal(false); + const contents = fs.readFileSync(testDb, 'utf8') + assert.isNull(err) + fs.existsSync(testDb).should.equal(true) + fs.existsSync(testDb + '~').should.equal(false) if (!contents.match(/^{"hello":"world","_id":"[0-9a-zA-Z]{16}"}\n$/)) { - throw new Error("Datafile contents not as expected"); + throw new Error('Datafile contents not as expected') } - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('persistCachedDatabase should update the contents of the datafile and leave a clean state even if there is a temp datafile', function (done) { d.insert({ hello: 'world' }, function () { + // eslint-disable-next-line node/handle-callback-err d.find({}, function (err, docs) { - docs.length.should.equal(1); + docs.length.should.equal(1) - if (fs.existsSync(testDb)) { fs.unlinkSync(testDb); } - fs.writeFileSync(testDb + '~', 'blabla', 'utf8'); - fs.existsSync(testDb).should.equal(false); - fs.existsSync(testDb + '~').should.equal(true); + if (fs.existsSync(testDb)) { fs.unlinkSync(testDb) } + fs.writeFileSync(testDb + '~', 'blabla', 'utf8') + fs.existsSync(testDb).should.equal(false) + fs.existsSync(testDb + '~').should.equal(true) d.persistence.persistCachedDatabase(function (err) { - var contents = fs.readFileSync(testDb, 'utf8'); - assert.isNull(err); - fs.existsSync(testDb).should.equal(true); - fs.existsSync(testDb + '~').should.equal(false); + const contents = fs.readFileSync(testDb, 'utf8') + assert.isNull(err) + fs.existsSync(testDb).should.equal(true) + fs.existsSync(testDb + '~').should.equal(false) if (!contents.match(/^{"hello":"world","_id":"[0-9a-zA-Z]{16}"}\n$/)) { - throw new Error("Datafile contents not as expected"); + throw new Error('Datafile contents not as expected') } - done(); - }); - }); - }); - }); + done() + }) + }) + }) + }) it('persistCachedDatabase should update the contents of the datafile and leave a clean state even if there is a temp datafile', function (done) { - var dbFile = 'workspace/test2.db', theDb; + const dbFile = 'workspace/test2.db' - if (fs.existsSync(dbFile)) { fs.unlinkSync(dbFile); } - if (fs.existsSync(dbFile + '~')) { fs.unlinkSync(dbFile + '~'); } + if (fs.existsSync(dbFile)) { fs.unlinkSync(dbFile) } + if (fs.existsSync(dbFile + '~')) { fs.unlinkSync(dbFile + '~') } - theDb = new Datastore({ filename: dbFile }); + const theDb = new Datastore({ filename: dbFile }) theDb.loadDatabase(function (err) { - var contents = fs.readFileSync(dbFile, 'utf8'); - assert.isNull(err); - fs.existsSync(dbFile).should.equal(true); - fs.existsSync(dbFile + '~').should.equal(false); - if (contents != "") { - throw new Error("Datafile contents not as expected"); + const contents = fs.readFileSync(dbFile, 'utf8') + assert.isNull(err) + fs.existsSync(dbFile).should.equal(true) + fs.existsSync(dbFile + '~').should.equal(false) + if (contents !== '') { + throw new Error('Datafile contents not as expected') } - done(); - }); - }); + done() + }) + }) it('Persistence works as expected when everything goes fine', function (done) { - var dbFile = 'workspace/test2.db', theDb, theDb2, doc1, doc2; + const dbFile = 'workspace/test2.db' + let theDb, theDb2, doc1, doc2 async.waterfall([ - async.apply(storage.ensureFileDoesntExist, dbFile) - , async.apply(storage.ensureFileDoesntExist, dbFile + '~') - , function (cb) { - theDb = new Datastore({ filename: dbFile }); - theDb.loadDatabase(cb); - } - , function (cb) { + async.apply(storage.ensureFileDoesntExist, dbFile), + async.apply(storage.ensureFileDoesntExist, dbFile + '~'), + function (cb) { + theDb = new Datastore({ filename: dbFile }) + theDb.loadDatabase(cb) + }, + function (cb) { + theDb.find({}, function (err, docs) { + assert.isNull(err) + docs.length.should.equal(0) + return cb() + }) + }, + function (cb) { + theDb.insert({ a: 'hello' }, function (err, _doc1) { + assert.isNull(err) + doc1 = _doc1 + theDb.insert({ a: 'world' }, function (err, _doc2) { + assert.isNull(err) + doc2 = _doc2 + return cb() + }) + }) + }, + function (cb) { theDb.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(0); - return cb(); - }); + assert.isNull(err) + docs.length.should.equal(2) + _.find(docs, function (item) { return item._id === doc1._id }).a.should.equal('hello') + _.find(docs, function (item) { return item._id === doc2._id }).a.should.equal('world') + return cb() + }) + }, + function (cb) { + theDb.loadDatabase(cb) + }, + function (cb) { // No change + theDb.find({}, function (err, docs) { + assert.isNull(err) + docs.length.should.equal(2) + _.find(docs, function (item) { return item._id === doc1._id }).a.should.equal('hello') + _.find(docs, function (item) { return item._id === doc2._id }).a.should.equal('world') + return cb() + }) + }, + function (cb) { + fs.existsSync(dbFile).should.equal(true) + fs.existsSync(dbFile + '~').should.equal(false) + return cb() + }, + function (cb) { + theDb2 = new Datastore({ filename: dbFile }) + theDb2.loadDatabase(cb) + }, + function (cb) { // No change in second db + theDb2.find({}, function (err, docs) { + assert.isNull(err) + docs.length.should.equal(2) + _.find(docs, function (item) { return item._id === doc1._id }).a.should.equal('hello') + _.find(docs, function (item) { return item._id === doc2._id }).a.should.equal('world') + return cb() + }) + }, + function (cb) { + fs.existsSync(dbFile).should.equal(true) + fs.existsSync(dbFile + '~').should.equal(false) + return cb() } - , function (cb) { - theDb.insert({ a: 'hello' }, function (err, _doc1) { - assert.isNull(err); - doc1 = _doc1; - theDb.insert({ a: 'world' }, function (err, _doc2) { - assert.isNull(err); - doc2 = _doc2; - return cb(); - }); - }); - } - , function (cb) { - theDb.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - _.find(docs, function (item) { return item._id === doc1._id }).a.should.equal('hello'); - _.find(docs, function (item) { return item._id === doc2._id }).a.should.equal('world'); - return cb(); - }); - } - , function (cb) { - theDb.loadDatabase(cb); - } - , function (cb) { // No change - theDb.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - _.find(docs, function (item) { return item._id === doc1._id }).a.should.equal('hello'); - _.find(docs, function (item) { return item._id === doc2._id }).a.should.equal('world'); - return cb(); - }); - } - , function (cb) { - fs.existsSync(dbFile).should.equal(true); - fs.existsSync(dbFile + '~').should.equal(false); - return cb(); - } - , function (cb) { - theDb2 = new Datastore({ filename: dbFile }); - theDb2.loadDatabase(cb); - } - , function (cb) { // No change in second db - theDb2.find({}, function (err, docs) { - assert.isNull(err); - docs.length.should.equal(2); - _.find(docs, function (item) { return item._id === doc1._id }).a.should.equal('hello'); - _.find(docs, function (item) { return item._id === doc2._id }).a.should.equal('world'); - return cb(); - }); - } - , function (cb) { - fs.existsSync(dbFile).should.equal(true); - fs.existsSync(dbFile + '~').should.equal(false); - return cb(); - } - ], done); - }); + ], done) + }) // The child process will load the database with the given datafile, but the fs.writeFile function // is rewritten to crash the process before it finished (after 5000 bytes), to ensure data was not lost it('If system crashes during a loadDatabase, the former version is not lost', function (done) { - var N = 500, toWrite = "", i, doc_i; + const N = 500 + let toWrite = '' + let i + let docI // Ensuring the state is clean - if (fs.existsSync('workspace/lac.db')) { fs.unlinkSync('workspace/lac.db'); } - if (fs.existsSync('workspace/lac.db~')) { fs.unlinkSync('workspace/lac.db~'); } + if (fs.existsSync('workspace/lac.db')) { fs.unlinkSync('workspace/lac.db') } + if (fs.existsSync('workspace/lac.db~')) { fs.unlinkSync('workspace/lac.db~') } // Creating a db file with 150k records (a bit long to load) for (i = 0; i < N; i += 1) { - toWrite += model.serialize({ _id: 'anid_' + i, hello: 'world' }) + '\n'; + toWrite += model.serialize({ _id: 'anid_' + i, hello: 'world' }) + '\n' } - fs.writeFileSync('workspace/lac.db', toWrite, 'utf8'); + fs.writeFileSync('workspace/lac.db', toWrite, 'utf8') - var datafileLength = fs.readFileSync('workspace/lac.db', 'utf8').length; + const datafileLength = fs.readFileSync('workspace/lac.db', 'utf8').length // Loading it in a separate process that we will crash before finishing the loadDatabase - child_process.fork('test_lac/loadAndCrash.test').on('exit', function (code) { - code.should.equal(1); // See test_lac/loadAndCrash.test.js + fork('test_lac/loadAndCrash.test').on('exit', function (code) { + code.should.equal(1) // See test_lac/loadAndCrash.test.js - fs.existsSync('workspace/lac.db').should.equal(true); - fs.existsSync('workspace/lac.db~').should.equal(true); - fs.readFileSync('workspace/lac.db', 'utf8').length.should.equal(datafileLength); - fs.readFileSync('workspace/lac.db~', 'utf8').length.should.equal(5000); + fs.existsSync('workspace/lac.db').should.equal(true) + fs.existsSync('workspace/lac.db~').should.equal(true) + fs.readFileSync('workspace/lac.db', 'utf8').length.should.equal(datafileLength) + fs.readFileSync('workspace/lac.db~', 'utf8').length.should.equal(5000) // Reload database without a crash, check that no data was lost and fs state is clean (no temp file) - var db = new Datastore({ filename: 'workspace/lac.db' }); + const db = new Datastore({ filename: 'workspace/lac.db' }) db.loadDatabase(function (err) { - assert.isNull(err); + assert.isNull(err) - fs.existsSync('workspace/lac.db').should.equal(true); - fs.existsSync('workspace/lac.db~').should.equal(false); - fs.readFileSync('workspace/lac.db', 'utf8').length.should.equal(datafileLength); + fs.existsSync('workspace/lac.db').should.equal(true) + fs.existsSync('workspace/lac.db~').should.equal(false) + fs.readFileSync('workspace/lac.db', 'utf8').length.should.equal(datafileLength) + // eslint-disable-next-line node/handle-callback-err db.find({}, function (err, docs) { - docs.length.should.equal(N); + docs.length.should.equal(N) for (i = 0; i < N; i += 1) { - doc_i = _.find(docs, function (d) { return d._id === 'anid_' + i; }); - assert.isDefined(doc_i); - assert.deepEqual({ hello: 'world', _id: 'anid_' + i }, doc_i); + docI = _.find(docs, function (d) { return d._id === 'anid_' + i }) + assert.isDefined(docI) + assert.deepEqual({ hello: 'world', _id: 'anid_' + i }, docI) } - return done(); - }); - }); - }); - }); + return done() + }) + }) + }) + }) // Not run on Windows as there is no clean way to set maximum file descriptors. Not an issue as the code itself is tested. - it("Cannot cause EMFILE errors by opening too many file descriptors", function (done) { - if (process.platform === 'win32' || process.platform === 'win64') { return done(); } - child_process.execFile('test_lac/openFdsLaunch.sh', function (err, stdout, stderr) { - if (err) { return done(err); } + it('Cannot cause EMFILE errors by opening too many file descriptors', function (done) { + if (process.platform === 'win32' || process.platform === 'win64') { return done() } + execFile('test_lac/openFdsLaunch.sh', function (err, stdout, stderr) { + if (err) { return done(err) } // The subprocess will not output anything to stdout unless part of the test fails if (stdout.length !== 0) { - return done(stdout); + return done(stdout) } else { - return done(); + return done() } - }); - }); - - }); // ==== End of 'Prevent dataloss when persisting data' ==== - + }) + }) + }) // ==== End of 'Prevent dataloss when persisting data' ==== describe('ensureFileDoesntExist', function () { - it('Doesnt do anything if file already doesnt exist', function (done) { storage.ensureFileDoesntExist('workspace/nonexisting', function (err) { - assert.isNull(err); - fs.existsSync('workspace/nonexisting').should.equal(false); - done(); - }); - }); + assert.isNull(err) + fs.existsSync('workspace/nonexisting').should.equal(false) + done() + }) + }) - it('Deletes file if it exists', function (done) { - fs.writeFileSync('workspace/existing', 'hello world', 'utf8'); - fs.existsSync('workspace/existing').should.equal(true); + it('Deletes file if it stat', function (done) { + fs.writeFileSync('workspace/existing', 'hello world', 'utf8') + fs.existsSync('workspace/existing').should.equal(true) storage.ensureFileDoesntExist('workspace/existing', function (err) { - assert.isNull(err); - fs.existsSync('workspace/existing').should.equal(false); - done(); - }); - }); - - }); // ==== End of 'ensureFileDoesntExist' ==== - - -}); + assert.isNull(err) + fs.existsSync('workspace/existing').should.equal(false) + done() + }) + }) + }) // ==== End of 'ensureFileDoesntExist' ==== +}) diff --git a/test_lac/loadAndCrash.test.js b/test_lac/loadAndCrash.test.js index cbe9bc3..f77de87 100755 --- a/test_lac/loadAndCrash.test.js +++ b/test_lac/loadAndCrash.test.js @@ -1,123 +1,121 @@ +/* eslint-env mocha */ +/* global DEBUG */ /** * Load and modify part of fs to ensure writeFile will crash after writing 5000 bytes */ -var fs = require('fs'); +const fs = require('fs') -function rethrow() { +function rethrow () { // Only enable in debug mode. A backtrace uses ~1000 bytes of heap space and // is fairly slow to generate. if (DEBUG) { - var backtrace = new Error(); - return function(err) { + const backtrace = new Error() + return function (err) { if (err) { backtrace.stack = err.name + ': ' + err.message + - backtrace.stack.substr(backtrace.name.length); - throw backtrace; + backtrace.stack.substr(backtrace.name.length) + throw backtrace } - }; + } } - return function(err) { + return function (err) { if (err) { - throw err; // Forgot a callback but don't know where? Use NODE_DEBUG=fs + throw err // Forgot a callback but don't know where? Use NODE_DEBUG=fs } - }; + } } -function maybeCallback(cb) { - return typeof cb === 'function' ? cb : rethrow(); +function maybeCallback (cb) { + return typeof cb === 'function' ? cb : rethrow() } -function isFd(path) { - return (path >>> 0) === path; +function isFd (path) { + return (path >>> 0) === path } -function assertEncoding(encoding) { +function assertEncoding (encoding) { if (encoding && !Buffer.isEncoding(encoding)) { - throw new Error('Unknown encoding: ' + encoding); + throw new Error('Unknown encoding: ' + encoding) } } -var onePassDone = false; -function writeAll(fd, isUserFd, buffer, offset, length, position, callback_) { - var callback = maybeCallback(arguments[arguments.length - 1]); +let onePassDone = false - if (onePassDone) { process.exit(1); } // Crash on purpose before rewrite done - var l = Math.min(5000, length); // Force write by chunks of 5000 bytes to ensure data will be incomplete on crash +function writeAll (fd, isUserFd, buffer, offset, length, position, callback_) { + const callback = maybeCallback(arguments[arguments.length - 1]) + + if (onePassDone) { process.exit(1) } // Crash on purpose before rewrite done + const l = Math.min(5000, length) // Force write by chunks of 5000 bytes to ensure data will be incomplete on crash // write(fd, buffer, offset, length, position, callback) - fs.write(fd, buffer, offset, l, position, function(writeErr, written) { + fs.write(fd, buffer, offset, l, position, function (writeErr, written) { if (writeErr) { if (isUserFd) { - if (callback) callback(writeErr); + if (callback) callback(writeErr) } else { - fs.close(fd, function() { - if (callback) callback(writeErr); - }); + fs.close(fd, function () { + if (callback) callback(writeErr) + }) } } else { - onePassDone = true; + onePassDone = true if (written === length) { if (isUserFd) { - if (callback) callback(null); + if (callback) callback(null) } else { - fs.close(fd, callback); + fs.close(fd, callback) } } else { - offset += written; - length -= written; + offset += written + length -= written if (position !== null) { - position += written; + position += written } - writeAll(fd, isUserFd, buffer, offset, length, position, callback); + writeAll(fd, isUserFd, buffer, offset, length, position, callback) } } - }); + }) } -fs.writeFile = function(path, data, options, callback_) { - var callback = maybeCallback(arguments[arguments.length - 1]); +fs.writeFile = function (path, data, options, callback_) { + const callback = maybeCallback(arguments[arguments.length - 1]) if (!options || typeof options === 'function') { - options = { encoding: 'utf8', mode: 438, flag: 'w' }; // Mode 438 == 0o666 (compatibility with older Node releases) + options = { encoding: 'utf8', mode: 438, flag: 'w' } // Mode 438 == 0o666 (compatibility with older Node releases) } else if (typeof options === 'string') { - options = { encoding: options, mode: 438, flag: 'w' }; // Mode 438 == 0o666 (compatibility with older Node releases) + options = { encoding: options, mode: 438, flag: 'w' } // Mode 438 == 0o666 (compatibility with older Node releases) } else if (typeof options !== 'object') { - throwOptionsError(options); + throw new Error(`throwOptionsError${options}`) } - assertEncoding(options.encoding); + assertEncoding(options.encoding) - var flag = options.flag || 'w'; + const flag = options.flag || 'w' if (isFd(path)) { - writeFd(path, true); - return; + writeFd(path, true) + return } - fs.open(path, flag, options.mode, function(openErr, fd) { + fs.open(path, flag, options.mode, function (openErr, fd) { if (openErr) { - if (callback) callback(openErr); + if (callback) callback(openErr) } else { - writeFd(fd, false); + writeFd(fd, false) } - }); + }) - function writeFd(fd, isUserFd) { - var buffer = (data instanceof Buffer) ? data : new Buffer('' + data, - options.encoding || 'utf8'); - var position = /a/.test(flag) ? null : 0; + function writeFd (fd, isUserFd) { + const buffer = (data instanceof Buffer) ? data : Buffer.from('' + data, options.encoding || 'utf8') + const position = /a/.test(flag) ? null : 0 - writeAll(fd, isUserFd, buffer, 0, buffer.length, position, callback); + writeAll(fd, isUserFd, buffer, 0, buffer.length, position, callback) } -}; - - - +} // End of fs modification -var Nedb = require('../lib/datastore.js') - , db = new Nedb({ filename: 'workspace/lac.db' }) - ; +const Nedb = require('../lib/datastore.js') +const db = new Nedb({ filename: 'workspace/lac.db' }) -db.loadDatabase(); +db.loadDatabase() diff --git a/test_lac/openFds.test.js b/test_lac/openFds.test.js index 2e81805..b93c06d 100644 --- a/test_lac/openFds.test.js +++ b/test_lac/openFds.test.js @@ -1,67 +1,64 @@ -var fs = require('fs') - , child_process = require('child_process') - , async = require('async') - , Nedb = require('../lib/datastore') - , db = new Nedb({ filename: './workspace/openfds.db', autoload: true }) - , N = 64 // Half the allowed file descriptors - , i, fds - ; +const fs = require('fs') +const async = require('async') +const Nedb = require('../lib/datastore') +const db = new Nedb({ filename: './workspace/openfds.db', autoload: true }) +const N = 64 +let i +let fds function multipleOpen (filename, N, callback) { - async.whilst( function () { return i < N; } - , function (cb) { - fs.open(filename, 'r', function (err, fd) { - i += 1; - if (fd) { fds.push(fd); } - return cb(err); - }); - } - , callback); + async.whilst(function () { return i < N } + , function (cb) { + fs.open(filename, 'r', function (err, fd) { + i += 1 + if (fd) { fds.push(fd) } + return cb(err) + }) + } + , callback) } async.waterfall([ // Check that ulimit has been set to the correct value function (cb) { - i = 0; - fds = []; + i = 0 + fds = [] multipleOpen('./test_lac/openFdsTestFile', 2 * N + 1, function (err) { - if (!err) { console.log("No error occured while opening a file too many times"); } - fds.forEach(function (fd) { fs.closeSync(fd); }); - return cb(); + if (!err) { console.log('No error occured while opening a file too many times') } + fds.forEach(function (fd) { fs.closeSync(fd) }) + return cb() }) - } -, function (cb) { - i = 0; - fds = []; + }, + function (cb) { + i = 0 + fds = [] multipleOpen('./test_lac/openFdsTestFile2', N, function (err) { - if (err) { console.log('An unexpected error occured when opening file not too many times: ' + err); } - fds.forEach(function (fd) { fs.closeSync(fd); }); - return cb(); + if (err) { console.log('An unexpected error occured when opening file not too many times: ' + err) } + fds.forEach(function (fd) { fs.closeSync(fd) }) + return cb() }) - } + }, // Then actually test NeDB persistence -, function () { + function () { db.remove({}, { multi: true }, function (err) { - if (err) { console.log(err); } + if (err) { console.log(err) } db.insert({ hello: 'world' }, function (err) { - if (err) { console.log(err); } + if (err) { console.log(err) } - i = 0; - async.whilst( function () { return i < 2 * N + 1; } - , function (cb) { - db.persistence.persistCachedDatabase(function (err) { - if (err) { return cb(err); } - i += 1; - return cb(); - }); - } - , function (err) { - if (err) { console.log("Got unexpected error during one peresistence operation: " + err); } - } - ); - - }); - }); + i = 0 + async.whilst(function () { return i < 2 * N + 1 } + , function (cb) { + db.persistence.persistCachedDatabase(function (err) { + if (err) { return cb(err) } + i += 1 + return cb() + }) + } + , function (err) { + if (err) { console.log('Got unexpected error during one peresistence operation: ' + err) } + } + ) + }) + }) } -]); - +])