├── .gitignore ├── .github └── workflows │ └── ci.yml ├── package.json ├── LICENSE ├── README.md ├── testCommon.js ├── test.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [4, 5, 6, 8, 10, 12, 14] 11 | mongodb-version: 12 | ["2.6", "3.0", "3.2", "3.4", "3.6", "4.0", "4.2", "4.4", "5.0"] 13 | 14 | steps: 15 | - name: Git checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Start MongoDB 24 | uses: supercharge/mongodb-github-action@1.7.0 25 | with: 26 | mongodb-version: ${{ matrix.mongodb-version }} 27 | 28 | - run: npm install 29 | 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongodown", 3 | "version": "2.0.0", 4 | "description": "A MongoDB implementation of the LevelDOWN API", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "author": "Thomas Watson Steen ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "abstract-leveldown": "^2.6.0", 13 | "after-all": "^2.0.0", 14 | "mongojs": "^3.1.0" 15 | }, 16 | "devDependencies": { 17 | "tap": "^0.5.0" 18 | }, 19 | "keywords": [ 20 | "mongo", 21 | "mongodb", 22 | "level", 23 | "leveldown", 24 | "levelup", 25 | "leveldb", 26 | "database", 27 | "db" 28 | ], 29 | "engines": { 30 | "node": ">=4.0.0" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/watson/mongodown.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/watson/mongodown/issues" 38 | }, 39 | "homepage": "https://github.com/watson/mongodown", 40 | "coordinates": [ 41 | 40.7458692, 42 | -73.9881377 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Thomas Watson Steen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MongoDOWN 2 | 3 | A drop-in replacement for 4 | [LevelDOWN](https://github.com/rvagg/node-leveldown) that runs on 5 | MongoDB. Can be used as a back-end for 6 | [LevelUP](https://github.com/rvagg/node-levelup) rather than an actual 7 | LevelDB store. 8 | 9 | [![Build Status](https://travis-ci.org/watson/mongodown.png)](https://travis-ci.org/watson/mongodown) 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install mongodown 15 | ``` 16 | 17 | ## Example 18 | 19 | ```javascript 20 | var levelup = require('levelup') 21 | var mongodown = require('mongodown') 22 | 23 | // MongoDB Collection name defaults to 'mongodown' 24 | var db = levelup(mongodown('localhost/my-database')) 25 | 26 | // OR pass custom MongoDB collection name 27 | db = levelup(mongodown('localhost/my-database'), { collection:'People_C' }) 28 | 29 | db.put('name', 'Yuri Irsenovich Kim') 30 | db.put('dob', '16 February 1941') 31 | db.put('spouse', 'Kim Young-sook') 32 | db.put('occupation', 'Clown') 33 | 34 | db.readStream() 35 | .on('data', console.log) 36 | .on('close', function () { console.log('Show\'s over folks!') }) 37 | ``` 38 | 39 | ## Limitations 40 | 41 | MongoDOWN does not support iterator snapshots 42 | 43 | ## License 44 | 45 | MIT 46 | -------------------------------------------------------------------------------- /testCommon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongojs = require('mongojs'); 4 | var afterAll = require('after-all'); 5 | 6 | var dbidx = 0 7 | 8 | , location = function () { 9 | return 'mongodown_test_' + dbidx++ 10 | } 11 | 12 | , lastLocation = function () { 13 | return 'mongodown_test_' + dbidx 14 | } 15 | 16 | , cleanup = function (callback) { 17 | var finished = function (err) { 18 | admin.close() 19 | callback(err) 20 | } 21 | var admin = mongojs('admin') 22 | admin.runCommand('listDatabases', function (err, result) { 23 | if (err) return finished(err) 24 | var next = afterAll(finished) 25 | result.databases 26 | .filter(function (database) { 27 | return /^mongodown_test_\d+$/.test(database.name) 28 | }) 29 | .forEach(function (database) { 30 | var db = mongojs(database.name) 31 | var done = next() 32 | db.dropDatabase(function (err) { 33 | db.close() 34 | done(err) 35 | }) 36 | }) 37 | }); 38 | } 39 | 40 | , setUp = function (t) { 41 | cleanup(function (err) { 42 | t.notOk(err, 'cleanup returned an error') 43 | t.end() 44 | }) 45 | } 46 | 47 | , tearDown = function (t) { 48 | setUp(t) // same cleanup! 49 | } 50 | 51 | , collectEntries = function (iterator, callback) { 52 | var data = [] 53 | , next = function () { 54 | iterator.next(function (err, key, value) { 55 | if (err) return callback(err) 56 | if (!arguments.length) { 57 | return iterator.end(function (err) { 58 | callback(err, data) 59 | }) 60 | } 61 | data.push({ key: key, value: value }) 62 | process.nextTick(next) 63 | }) 64 | } 65 | next() 66 | } 67 | 68 | module.exports = { 69 | location : location 70 | , cleanup : cleanup 71 | , lastLocation : lastLocation 72 | , setUp : setUp 73 | , tearDown : tearDown 74 | , collectEntries : collectEntries 75 | } 76 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tap').test; 4 | var testCommon = require('./testCommon'); 5 | var MongoDOWN = require('./'); 6 | 7 | function factory (location) { 8 | return new MongoDOWN(location); 9 | } 10 | 11 | /*** compatibility with basic LevelDOWN API ***/ 12 | 13 | require('abstract-leveldown/abstract/leveldown-test').args(factory, test, testCommon) 14 | 15 | require('abstract-leveldown/abstract/open-test').setUp(factory, test, testCommon) 16 | require('abstract-leveldown/abstract/open-test').args(factory, test, testCommon) 17 | require('abstract-leveldown/abstract/open-test').open(factory, test, testCommon) 18 | // Test will not run because the database isn't actually created upon touch, 19 | // but rather when the first document is added 20 | // require('abstract-leveldown/abstract/open-test').openAdvanced(factory, test, testCommon) 21 | require('abstract-leveldown/abstract/open-test').tearDown(factory, test, testCommon) 22 | 23 | require('abstract-leveldown/abstract/del-test').all(factory, test, testCommon) 24 | 25 | require('abstract-leveldown/abstract/get-test').all(factory, test, testCommon) 26 | 27 | require('abstract-leveldown/abstract/put-test').all(factory, test, testCommon) 28 | 29 | require('abstract-leveldown/abstract/put-get-del-test').setUp(factory, test, testCommon) 30 | require('abstract-leveldown/abstract/put-get-del-test').errorKeys(test) 31 | //require('abstract-leveldown/abstract/put-get-del-test').nonErrorKeys(test, testCommon) 32 | require('abstract-leveldown/abstract/put-get-del-test').errorValues(test) 33 | //require('abstract-leveldown/abstract/test/put-get-del-test').nonErrorKeys(test, testCommon) 34 | require('abstract-leveldown/abstract/put-get-del-test').tearDown(test, testCommon) 35 | 36 | require('abstract-leveldown/abstract/approximate-size-test').setUp(factory, test, testCommon) 37 | require('abstract-leveldown/abstract/approximate-size-test').args(test) 38 | 39 | require('abstract-leveldown/abstract/batch-test').setUp(factory, test, testCommon) 40 | require('abstract-leveldown/abstract/batch-test').args(test) 41 | 42 | require('abstract-leveldown/abstract/chained-batch-test').setUp(factory, test, testCommon) 43 | require('abstract-leveldown/abstract/chained-batch-test').args(test) 44 | 45 | require('abstract-leveldown/abstract/close-test').close(factory, test, testCommon) 46 | 47 | require('abstract-leveldown/abstract/iterator-test').setUp(factory, test, testCommon) 48 | require('abstract-leveldown/abstract/iterator-test').args(test) 49 | require('abstract-leveldown/abstract/iterator-test').sequence(test) 50 | require('abstract-leveldown/abstract/iterator-test').iterator(factory, test, testCommon, testCommon.collectEntries) 51 | require('abstract-leveldown/abstract/iterator-test').tearDown(test, testCommon) 52 | 53 | test('end', function (t) { 54 | t.end(); 55 | process.nextTick(process.exit); 56 | }) 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var mongojs = require('mongojs'); 5 | var AbstractLevelDOWN = require('abstract-leveldown').AbstractLevelDOWN; 6 | var AbstractIterator = require('abstract-leveldown').AbstractIterator; 7 | var afterAll = require('after-all'); 8 | 9 | var dbExists = function (name, callback) { 10 | var admin = mongojs('admin'); 11 | admin.runCommand('listDatabases', function (err, result) { 12 | if (err) return callback(err); 13 | for (var n = 0, l = result.databases.length; n < l; n++) 14 | if (result.databases[n].name === name) return callback(null, true); 15 | callback(); 16 | admin.close(); 17 | }); 18 | }; 19 | 20 | var MongoDOWN = module.exports = function (mongoUri) { 21 | if (!(this instanceof MongoDOWN)) 22 | return new MongoDOWN(mongoUri); 23 | AbstractLevelDOWN.call(this, mongoUri); 24 | }; 25 | 26 | util.inherits(MongoDOWN, AbstractLevelDOWN); 27 | 28 | MongoDOWN.prototype._open = function (options, callback) { 29 | 30 | var self = this; 31 | 32 | self.collection = options.collection || 'mongodown'; 33 | 34 | var connect = function () { 35 | self._db = mongojs(self.location, [self.collection]); 36 | callback(null, self); 37 | }; 38 | 39 | if (!options.createIfMissing) 40 | dbExists(self.location, function (err, exists) { 41 | if (err) return callback(err); 42 | if (!exists) return callback(new Error('Database ' + self.location + ' does not exist')); 43 | connect(); 44 | }); 45 | else if (options.errorIfExists) 46 | dbExists(self.location, function (err, exists) { 47 | if (err) return callback(err); 48 | if (exists) return callback(new Error('Database ' + self.location + ' already exists')); 49 | connect(); 50 | }); 51 | else 52 | process.nextTick(connect); 53 | }; 54 | 55 | MongoDOWN.prototype._close = function (callback) { 56 | this._db.close(); 57 | process.nextTick(callback); 58 | }; 59 | 60 | MongoDOWN.prototype._get = function (key, options, callback) { 61 | this._db[this.collection].findOne({ _id: key }, function (err, doc) { 62 | if (err) return callback(err); 63 | if (!doc) return callback(new Error('notFound')); 64 | var value = options.asBuffer ? 65 | (Buffer.isBuffer(doc.value) ? doc.value : new Buffer(doc.value)) : 66 | (Buffer.isBuffer(doc.value) ? doc.value.toString() : doc.value); 67 | callback(null, value); 68 | }); 69 | }; 70 | 71 | MongoDOWN.prototype._put = function (key, value, options, callback) { 72 | this._db[this.collection].save({ _id: key, value: value }, callback); 73 | }; 74 | 75 | MongoDOWN.prototype._del = function (key, options, callback) { 76 | this._db[this.collection].remove({ _id: key }, callback); 77 | }; 78 | 79 | // TODO: Consider using writeConcern's in MongoDB to simulate sync 80 | MongoDOWN.prototype._batch = function (array, options, callback) { 81 | var self = this, 82 | batches = [[]], 83 | batchIndex = 0, 84 | cmdIndex = 0, 85 | cmdLength = array.length, 86 | cmd, batch, prevType; 87 | 88 | // TODO: Does AbstractLevelDOWN take care of not calling _batch if array is 89 | // empty? If not we need to handle this: 90 | if (!cmdLength) return process.nextTick(callback); 91 | 92 | for (; cmdIndex < cmdLength; cmdIndex++) { 93 | cmd = array[cmdIndex]; 94 | if (prevType && cmd.type !== prevType) batches[++batchIndex] = [cmd]; 95 | else batches[batchIndex].push(cmd); 96 | prevType = cmd.type; 97 | } 98 | 99 | (function commit (err) { 100 | if (err) return callback(err); 101 | var batch = batches.shift(); 102 | if (!batch) return callback(); 103 | switch (batch[0].type) { 104 | case 'put': 105 | var next = afterAll(commit); 106 | for (var n = 0, l = batch.length; n < l; n++) 107 | self._db[self.collection].save({ _id: batch[n].key, value: batch[n].value }, next()); 108 | break; 109 | case 'del': 110 | var keys = batch.map(function (e) { return e.key; }); 111 | self._db[self.collection].remove({ _id: { $in: keys } }, commit); 112 | break; 113 | default: // TODO: Does AbstractLevelDOWN take care of this for us? 114 | callback(new Error('Unknown batch type: ' + batch[0].type)); 115 | } 116 | })(); 117 | }; 118 | 119 | MongoDOWN.prototype._approximateSize = function (start, end, callback) { 120 | this._db[this.collection].count({ _id: { $gte: start, $lte: end } }, callback); 121 | }; 122 | 123 | MongoDOWN.prototype._iterator = function (options) { 124 | return new MongoIterator(this, options); 125 | }; 126 | 127 | var MongoIterator = function (db, options) { 128 | AbstractIterator.call(this, db); 129 | if (options.limit === 0) return; 130 | this._options = options; 131 | var query = { _id: {} }; 132 | if (options.reverse) { 133 | if (options.start) query._id.$lte = options.start; 134 | if (options.end) query._id.$gte = options.end; 135 | if (options.gt) query._id.$lt = options.gt; 136 | if (options.gte) query._id.$lte = options.gte; 137 | if (options.lt) query._id.$gt = options.lt; 138 | if (options.lte) query._id.$gte = options.lte; 139 | } else { 140 | if (options.start) query._id.$gte = options.start; 141 | if (options.end) query._id.$lte = options.end; 142 | if (options.gt) query._id.$gt = options.gt; 143 | if (options.gte) query._id.$gte = options.gte; 144 | if (options.lt) query._id.$lt = options.lt; 145 | if (options.lte) query._id.$lte = options.lte; 146 | } 147 | if (!Object.keys(query._id).length) delete query._id; 148 | this._cursor = db._db[db.collection].find(query).sort({ _id: options.reverse ? -1 : 1 }); 149 | if (options.limit && options.limit !== -1) this._cursor = this._cursor.limit(options.limit); 150 | }; 151 | 152 | util.inherits(MongoIterator, AbstractIterator); 153 | 154 | MongoIterator.prototype._next = function (callback) { 155 | var options = this._options; 156 | if (!this._cursor) return callback(); 157 | this._cursor.next(function (err, doc) { 158 | if (err) return callback(err); 159 | if (!doc) return callback(); 160 | var key = options.keyAsBuffer ? 161 | (Buffer.isBuffer(doc._id) ? doc._id : new Buffer(doc._id)) : 162 | (Buffer.isBuffer(doc._id) ? doc._id.toString() : doc._id); 163 | var val = options.valueAsBuffer ? 164 | (Buffer.isBuffer(doc.value) ? doc.value : new Buffer(doc.value)) : 165 | (Buffer.isBuffer(doc.value) ? doc.value.toString() : doc.value); 166 | callback(undefined, key, val); 167 | }); 168 | }; 169 | 170 | MongoIterator.prototype._end = function (callback) { 171 | if (this._cursor) this._cursor.destroy(); 172 | callback(); 173 | }; 174 | --------------------------------------------------------------------------------