├── .idea ├── .name ├── copyright │ └── profiles_settings.xml ├── ant.xml ├── encodings.xml ├── vcs.xml ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── modules.xml ├── compiler.xml ├── misc.xml └── uiDesigner.xml ├── .npmignore ├── .gitignore ├── .DS_Store ├── .travis.yml ├── mongolian.js ├── Node-mongolian.iml ├── package.json ├── LICENSE ├── lib ├── util.js ├── gridfs.js ├── connection.js ├── db.js ├── cursor.js ├── gridfile.js ├── server.js └── collection.js ├── test ├── collection3.js ├── distinct.js ├── misc.js ├── collection10000.js ├── async.js ├── collection2.js ├── index.js ├── collection1000.js ├── collection1.js └── gridfs.js ├── Changelog.md ├── examples └── mongolian_trainer.js └── Readme.md /.idea/.name: -------------------------------------------------------------------------------- 1 | node-mongolian -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea/workspace.xml -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcello3d/node-mongolian/HEAD/.DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | services: mongodb 3 | node_js: 4 | - 0.6 5 | - 0.8 -------------------------------------------------------------------------------- /mongolian.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | module.exports = require('./lib/server') -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/ant.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Node-mongolian.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongolian", 3 | "description": "Mongolian DeadBeef is an awesome Mongo DB node.js driver", 4 | "version": "0.1.18", 5 | "homepage": "https://github.com/marcello3d/node-mongolian", 6 | "repository": "git://github.com/marcello3d/node-mongolian.git", 7 | "author": "Marcello Bastéa-Forte (http://marcello.cellosoft.com/)", 8 | "main": "mongolian.js", 9 | "keywords": ["mongo", "mongodb", "database", "db", "nosql"], 10 | "dependencies": { 11 | "buffalo": "0.1.3", 12 | "waiter": "0.1.1", 13 | "taxman": "0.1.1" 14 | }, 15 | "devDependencies": { 16 | "nodeunit": "0.6.4" 17 | }, 18 | "scripts": { 19 | "test": "node_modules/.bin/nodeunit test" 20 | }, 21 | "engines": { 22 | "node": ">=0.4.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Marcello Bastéa-Forte (marcello@cellosoft.com) 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must not be 17 | misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source 20 | distribution. -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | var util = require('util') 3 | 4 | ////////////////////////////////////////////////////////////////////////////////// 5 | // Internal 6 | 7 | /** 8 | * Convenience method for handling async value callbacks 9 | * 10 | * @param callback the target async callback 11 | * @param body the body to call 12 | * @returns an async function(err,value) 13 | */ 14 | exports.safetyNet = function(callback,body) { 15 | return function(error, result) { 16 | if (error) { 17 | callback && callback(error) 18 | } else { 19 | body(result) 20 | } 21 | } 22 | } 23 | 24 | exports.extend = function(destination, source) { 25 | if (source) for (var key in source) destination[key] = source[key] 26 | return destination 27 | } 28 | 29 | exports.callback = function(argumentsObject, required, offset) { 30 | var callback = argumentsObject[argumentsObject.length - (offset || 1)] 31 | if (typeof callback === 'function') return callback 32 | if (required) throw new Error(required + ' callback is required') 33 | return undefined 34 | } -------------------------------------------------------------------------------- /test/collection3.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var db,collection 6 | 7 | module.exports = { 8 | "create connection": function(test) { 9 | db = new Mongolian('mongo://localhost/mongolian_test', { log:false }) 10 | db.dropDatabase(function(error) { 11 | test.ifError(error) 12 | test.done() 13 | }) 14 | }, 15 | 16 | "check isCapped": function(test) { 17 | db.eval( 18 | function() { 19 | return db.createCollection("test3", { capped:true, size:10 }) 20 | }, 21 | function(error, result) { 22 | test.ifError(error) 23 | collection = db.collection("test3") 24 | collection.isCapped(function(error, capped, size) { 25 | test.equal(capped, true) 26 | test.equal(size, 10) 27 | test.done() 28 | }) 29 | } 30 | ) 31 | }, 32 | 33 | "close connection": function(test) { 34 | db.server.close() 35 | test.done() 36 | } 37 | } -------------------------------------------------------------------------------- /test/distinct.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var db,collection 6 | 7 | module.exports = { 8 | "create connection": function(test) { 9 | db = new Mongolian('mongo://localhost/mongolian_test', { log:false }) 10 | collection = db.collection('test_distinct') 11 | db.dropDatabase(function(error) { 12 | test.ifError(error) 13 | test.done() 14 | }) 15 | }, 16 | 17 | 18 | "insert some rows": function(test) { 19 | var array = [{ 20 | i: 0, 21 | color: 'yellow' 22 | },{ 23 | i:1, 24 | color: 'white' 25 | },{ 26 | i:2, 27 | color: 'red' 28 | },{ 29 | i:3, 30 | color: 'red' 31 | },{ 32 | i:4, 33 | color: 'green' 34 | }] 35 | 36 | collection.insert(array, function(error, insertedRows) { 37 | test.ifError(error) 38 | test.equal(insertedRows.length, 5) 39 | test.done() 40 | }) 41 | }, 42 | "test for distinct colors": function(test) { 43 | collection.distinct("color", function(error, values) { 44 | test.deepEqual(values, ['yellow', 'white', 'red', 'green']) 45 | test.done() 46 | }) 47 | }, 48 | "test for distinct colors with condition i<4": function(test) { 49 | collection.distinct("color", {i: { $lt: 4 }}, function(error, values) { 50 | test.deepEqual(values, ['yellow', 'white', 'red']) 51 | test.done() 52 | }) 53 | }, 54 | 55 | "close connection": function(test) { 56 | db.server.close() 57 | test.done() 58 | } 59 | } -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /test/misc.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var db,collection 6 | 7 | module.exports = { 8 | "create connection": function(test) { 9 | db = new Mongolian('mongo://localhost/mongolian_test', { log:false }) 10 | db.dropDatabase(function(error) { 11 | test.ifError(error) 12 | test.done() 13 | }) 14 | }, 15 | 16 | "db is not null": function(test) { 17 | test.ok(db) 18 | test.done() 19 | }, 20 | "its name is right": function(test) { 21 | test.equal(db.name, "mongolian_test") 22 | test.done() 23 | }, 24 | "its collectionNames": function(test) { 25 | db.collectionNames(function(error, names) { 26 | test.ifError(error) 27 | test.equal(names.length, 0) 28 | test.done() 29 | }) 30 | }, 31 | "eval": function(test) { 32 | db.eval(function() { 33 | return 5 34 | }, function(error, result) { 35 | test.ifError(error) 36 | test.equal(result,5) 37 | test.done() 38 | }) 39 | }, 40 | "eval with parameter": function(test) { 41 | db.eval( 42 | function(x) { 43 | return x 44 | }, 45 | 5, 46 | function(error, result) { 47 | test.ifError(error) 48 | test.equal(result,5) 49 | test.done() 50 | } 51 | ) 52 | }, 53 | "eval with two parameters": function(test) { 54 | db.eval( 55 | function(x, y) { 56 | return x + y 57 | }, 58 | 5, 6, 59 | function(error, result) { 60 | test.ifError(error) 61 | test.equal(result,11) 62 | test.done() 63 | } 64 | ) 65 | }, 66 | 67 | "close connection": function(test) { 68 | db.server.close() 69 | test.done() 70 | } 71 | } -------------------------------------------------------------------------------- /test/collection10000.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var db,collection 6 | 7 | module.exports = { 8 | "create connection": function(test) { 9 | db = new Mongolian('mongo://localhost/mongolian_test', { log:false }) 10 | collection = db.collection('test_10000') 11 | db.dropDatabase(function(error) { 12 | test.ifError(error) 13 | test.done() 14 | }) 15 | }, 16 | 17 | 18 | "insert 10000 documents": function(test) { 19 | var array = [] 20 | for (var i=0; i<10000; i++) { 21 | array.push({ 22 | i:i, 23 | even:(i%2) == 0 24 | }) 25 | } 26 | collection.insert(array, function(error,insertedRows) { 27 | test.ifError(error) 28 | test.equal(insertedRows.length, 10000) 29 | test.done() 30 | }) 31 | }, 32 | "count": function(test) { 33 | collection.find().count(function(error, count) { 34 | test.ifError(error) 35 | test.equal(count, 10000) 36 | test.done() 37 | }) 38 | }, 39 | "size": function(test) { 40 | collection.find().size(function(error, count) { 41 | test.ifError(error) 42 | test.equal(count, 10000) 43 | test.done() 44 | }) 45 | }, 46 | "find().sort({i:-1}).limit(10000)": function(test) { 47 | collection.find().sort({i:-1}).limit(10000).toArray(function(error, array) { 48 | test.ifError(error) 49 | test.equal(array.length, 10000) 50 | test.done() 51 | }) 52 | }, 53 | "forEach counter": function(test) { 54 | var counter = 0 55 | collection.find().forEach(function(item) { 56 | counter++ 57 | }, function(error) { 58 | test.ifError(error) 59 | test.equal(counter, 10000) 60 | test.done() 61 | }) 62 | }, 63 | 64 | "close connection": function(test) { 65 | db.server.close() 66 | test.done() 67 | } 68 | } -------------------------------------------------------------------------------- /lib/gridfs.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | var buffalo = require('buffalo') 3 | 4 | var safetyNet = require('./util').safetyNet 5 | var MongolianGridFile = require('./gridfile') 6 | 7 | var MongolianGridFS = module.exports = function(db, name) { 8 | this.server = db.server 9 | this.db = db 10 | this.name = name || "fs" 11 | var files = this.files = db.collection(name+".files") 12 | var chunks = this.chunks = db.collection(name+".chunks") 13 | files.count(safetyNet(null, function(count) { 14 | if (count < 1000) files.ensureIndex({ filename:1, uploadDate:1 }) 15 | })) 16 | chunks.count(safetyNet(null, function(count) { 17 | if (count < 1000) chunks.ensureIndex({ files_id:1, n:1 }, { unique: true }) 18 | })) 19 | } 20 | 21 | /** 22 | * Creates a new MongolianGridFile 23 | */ 24 | MongolianGridFS.prototype.create = function(descriptor) { 25 | if (typeof descriptor === 'string') { 26 | descriptor = { filename:descriptor } 27 | } 28 | return new MongolianGridFile(this, descriptor) 29 | } 30 | 31 | /** 32 | * Returns a MongolianCursor mapped to MongolianGridFile of all grid files matching the searchBy query. 33 | * 34 | * find() - all results 35 | * find(object) - mongodb query on the fs.files collection 36 | * find(string/regexp) - shorthand for find({ filename: string }) 37 | * find(objectId) - shorthand to find({ _id:objectId }) 38 | */ 39 | MongolianGridFS.prototype.find = function(searchBy) { 40 | var query 41 | if (typeof searchBy === 'string' || searchBy instanceof RegExp) { 42 | query = { filename:searchBy } 43 | } else if (searchBy instanceof buffalo.ObjectId) { 44 | query = { _id:searchBy } 45 | } else { 46 | query = searchBy 47 | } 48 | var self = this 49 | return self.files.find(query).map(function (document) { 50 | return document && new MongolianGridFile(self, document) 51 | }) 52 | } 53 | /** 54 | * Returns the first result that matches the searchBy query (see find(searchBy)) 55 | * 56 | * Shorthand for find(searchBy).limit(1).next(callback) 57 | */ 58 | MongolianGridFS.prototype.findOne = function(searchBy, callback) { 59 | this.find(searchBy).limit(1).next(callback) 60 | } 61 | 62 | MongolianGridFS.prototype.toString = function() { 63 | return this.db+"/"+this.name+"[gridfs]" 64 | } -------------------------------------------------------------------------------- /test/async.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var Waiter = require('waiter') 6 | 7 | var db,collection 8 | 9 | module.exports = { 10 | "create connection": function(test) { 11 | db = new Mongolian('mongo://localhost/mongolian_test', { log:false }) 12 | collection = db.collection('test_async') 13 | db.dropDatabase(function(error) { 14 | test.ifError(error) 15 | test.done() 16 | }) 17 | }, 18 | 19 | "insert 1000 documents": function(test) { 20 | var array = [] 21 | var data = '' 22 | for (var i=0; i<1000; i++) { 23 | array.push({ 24 | i:i, 25 | even:(i%2) == 0, 26 | data:data+='1234567890' 27 | }) 28 | } 29 | collection.insert(array, function(error,insertedRows) { 30 | test.ifError(error) 31 | test.equal(insertedRows.length, 1000) 32 | test.done() 33 | }) 34 | }, 35 | "async 10": function(test) { 36 | var waiter = new Waiter 37 | for (var i=0; i<10; i++) { 38 | collection.find({i:{$lt:i}}).toArray(waiter()) 39 | } 40 | waiter.waitForAll(function() { 41 | test.done() 42 | }) 43 | }, 44 | "async 100": function(test) { 45 | var waiter = new Waiter 46 | for (var i=0; i<100; i++) { 47 | collection.find({i:{$lt:i}}).toArray(waiter()) 48 | } 49 | waiter.waitForAll(function() { 50 | test.done() 51 | }) 52 | }, 53 | 54 | "insert 1000 documents and async read": function(test) { 55 | var array = [] 56 | var idMap = {} 57 | var data = '' 58 | for (var i=0; i<1000; i++) { 59 | var id = new Mongolian.ObjectId 60 | array.push({ 61 | _id:id, 62 | i:i, 63 | name:'async', 64 | data:data+='X' 65 | }) 66 | idMap[id.toString()] = true 67 | } 68 | collection.insert(array, function(error,insertedRows) { 69 | test.ifError(error) 70 | test.equal(insertedRows.length, 1000) 71 | collection.find({name:'async'}).toArray(function(error, array) { 72 | test.ifError(error) 73 | test.equal(array.length, 1000) 74 | var validIds = 0 75 | array.forEach(function(item) { 76 | var idString = item._id.toString() 77 | // Check that this is an id we inserted 78 | if (idMap[idString]) validIds++ 79 | // Remove it from our map so we can catch duplicates 80 | delete idMap[idString] 81 | }) 82 | test.equal(validIds, 1000) 83 | test.done() 84 | }) 85 | }) 86 | }, 87 | 88 | "close connection": function(test) { 89 | db.server.close() 90 | test.done() 91 | } 92 | } -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | Mongolian DeadBeef Changelog 2 | ---------------------------- 3 | 4 | v0.1.18 5 | 6 | + GH-90 gridfs now allows multiple files with the same name 7 | + GH-96 cursor.forEach ignores cursor.map() 8 | 9 | v0.1.17 10 | 11 | + major bugfix release 12 | + GH-77,GH-86 fixed authentication regression from 0.1.15 13 | + GH-82 unit tests 14 | + GH-83 avoid buffer reuse bug with updated buffalo 15 | + GH-84 pass number of updated/deleted rows to update() and remove() methods 16 | + GH-87 catch Query Failure and Cursor Not Found errors and return them as errors 17 | 18 | v0.1.16 19 | 20 | + GH-72 - fixes Buffer is not large enough exception 21 | 22 | v0.1.15 23 | 24 | + removed mongodb-native dependency, now using node-buffalo (https://github.com/marcello3d/node-buffalo) 25 | + GH-69 - exceptions in user callbacks should now bubble up correctly 26 | + GH-59 - fixes ensureIndex throws error about key name 27 | 28 | v0.1.14 29 | 30 | + GH-28 - tweak for mongos support from yosefd (I haven't personally tested a sharded setup yet) 31 | + GH-61 - getLastError -> getlasterror compatibility fix 32 | + GH-63 GH-64 GH-65 - update to latest mongodb-native socket handling code (thanks to xcoderzach) 33 | + peg mongodb@0.9.7-2-2 to avoid untested version conflicts 34 | 35 | v0.1.13 36 | 37 | + GH-46 - Rewrote cursor.forEach to use nextBatch instead of next, fixes stack overflow 38 | + GH-60 - upgrade to new mongodb@0.9.7 socket connection code 39 | + new test framework (using nodeunit instead of vowsjs) 40 | + new index tests 41 | + tweaks to ensureIndex/createIndex 42 | + other code style changes 43 | 44 | v0.1.12 45 | 46 | + GH-53 - regression if save has no callback 47 | 48 | v0.1.11 49 | 50 | + GH-24 - Remove hack for mongodb-native bug 51 | + GH-31 - Save callback gets no data 52 | + GH-34 - updated taxman dependency 53 | + GH-37 - mapReduce wasn't converting optional finalize function to bson.Code instance 54 | + GH-41 - Add collection.distinct (Filirom1) 55 | + GH-51 - support for mongodb://-style url (mschuetz) 56 | + GH-52 - migrated from x instanceof Function to typeof x === 'function' 57 | + logging/error tweaks 58 | + documentation tweaks 59 | + removed broken/unsupported server.closeWhenDone method 60 | + error in url parsing error message (the irony!) 61 | 62 | v0.1.10 63 | 64 | + added collection.runCommand 65 | + renamed db.queryCommand -> db.runCommand, matching mongo shell syntax 66 | + gridfs tweaks 67 | + fixed collection._indexCache creation (bug caught by dvv) and improved ensureIndex return value consistency 68 | + GH-21 - collection.drop() and db.dropDatabase() not checking if callback is valid 69 | + GH-22 - EventEmitter error 70 | + GH-15/GH-29 - more robust error catching! 71 | + GH-24 - temporary workaround for (mongodb 0.9.4-4 ~ 0.9.6.1) incompatibility 72 | + more tests 73 | 74 | v0.1.9 75 | 76 | + added replicaset support (automatically finds primary and detects secondaries) 77 | + removed keepAlive functionality (GH-19) 78 | + added collection.findAndModify 79 | + added db.eval (GH-12) 80 | + renamed db.drop to db.dropDatabase 81 | + renamed cursor.mapper to cursor.map 82 | + fixed BSON type exports (GH-18) 83 | + various documentation/error message tweaks 84 | + couple more tests -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var tls = require('tls') 4 | var net = require('net') 5 | var events = require('events') 6 | var util = require('util') 7 | 8 | function Connection(options) { 9 | var initialBufferSize = options && options.initialBufferSize || 1024*10 10 | var maxMessageSize = options && options.maxMessageSize || 1024*1024*32 11 | 12 | var thiz = this 13 | 14 | var socket = new net.Socket 15 | 16 | // Dynamicly sized buffer 17 | var buffer = new Buffer(initialBufferSize) 18 | var readIndex = 0 19 | var writeIndex = 0 20 | 21 | function grow(minAmount) { 22 | var buffer2 = new Buffer(buffer.length + Math.max(minAmount, buffer.length)) 23 | buffer.copy(buffer2, 0, readIndex) 24 | writeIndex -= readIndex 25 | readIndex = 0 26 | buffer = buffer2 27 | } 28 | 29 | function append(data) { 30 | var bytesNeeded = (writeIndex + data.length) - buffer.length 31 | if (bytesNeeded > 0) grow(bytesNeeded) 32 | data.copy(buffer, writeIndex) 33 | writeIndex += data.length 34 | } 35 | var expectedMessageSize = 4 36 | var readMessageLength = false 37 | 38 | // Configure socket 39 | socket.setNoDelay(false) 40 | 41 | // SSL support 42 | var readWriteStream = socket 43 | if (options && options.ssl) { 44 | var pair = tls.createSecurePair(options.tlsCredentials) 45 | pair.encrypted.pipe(socket) 46 | socket.pipe(pair.encrypted) 47 | 48 | readWriteStream = pair.cleartext 49 | } 50 | 51 | // Setup write command 52 | this.write = function(buffer, callback) { 53 | readWriteStream.write(buffer, callback) 54 | } 55 | 56 | // Setup data listener 57 | readWriteStream.on('data', function(data) { 58 | append(data) 59 | while (writeIndex - readIndex >= expectedMessageSize) { 60 | if (readMessageLength) { 61 | thiz.emit('message', buffer.slice(readIndex, readIndex += expectedMessageSize)) 62 | if (readIndex == writeIndex) { 63 | readIndex = writeIndex = 0 64 | } 65 | expectedMessageSize = 4 66 | readMessageLength = false 67 | } else { 68 | expectedMessageSize = (buffer[readIndex]) | 69 | (buffer[readIndex+1] << 8) | 70 | (buffer[readIndex+2] << 16) | 71 | (buffer[readIndex+3] << 24) 72 | readMessageLength = true 73 | if (expectedMessageSize > maxMessageSize) { 74 | thiz.emit('error', 'message too large: ' + expectedMessageSize + ' (max=' + maxMessageSize + ')') 75 | thiz.close() 76 | return 77 | } 78 | } 79 | } 80 | }) 81 | socket.on('connect', function() { thiz.emit('connect') }) 82 | socket.on('error', function(message) { thiz.emit('error', message) }) 83 | socket.on('close', function() { thiz.emit('close') }) 84 | this.connect = function(port,host) { socket.connect(port,host) } 85 | this.close = function() { socket.end() } 86 | this.destroy = function() { socket.destroy() } 87 | } 88 | util.inherits(Connection, events.EventEmitter) 89 | 90 | module.exports = Connection 91 | -------------------------------------------------------------------------------- /test/collection2.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var db,collection 6 | 7 | module.exports = { 8 | "create connection": function(test) { 9 | db = new Mongolian('mongo://localhost/mongolian_test', { log:false }) 10 | collection = db.collection('test2') 11 | db.dropDatabase(function(error) { 12 | test.ifError(error) 13 | test.done() 14 | }) 15 | }, 16 | 17 | "insert multiple documents": function(test) { 18 | collection.insert([ 19 | { i:1, j:true }, 20 | { i:2, j:true }, 21 | { i:3, j:false }, 22 | { i:4, j:false }, 23 | { i:5, j:true } 24 | ], function(error, insertedRows) { 25 | test.ifError(error) 26 | test.equal(insertedRows.length, 5) 27 | collection.findOne(function(error, foundRow) { 28 | test.ifError(error) 29 | test.ok(foundRow.i > 0) 30 | 31 | collection.find().toArray(function(error, array) { 32 | test.ifError(error) 33 | test.equal(array.length, 5) 34 | test.equal(array[0].i, 1) 35 | test.equal(array[1].i, 2) 36 | test.equal(array[2].i, 3) 37 | test.equal(array[3].i, 4) 38 | test.equal(array[4].i, 5) 39 | test.done() 40 | }) 41 | }) 42 | }) 43 | }, 44 | "sorted find": function(test) { 45 | collection.find().sort({i:1}).toArray(function(error, array) { 46 | test.ifError(error) 47 | test.equal(array.length, 5) 48 | test.equal(array[0].i, 1) 49 | test.equal(array[1].i, 2) 50 | test.equal(array[2].i, 3) 51 | test.equal(array[3].i, 4) 52 | test.equal(array[4].i, 5) 53 | test.done() 54 | }) 55 | }, 56 | "reverse sorted find": function(test) { 57 | collection.find().sort({i:-1}).toArray(function(error, array) { 58 | test.ifError(error) 59 | test.equal(array.length, 5) 60 | test.equal(array[0].i, 5) 61 | test.equal(array[1].i, 4) 62 | test.equal(array[2].i, 3) 63 | test.equal(array[3].i, 2) 64 | test.equal(array[4].i, 1) 65 | test.done() 66 | }) 67 | }, 68 | "find().limit(3).sort({i:1})": function(test) { 69 | collection.find().limit(3).sort({i:1}).toArray(function(error, array) { 70 | test.ifError(error) 71 | test.equal(array.length, 3) 72 | test.equal(array[0].i, 1) 73 | test.equal(array[1].i, 2) 74 | test.equal(array[2].i, 3) 75 | test.done() 76 | }) 77 | }, 78 | "find().skip(1).limit(3).sort({i:1})": function(test) { 79 | collection.find().skip(1).limit(3).sort({i:1}).toArray(function(error, array) { 80 | test.ifError(error) 81 | test.equal(array.length, 3) 82 | test.equal(array[0].i, 2) 83 | test.equal(array[1].i, 3) 84 | test.equal(array[2].i, 4) 85 | test.done() 86 | }) 87 | }, 88 | "query failure": function(test) { 89 | collection.find([undefined]).toArray(function(error, array) { 90 | test.ok(error) 91 | test.equal(array, undefined) 92 | test.equal(error.message, "Query failure: can't have undefined in a query expression") 93 | test.done() 94 | }) 95 | }, 96 | 97 | "close connection": function(test) { 98 | db.server.close() 99 | test.done() 100 | } 101 | } -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var db,collection 6 | 7 | module.exports = { 8 | "create connection": function(test) { 9 | db = new Mongolian('mongo://localhost/mongolian_test', { log:false }) 10 | collection = db.collection('testindex') 11 | db.dropDatabase(function(error) { 12 | test.ifError(error) 13 | test.done() 14 | }) 15 | }, 16 | 17 | "createIndex": function(test) { 18 | collection.createIndex({foo: 1}, function(error,index) { 19 | test.ifError(error) 20 | test.deepEqual(index.key,{foo: 1}) 21 | test.done() 22 | }) 23 | }, 24 | "createIndex with options": function(test) { 25 | collection.createIndex({foo2: 1}, {background: 1}, function(error,index) { 26 | test.ifError(error) 27 | test.deepEqual(index.key,{foo2: 1}) 28 | test.done() 29 | }) 30 | }, 31 | 32 | "createIndex with dot type": function(test) { 33 | collection.createIndex({"foo3.bar": 1}, function(error,index) { 34 | test.ifError(error) 35 | test.deepEqual(index.key,{"foo3.bar": 1}) 36 | test.done() 37 | }) 38 | }, 39 | "createIndex with dot type and options": function(test) { 40 | collection.createIndex({"foo4.bar": 1}, {background: 1}, function(error,index) { 41 | test.ifError(error) 42 | test.deepEqual(index.key,{"foo4.bar": 1}) 43 | test.done() 44 | }) 45 | }, 46 | "ensureIndex": function(test) { 47 | collection.ensureIndex({foo5: 1}, function(error,index) { 48 | test.ifError(error) 49 | test.deepEqual(index.key,{foo5: 1}) 50 | test.done() 51 | }) 52 | }, 53 | "ensureIndex with options": function(test) { 54 | collection.ensureIndex({foo6: 1}, {background: 1}, function(error,index) { 55 | test.ifError(error) 56 | test.deepEqual(index.key,{foo6: 1}) 57 | test.done() 58 | }) 59 | }, 60 | 61 | "ensureIndex with dot type": function(test) { 62 | collection.ensureIndex({"foo7.bar": 1}, function(error,index) { 63 | test.ifError(error) 64 | test.deepEqual(index.key,{"foo7.bar": 1}) 65 | test.done() 66 | }) 67 | }, 68 | "ensureIndex with dot type and options": function(test) { 69 | collection.ensureIndex({"foo8.bar": 1}, {background: 1}, function(error,index) { 70 | test.ifError(error) 71 | test.deepEqual(index.key,{"foo8.bar": 1}) 72 | test.done() 73 | }) 74 | }, 75 | "retry ensureIndex": function(test) { 76 | collection.ensureIndex({foo5: 1}, function(error,index) { 77 | test.ifError(error) 78 | test.deepEqual(index.key,{foo5: 1}) 79 | test.done() 80 | }) 81 | }, 82 | "retry ensureIndex with options": function(test) { 83 | collection.ensureIndex({foo6: 1}, {background: 1}, function(error,index) { 84 | test.ifError(error) 85 | test.deepEqual(index.key,{foo6: 1}) 86 | test.done() 87 | }) 88 | }, 89 | 90 | "retry ensureIndex with dot type": function(test) { 91 | collection.ensureIndex({"foo7.bar": 1}, function(error,index) { 92 | test.ifError(error) 93 | test.deepEqual(index.key,{"foo7.bar": 1}) 94 | test.done() 95 | }) 96 | }, 97 | "retry ensureIndex with dot type and options": function(test) { 98 | collection.ensureIndex({"foo8.bar": 1}, {background: 1}, function(error,index) { 99 | test.ifError(error) 100 | test.deepEqual(index.key,{"foo8.bar": 1}) 101 | test.done() 102 | }) 103 | }, 104 | 105 | "close connection": function(test) { 106 | db.server.close() 107 | test.done() 108 | } 109 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 19 | 20 | 22 | 23 | 24 | 40 | 41 | 43 | 44 | http://www.w3.org/1999/xhtml 45 | 46 | 47 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 84 | 101 | 104 | 105 | 106 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /test/collection1000.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var db,collection 6 | 7 | module.exports = { 8 | "create connection": function(test) { 9 | db = new Mongolian('mongo://localhost/mongolian_test', { log:false }) 10 | collection = db.collection('test_1000') 11 | db.dropDatabase(function(error) { 12 | test.ifError(error) 13 | test.done() 14 | }) 15 | }, 16 | 17 | "insert 1000 documents": function(test) { 18 | var array = [] 19 | for (var i=0; i<1000; i++) { 20 | array.push({ 21 | i:i, 22 | even:(i%2) == 0 23 | }) 24 | } 25 | collection.insert(array, function(error,insertedRows) { 26 | test.ifError(error) 27 | test.equal(insertedRows.length, 1000) 28 | test.done() 29 | }) 30 | }, 31 | "count": function(test) { 32 | collection.find().count(function(error, count) { 33 | test.ifError(error) 34 | test.equal(count, 1000) 35 | test.done() 36 | }) 37 | }, 38 | "size": function(test) { 39 | collection.find().size(function(error, count) { 40 | test.ifError(error) 41 | test.equal(count, 1000) 42 | test.done() 43 | }) 44 | }, 45 | "forEach counter": function(test) { 46 | var counter = 0 47 | collection.find().forEach(function(item) { 48 | counter++ 49 | }, function(error) { 50 | test.ifError(error) 51 | test.equal(counter, 1000) 52 | test.done() 53 | }) 54 | }, 55 | "group check": function (test) { 56 | collection.group({ 57 | 'ns': 'test_1000', 58 | 'key':'even', 59 | 'initial': {evenCount:0, oddCount: 0, total:0}, 60 | '$reduce': function (doc, out) { 61 | ++out[doc.even ? 'evenCount' : 'oddCount']; 62 | ++out.total; 63 | }, 64 | finalize: function (out) {} 65 | }, function (error,group) { 66 | var ret = group.retval[0]; 67 | test.ifError(error); 68 | test.equal(ret.total, 1000); 69 | test.equal(ret.evenCount, 500); 70 | test.equal(ret.oddCount,500); 71 | test.done(); 72 | }); 73 | }, 74 | "mapped forEach": function(test) { 75 | var counter = 0 76 | collection.find().map(function(item) { 77 | return item.i 78 | }).forEach(function(item) { 79 | counter += item 80 | }, function(error) { 81 | test.ifError(error) 82 | test.equal(counter, 499500) 83 | test.done() 84 | }) 85 | }, 86 | "sort({i:-1}).count": function(test) { 87 | collection.find().sort({i:-1}).count(function(error, count) { 88 | test.ifError(error) 89 | test.equal(count, 1000) 90 | test.done() 91 | }) 92 | }, 93 | "sort({i:-1}).size": function(test) { 94 | collection.find().sort({i:-1}).size(function(error, count) { 95 | test.ifError(error) 96 | test.equal(count, 1000) 97 | test.done() 98 | }) 99 | }, 100 | "limit(50).count": function(test) { 101 | collection.find().limit(50).count(function(error, count) { 102 | test.ifError(error) 103 | test.equal(count, 1000) 104 | test.done() 105 | }) 106 | }, 107 | "limit(50).size": function(test) { 108 | collection.find().limit(50).size(function(error, count) { 109 | test.ifError(error) 110 | test.equal(count, 50) 111 | test.done() 112 | }) 113 | }, 114 | "limit(50).skip(50).count": function(test) { 115 | collection.find().limit(50).skip(50).count(function(error, count) { 116 | test.ifError(error) 117 | test.equal(count, 1000) 118 | test.done() 119 | }) 120 | }, 121 | "limit(50).skip(50).size": function(test) { 122 | collection.find().limit(50).skip(50).size(function(error, count) { 123 | test.ifError(error) 124 | test.equal(count, 50) 125 | test.done() 126 | }) 127 | }, 128 | "limit(50).skip(990).count": function(test) { 129 | collection.find().limit(50).skip(990).count(function(error, count) { 130 | test.ifError(error) 131 | test.equal(count, 1000) 132 | test.done() 133 | }) 134 | }, 135 | "limit(50).skip(990).size": function(test) { 136 | collection.find().limit(50).skip(990).size(function(error, count) { 137 | test.ifError(error) 138 | test.equal(count, 10) 139 | test.done() 140 | }) 141 | }, 142 | "sort({i:-1}).limit(1000).size": function(test) { 143 | collection.find().sort({i:-1}).limit(1000).toArray(function(error, array) { 144 | test.ifError(error) 145 | test.equal(array.length, 1000) 146 | test.done() 147 | }) 148 | }, 149 | 150 | "close connection": function(test) { 151 | db.server.close() 152 | test.done() 153 | } 154 | } -------------------------------------------------------------------------------- /examples/mongolian_trainer.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var fs = require("fs") 4 | 5 | var Mongolian = require('mongolian') 6 | 7 | var server = new Mongolian 8 | 9 | var db = server.db("mongolian_trainer"), 10 | small = db.collection("small"), 11 | medium = db.collection("medium"), 12 | large = db.collection("large") 13 | 14 | //db.dropDatabase(asyncLog("dropped database")) 15 | 16 | small.ensureIndex({foo:1},asyncLog("ensuredIndex!")) 17 | medium.ensureIndex({foo:1}) 18 | large.ensureIndex({foo:1}) 19 | 20 | // Adds some junk to the database 21 | function fillCollection(collection, max, callback) { 22 | collection.count(function(err,count) { 23 | console.log(collection+" count = "+count) 24 | while (count < max) { 25 | var toInsert = [] 26 | while (count < max) { 27 | toInsert.push({ foo: Math.random(), index: count++ }) 28 | if (toInsert.length == 300) { 29 | break 30 | } 31 | } 32 | console.log("inserting "+toInsert.length+" document(s) into "+collection) 33 | collection.insert(toInsert) 34 | } 35 | callback() 36 | }) 37 | } 38 | 39 | // Creates function(err,value) that prints out the result to console 40 | function asyncLog(prefix) { 41 | return function(err,value) { 42 | if (err) { 43 | console.warn("Error getting "+prefix+': '+err, err.stack) 44 | } else { 45 | console.log(prefix+':',value) 46 | } 47 | } 48 | } 49 | 50 | 51 | fillCollection(small, 50, function() { 52 | small.find().limit(5).toArray(asyncLog('small find limit 5')) 53 | small.find({foo:{$lte:0.5}},{foo:true,_id:false}).limit(49).toArray(asyncLog('small find foo<0.5 limit 49')) 54 | small.find().limit(100).sort({foo:1}).toArray(asyncLog('small find sorted limit 100')) 55 | small.find().batchSize(10).toArray(asyncLog('small find all, batchsize=10')) 56 | }) 57 | fillCollection(medium, 500, function() { 58 | medium.findOne(asyncLog('medium findOne')) 59 | }) 60 | fillCollection(large, 50000, function() { 61 | large.findOne({foo:{$gt:0.5}}, asyncLog('large foo>0.5 findOne')) 62 | 63 | }) 64 | 65 | // Get a list of databases on this server 66 | server.dbNames(asyncLog("db names")) 67 | 68 | // Get a list of collections on this database (should be [ 'system.indexes', 'small', 'medium', 'large' ]) 69 | db.collectionNames(asyncLog("collection names")) 70 | 71 | ////// Map reduce 72 | large.mapReduce( 73 | function map() { 74 | emit('count', { 75 | count:1, 76 | sum: this.foo 77 | }) 78 | }, 79 | function reduce(key, values) { 80 | var count = 0, sum = 0 81 | values.forEach(function(value) { 82 | count += value.count 83 | sum += values.sum 84 | }) 85 | return { 86 | count:count, 87 | sum:sum 88 | } 89 | },{ 90 | out: 'periscope' 91 | },function (error, result) { 92 | result.find().toArray(asyncLog('map reduce result')) 93 | } 94 | ) 95 | 96 | 97 | 98 | ////// Grid FS 99 | 100 | var gridfs = db.gridfs() 101 | 102 | // Create new file write stream 103 | var stream = gridfs.create({ 104 | filename:"License", 105 | contentType: "text/plain" 106 | }).writeStream() 107 | 108 | // Pipe license file to gridfile 109 | fs.createReadStream(__dirname+'/../LICENSE').pipe(stream) 110 | 111 | // Read file back from gridfs 112 | stream.on('close', function() { 113 | gridfs.findOne("License", function (err, file) { 114 | if (err) throw err 115 | console.log("opened file:",file) 116 | var read = file.readStream() 117 | read.on('data', function (chunk) { 118 | console.log("chunk["+chunk.length+"] = <"+chunk+">") 119 | }) 120 | read.on('end', function() { 121 | console.log("all done") 122 | }) 123 | }) 124 | }) 125 | 126 | 127 | // Create new file write stream 128 | var stream2 = gridfs.create({ 129 | filename:"License", 130 | contentType: "text/plain" 131 | }).writeStream() 132 | 133 | // Pipe license file to gridfile 134 | fs.createReadStream(__dirname+'/../LICENSE').pipe(stream2) 135 | 136 | // Read file back from gridfs 137 | stream2.on('close', function() { 138 | gridfs.findOne("License", function (err, file) { 139 | if (err) throw err 140 | console.log("opened file:",file) 141 | var read = file.readStream() 142 | read.on('data', function (chunk) { 143 | console.log("chunk["+chunk.length+"] = <"+chunk+">") 144 | }) 145 | read.on('end', function() { 146 | console.log("all done") 147 | }) 148 | }) 149 | }) 150 | 151 | // Generates alphabet buffers 152 | function funBuffer(size) { 153 | var buffer = new Buffer(size) 154 | for (var i=0; i 100 ? chunk.slice(0,100) + "..." : chunk)+">") 172 | }) 173 | read.on('end', function() { 174 | console.log("all done") 175 | }) 176 | }) 177 | }) 178 | 179 | stream3.write(new Buffer("hello world 100")) 180 | stream3.write(funBuffer(100)) 181 | stream3.write(new Buffer("hello world 10000")) 182 | stream3.write(funBuffer(10000)) 183 | stream3.write(new Buffer("hello world 500000")) 184 | stream3.write(funBuffer(500000)) 185 | stream3.write(new Buffer("hello world 500000 again")) 186 | stream3.write(funBuffer(500000)) 187 | stream3.end() 188 | -------------------------------------------------------------------------------- /test/collection1.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var db,collection 6 | 7 | module.exports = { 8 | "create connection": function(test) { 9 | db = new Mongolian('mongo://localhost/mongolian_test', { log:false }) 10 | collection = db.collection('test') 11 | db.dropDatabase(function(error) { 12 | test.ifError(error) 13 | test.done() 14 | }) 15 | }, 16 | 17 | "can insert into 'test'": function (test) { 18 | collection.insert({ name:'hello world' }, function(error, insertedRow) { 19 | test.ifError(error) 20 | test.equal(insertedRow.name, "hello world") 21 | test.ok(insertedRow._id) 22 | 23 | collection.findOne(function(error, foundRow) { 24 | test.ifError(error) 25 | test.equal(foundRow._id.toString(), insertedRow._id.toString()) 26 | test.equal(foundRow.name, "hello world") 27 | 28 | collection.findOne({ 29 | _id:new Mongolian.ObjectId(insertedRow._id.toString()) 30 | }, function(error, foundRow) { 31 | test.ifError(error) 32 | test.equal(foundRow._id.toString(), insertedRow._id.toString()) 33 | test.equal(foundRow.name, "hello world") 34 | 35 | test.done() 36 | }) 37 | 38 | }) 39 | }) 40 | }, 41 | "can insert and find by _id": function (test) { 42 | collection.insert({ name:'hello world' }, function(error, insertedRow) { 43 | test.ifError(error) 44 | collection.findOne({ 45 | _id:new Mongolian.ObjectId(insertedRow._id.toString()) 46 | }, function(error, foundRow) { 47 | test.ifError(error) 48 | test.equal(foundRow._id.toString(), insertedRow._id.toString()) 49 | test.equal(foundRow.name, "hello world") 50 | 51 | test.done() 52 | }) 53 | }) 54 | }, 55 | "can insert and update by _id": function (test) { 56 | collection.insert({ name:'hello sun' }, function(error, insertedRow) { 57 | test.ifError(error) 58 | test.ok(insertedRow) 59 | 60 | collection.update({name:'hello sun'}, {age:45}, function(error, rowsUpdated) { 61 | test.ifError(error) 62 | test.equal(rowsUpdated, 1) 63 | 64 | collection.findOne({ 65 | _id:new Mongolian.ObjectId(insertedRow._id.toString()) 66 | }, function(error, row) { 67 | test.ifError(error) 68 | test.equal(row.age, 45) 69 | test.equal(row.name, undefined) 70 | test.done() 71 | }) 72 | }) 73 | }) 74 | }, 75 | "can insert and remove by _id": function (test) { 76 | collection.insert({ name:'hello moon' }, function(error, insertedRow) { 77 | test.ifError(error) 78 | test.ok(insertedRow) 79 | 80 | collection.remove({name:'hello moon'}, function(error, rowsDeleted) { 81 | test.ifError(error) 82 | test.equal(rowsDeleted, 1) 83 | 84 | collection.findOne({ 85 | _id:new Mongolian.ObjectId(insertedRow._id.toString()) 86 | }, function(error, row) { 87 | test.ifError(error) 88 | test.equal(row, undefined) 89 | test.done() 90 | }) 91 | }) 92 | }) 93 | }, 94 | "can insert and remove multiple by _id": function (test) { 95 | collection.insert([ 96 | { name:'hello sky' }, 97 | { name:'hello sky' }, 98 | { name:'hello sky' }, 99 | { name:'hello sky' }, 100 | { name:'hello sky' } 101 | ], function(error, insertedRow) { 102 | test.ifError(error) 103 | test.ok(insertedRow) 104 | 105 | collection.remove({name:'hello sky'}, function(error, rowsDeleted) { 106 | test.ifError(error) 107 | test.equal(rowsDeleted, 5) 108 | 109 | collection.find({name:'hello sky'}).count(function(error, count) { 110 | test.ifError(error) 111 | test.equal(count, 0) 112 | test.done() 113 | }) 114 | }) 115 | }) 116 | }, 117 | "test updateAll({name:'hello world'}, {age:45}) fails": function(test) { 118 | collection.updateAll({}, {age:45}, function(error) { 119 | test.ok(error) 120 | test.equal(error.message, "Server Error: multi update only works with $ operators") 121 | test.done() 122 | }) 123 | }, 124 | "saving into 'test'": function(test) { 125 | collection.save({ name:'hello there' }, function(error, insertedRow) { 126 | test.ifError(error) 127 | test.equal(insertedRow.name, "hello there") 128 | test.ok(insertedRow._id) 129 | 130 | insertedRow.name = "awesome sauce" 131 | collection.save(insertedRow, function(error, savedRow) { 132 | test.ifError(error) 133 | test.ok(insertedRow._id) 134 | test.equal(insertedRow.name, "awesome sauce") 135 | test.equal(insertedRow, savedRow) 136 | collection.findOne({_id:insertedRow._id}, function(error, foundRow) { 137 | test.equal(foundRow.name, "awesome sauce") 138 | foundRow.name = "awesome hot sauce" 139 | collection.save(foundRow) 140 | test.equal(foundRow.name, "awesome hot sauce") 141 | test.done() 142 | }) 143 | }) 144 | }) 145 | }, 146 | "saving with bad id": function(test) { 147 | var id = new Mongolian.ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA") 148 | collection.save({ _id:id, name:'hello there' }, function(error, insertedRow) { 149 | test.ifError(error) 150 | test.equal(insertedRow.name, "hello there") 151 | test.equal(insertedRow._id.toString(), id.toString()) 152 | collection.findOne({_id:insertedRow._id}, function(error, foundRow) { 153 | test.equal(foundRow.name, "hello there") 154 | test.equal(foundRow._id.toString(), id.toString()) 155 | test.done() 156 | }) 157 | }) 158 | }, 159 | 160 | "db.collectionNames contains 'test'": function(test) { 161 | db.collectionNames(function(error,names) { 162 | test.ifError(error) 163 | test.ok(names.some(function(name) { 164 | return name == "test" 165 | }), "'test' found") 166 | test.ok(names.some(function(name) { 167 | return name == "system.indexes" 168 | }), "'system.indexes' found") 169 | test.done() 170 | }) 171 | }, 172 | 173 | "close connection": function(test) { 174 | db.server.close() 175 | test.done() 176 | } 177 | } -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | var crypto = require('crypto') 3 | var util = require('util') 4 | 5 | var safetyNet = require('./util').safetyNet 6 | var MongolianCollection = require('./collection') 7 | var MongolianGridFS = require('./gridfs') 8 | 9 | var MongolianDB = module.exports = function(server, name) { 10 | this.server = server 11 | this.name = name 12 | this._collections = {} 13 | this._gridfss = {} 14 | } 15 | 16 | /** 17 | * Get a collection 18 | */ 19 | MongolianDB.prototype.collection = function(name) { 20 | return this._collections[name] || 21 | (this._collections[name] = new MongolianCollection(this, name)) 22 | } 23 | 24 | /** 25 | * Get a gridfs 26 | */ 27 | MongolianDB.prototype.gridfs = function(name) { 28 | name = name || "fs" 29 | return this._gridfss[name] || 30 | (this._gridfss[name] = new MongolianGridFS(this, name)) 31 | } 32 | 33 | /** 34 | * Get list of collection names 35 | */ 36 | MongolianDB.prototype.collectionNames = function(callback) { 37 | if (typeof callback !== 'function') throw new Error("callback is not a function!") 38 | // Remove "dbname." from front 39 | var prefixLength = this.name.length+1 40 | this.collection("system.namespaces").find().toArray(safetyNet(callback, function(namespaces) { 41 | callback( 42 | null, 43 | // Extract name from namespace object 44 | namespaces.map(function(namespace) { 45 | return namespace.name.substring(prefixLength) 46 | // Filter out internal collections 47 | }).filter(function (name) { 48 | return !/\$/.test(name) 49 | }) 50 | ) 51 | })) 52 | } 53 | 54 | /** 55 | * Executes a command (equivalent to db.collection("$cmd").findOne(query, callback)) 56 | */ 57 | MongolianDB.prototype.runCommand = function(query, callback) { 58 | if (typeof query === 'string') { 59 | var n = {} 60 | n[query] = 1 61 | query = n 62 | } 63 | this.collection('$cmd').findOne(query, safetyNet(callback, function(result) { 64 | if (!result.ok || result.err || result.$err) { 65 | var error = new Error("Server Error: " + (result.err || result.$err || util.inspect(result))) 66 | error.result = result 67 | callback && callback(error) 68 | } else { 69 | callback && callback(null, result) 70 | } 71 | })) 72 | } 73 | 74 | /** 75 | * Sends a command to the server with authentication queueing 76 | */ 77 | MongolianDB.prototype.sendCommand = function(command, callback) { 78 | if (!this._authorize || this._authorized || (command.query && (command.query.getnonce || command.query.authenticate))) { 79 | this.server.sendCommand(command,callback) 80 | } else if (this._authorizeError) { 81 | callback(this._authorizeError) 82 | } else { 83 | this._authorizeQueue.push([command,callback]) 84 | } 85 | } 86 | 87 | function md5hash(string) { 88 | return crypto.createHash('md5').update(string).digest('hex') 89 | } 90 | 91 | /** 92 | * Adds user authentication to this database 93 | */ 94 | MongolianDB.prototype.addUser = function(username, password, readOnly, callback) { 95 | if (!callback && typeof readOnly === 'function') { 96 | callback = readOnly 97 | readOnly = false 98 | } 99 | readOnly = readOnly || false 100 | var users = this.collection("system.users") 101 | 102 | users.findOne({ user:username }, safetyNet(callback, function(user) { 103 | user = user || { user:username } 104 | user.readOnly = readOnly 105 | user.pwd = md5hash(username + ":mongo:" + password) 106 | console.log("saving user: ",user) 107 | users.save(user, callback) 108 | })) 109 | } 110 | 111 | /** 112 | * Removes a user (you'll need to be authenticated first) 113 | */ 114 | MongolianDB.prototype.removeUser = function(username, callback) { 115 | this.collection("system.users").remove({ user:username }, callback) 116 | } 117 | 118 | /** 119 | * Authenticate the database 120 | */ 121 | MongolianDB.prototype.auth = function(username, password, callback) { 122 | var self = this 123 | self._authorize = function(callback) { 124 | if (!self._authorizeQueue) self._authorizeQueue = [] 125 | delete self._authorizeError 126 | self.runCommand({ getnonce:1 }, safetyNet(callback, function(nonceResult) { 127 | self.runCommand({ 128 | authenticate:1, 129 | user:username, 130 | nonce:nonceResult.nonce, 131 | key:md5hash(nonceResult.nonce + username + md5hash(username + ":mongo:" + password)) 132 | }, function (error) { 133 | var queue = self._authorizeQueue 134 | delete self._authorizeQueue 135 | if (error) { 136 | self._authorizeError = error 137 | self.server.log.warn("Authentication failed for `"+username+"` @ "+self+": "+error) 138 | } else { 139 | self._authorized = true 140 | self.server.log.info("Authenticated `"+username+"` @ "+self) 141 | } 142 | if (callback) callback(error) 143 | queue && queue.forEach(function(queued) { 144 | if (error) { 145 | queued[1] && queued[1](error) 146 | } else { 147 | self.server.sendCommand(queued[0], queued[1]) 148 | } 149 | }) 150 | }) 151 | })) 152 | } 153 | self._authorize(callback) 154 | } 155 | 156 | /** 157 | * !!! Removes the entire database !!! 158 | */ 159 | MongolianDB.prototype.dropDatabase = function(callback) { 160 | this.runCommand({ dropDatabase:1 }, callback) 161 | } 162 | 163 | MongolianDB.prototype.eval = function(execFunction, args, callback) { 164 | var command = { $eval:execFunction } 165 | if (arguments.length > 1) { 166 | if (typeof arguments[arguments.length-1] === 'function') { 167 | command.args = Array.prototype.slice.call(arguments, 1, arguments.length-1) 168 | callback = arguments[arguments.length-1] 169 | } else { 170 | command.args = Array.prototype.slice.call(arguments, 1) 171 | callback = undefined 172 | } 173 | } 174 | this.runCommand(command, safetyNet(callback,function(result) { 175 | callback(null, result.retval) 176 | })) 177 | } 178 | 179 | /** 180 | * Gets the last error message from the server on this connection 181 | * 182 | * Note: this will return an async style error - i.e. an error in the *first* callback function argument 183 | */ 184 | MongolianDB.prototype.lastError = function(replicants, timeout, callback) { 185 | if (!callback && typeof timeout === 'function') { 186 | callback = timeout 187 | timeout = undefined 188 | } 189 | if (!callback && typeof replicants === 'function') { 190 | callback = replicants 191 | replicants = undefined 192 | } 193 | if (typeof callback !== 'function') throw new Error("callback is not a function!") 194 | 195 | var command = { getlasterror:1 } 196 | if (replicants) command.w = replicants 197 | if (timeout) command.wtimeout = timeout 198 | 199 | this.runCommand(command, callback) 200 | } 201 | 202 | MongolianDB.prototype.toString = function() { 203 | return this.server + "/" + this.name 204 | } -------------------------------------------------------------------------------- /test/gridfs.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | 3 | var Mongolian = require('../mongolian') 4 | 5 | var db,collection 6 | 7 | function testRandomFile(numberOfBytes, writeChunkSize, chunkSize) { 8 | return function (test) { 9 | // Delete any previous file 10 | gridfs.findOne('random',function(error,oldFile) { 11 | test.ifError(error) 12 | if (oldFile) oldFile.remove() 13 | 14 | // Generate random buffer 15 | var fileBuffer = new Buffer(numberOfBytes) 16 | for (var i=0; i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /lib/cursor.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | var safetyNet = require('./util').safetyNet 3 | var buffalo = require('buffalo') 4 | var mongo = buffalo.mongo 5 | 6 | var MongolianCursor = module.exports = function(collection, criteria, fields) { 7 | this.server = collection.db.server 8 | this.db = collection.db 9 | this.collection = collection 10 | this.criteria = criteria 11 | this.fields = fields 12 | this._retrieved = 0 13 | } 14 | /** 15 | * Resets the cursor to its initial state (this will cause the query to be executed again) 16 | */ 17 | MongolianCursor.prototype.reset = function() { 18 | this.close() 19 | this._retrieved = 0 20 | delete this._currentBatch 21 | } 22 | 23 | /** 24 | * Closes the cursor 25 | */ 26 | MongolianCursor.prototype.close = function(callback) { 27 | if (this._currentBatch && this._currentBatch.cursor) { 28 | this.db.sendCommand(mongo.serializeKillCursors([this._currentBatch.cursor]), callback) 29 | delete this._currentBatch.cursor 30 | } 31 | } 32 | 33 | /** 34 | * Adds a special value to this query 35 | */ 36 | MongolianCursor.prototype.addSpecial = function(name, value) { 37 | if (this._currentBatch) throw new Error("Cannot modify cursor - query already executed") 38 | if (!this._specials) { 39 | this._specials = { $query: this.criteria || {} } 40 | } 41 | this._specials[name] = value 42 | return this 43 | } 44 | 45 | /** 46 | * Specify the number of documents to retrieve per network request 47 | */ 48 | MongolianCursor.prototype.batchSize = function(size) { 49 | if (this._currentBatch) throw new Error("Cannot modify cursor - query already executed") 50 | if (size <= 1) throw new Error("Batch size must be > 1") 51 | this._batchSize = size 52 | return this 53 | } 54 | /** 55 | * Limit the total number of documents to get back from the server 56 | */ 57 | MongolianCursor.prototype.limit = function(limit) { 58 | if (this._currentBatch) throw new Error("Cannot modify cursor - query already executed") 59 | this._limit = limit 60 | return this 61 | } 62 | /** 63 | * Number of documents to skip before returning values (for paging) 64 | */ 65 | MongolianCursor.prototype.skip = function(skip) { 66 | if (this._currentBatch) throw new Error("Cannot modify cursor - query already executed") 67 | this._skip = skip 68 | return this 69 | } 70 | /** 71 | * Set document mapper (convert/modify raw BSON objects to something else) 72 | */ 73 | MongolianCursor.prototype.map = function(mapper) { 74 | if (this._currentBatch) throw new Error("Cannot modify cursor - query already executed") 75 | this._mapper = mapper 76 | return this 77 | } 78 | /** 79 | * Specify sort using MongoDB's { "ascendingField": 1, "descendingField": -1 } format 80 | * Shorthand for cursor._addSpecial("orderby", sort) 81 | */ 82 | MongolianCursor.prototype.sort = function(sort) { 83 | return this.addSpecial("$orderby", sort) 84 | } 85 | 86 | /** 87 | * Shorthand for cursor._addSpecial("$snapshot", true) 88 | */ 89 | MongolianCursor.prototype.snapshot = function() { 90 | return this.addSpecial("$snapshot", true) 91 | } 92 | 93 | /** 94 | * Shorthand for cursor._addSpecial("$explain", true) 95 | */ 96 | MongolianCursor.prototype.explain = function() { 97 | return this.addSpecial("$explain", true) 98 | } 99 | 100 | /** 101 | * Returns the next raw BSON result, or null if there are no more. No mapping/transformation is performed. 102 | */ 103 | MongolianCursor.prototype.nextBatch = function(callback) { 104 | if (typeof callback !== 'function') throw new Error("callback is not a function!") 105 | var self = this 106 | if (self._currentIndex && self._currentIndex < self._currentBatch.documents.length) throw new Error("nextBatch cannot be mixed with next") 107 | var filterBatch = safetyNet(callback, function(batch) { 108 | if ((batch.flags & 2) != 0) { 109 | var error = new Error("Query failure: "+batch.documents[0].$err) 110 | error.result = batch.documents[0] 111 | return callback(error) 112 | } 113 | if ((batch.flags & 1) != 0) { 114 | return callback(new Error("Cursor not found")) 115 | } 116 | // self.server.log.debug("<<<<<<---",batch.numberReturned,batch.cursor) 117 | self._currentIndex = 0 118 | self._currentBatch = batch 119 | self._retrieved += batch.documents.length 120 | if (batch.cursor && batch.cursor.isZero()) { 121 | delete batch.cursor 122 | } else if (self._limit && self._retrieved >= self._limit) { 123 | self.close() 124 | } 125 | callback(null, batch) 126 | }) 127 | 128 | 129 | var retrieveCount = self._batchSize ? 130 | Math.min(self._batchSize, self._limit - self._retrieved) : 131 | self._limit 132 | 133 | if (!self._currentBatch) { 134 | var query = self._specials || self.criteria || {} 135 | var queryCommand = mongo.serializeQuery( 136 | self.collection.fullName, 137 | 0, 138 | self._skip, 139 | retrieveCount, 140 | query, 141 | self.fields 142 | ) 143 | queryCommand.query = query 144 | self.db.sendCommand(queryCommand, filterBatch) 145 | } else if (self._currentBatch.cursor) { 146 | var getMoreCommand = mongo.serializeGetMore( 147 | self.collection.fullName, 148 | retrieveCount, 149 | self._currentBatch.cursor 150 | ) 151 | self.db.sendCommand(getMoreCommand, filterBatch) 152 | } else { 153 | callback(null, null) 154 | } 155 | } 156 | 157 | /** 158 | * Returns the next available document, or undefined if there is none 159 | */ 160 | MongolianCursor.prototype.next = function(callback) { 161 | if (typeof callback !== 'function') throw new Error("callback is not a function!") 162 | var self = this 163 | // We have a retrieved batch that hasn't been exhausted 164 | if (self._currentBatch && self._currentIndex < self._currentBatch.documents.length) { 165 | var document = self._currentBatch.documents[self._currentIndex++] 166 | // self.server.log.debug("<<<<<<---",document) 167 | callback(null, self._mapper ? self._mapper(document) : document) 168 | // We don't have a batch or the cursor hasn't been closed yet 169 | } else if (!self._currentBatch || self._currentBatch.cursor) { 170 | self.nextBatch(safetyNet(callback,function() { 171 | self.next(callback) 172 | })) 173 | // We have nothing left 174 | } else { 175 | callback(null) 176 | } 177 | } 178 | 179 | /** 180 | * Calls callback(doc) on every document in this cursor. On completion or error, finalCallback([err]) is called. 181 | */ 182 | MongolianCursor.prototype.forEach = function(callback, finalCallback) { 183 | if (this._retrieved) throw new Error("forEach must be called on an unused cursor or reset cursor") 184 | var self = this 185 | var handleNext = safetyNet(finalCallback,function(batch) { 186 | if (batch) { 187 | var documents = batch.documents 188 | var length = documents.length 189 | for (var i = 0; i < length; i++) { 190 | callback(self._mapper ? self._mapper(documents[i]) : documents[i]) 191 | } 192 | self.nextBatch(handleNext) 193 | } else { 194 | finalCallback && finalCallback() 195 | } 196 | }) 197 | self.nextBatch(handleNext) 198 | } 199 | /** 200 | * Combines all the documents from this cursor into a single array 201 | */ 202 | MongolianCursor.prototype.toArray = function(callback) { 203 | if (this._retrieved) throw new Error("toArray must be called on an unused cursor or reset cursor") 204 | var self = this 205 | var array = [] 206 | var handleNext = safetyNet(callback,function(batch) { 207 | if (batch) { 208 | array.push.apply(array, batch.documents) 209 | self.nextBatch(handleNext) 210 | } else { 211 | // self.server.log.debug("<<<<<<---",array) 212 | callback(null, self._mapper ? array.map(self._mapper) : array) 213 | } 214 | }) 215 | self.nextBatch(handleNext) 216 | } 217 | 218 | /** 219 | * Returns the number of rows that match this criteria, ignoring skip and limits 220 | */ 221 | MongolianCursor.prototype.count = function(callback) { 222 | _count(this, false, callback) 223 | } 224 | 225 | /** 226 | * Returns the minimum of cursor.count(), honoring skip and limit 227 | */ 228 | MongolianCursor.prototype.size = function(callback) { 229 | _count(this, true, callback) 230 | } 231 | 232 | ////////////////////////////////////////////////////////////////////////////////// 233 | // Internal 234 | 235 | function _count(self, usingSkipAndLimit, callback) { 236 | if (typeof callback !== 'function') throw new Error("callback is not a function!") 237 | var query = { count: self.collection.name } 238 | if (self.criteria) { 239 | query.query = self.criteria 240 | } 241 | if (usingSkipAndLimit) { 242 | query.skip = self._skip || 0 243 | query.limit = self._limit 244 | } 245 | self.db.runCommand(query, safetyNet(callback, function(result) { 246 | callback(null, result.n) 247 | })) 248 | } 249 | -------------------------------------------------------------------------------- /lib/gridfile.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | var stream = require('stream') 3 | var util = require('util') 4 | var crypto = require('crypto') 5 | var Waiter = require('waiter') 6 | var buffalo = require('buffalo') 7 | 8 | var safetyNet = require('./util').safetyNet 9 | var callback = require('./util').callback 10 | var Long = buffalo.Long 11 | 12 | var MongolianGridFile = module.exports = function(gridfs, document) { 13 | this.gridfs = gridfs 14 | this._id = document._id 15 | this.chunkSize = flattenLong(document.chunkSize || 256 * 1024) 16 | this.length = flattenLong(document.length) 17 | this.filename = document.filename 18 | this.aliases = document.aliases 19 | this.md5 = document.md5 20 | this.contentType = document.contentType 21 | if (document.uploadDate) this.uploadDate = document.uploadDate 22 | if (document.metadata) this.metadata = document.metadata 23 | } 24 | /** 25 | * Saves all the metadata associated with this file 26 | */ 27 | MongolianGridFile.prototype.save = function(callback) { 28 | if (!this._id) this._id = new buffalo.ObjectId 29 | 30 | var document = { 31 | _id: this._id, 32 | chunkSize: this.chunkSize, 33 | length: this.length, 34 | filename: this.filename, 35 | md5: this.md5, 36 | contentType: this.contentType, 37 | uploadDate: this.uploadDate 38 | } 39 | // Optional fields 40 | if (this.aliases) document.aliases = this.aliases 41 | if (this.metadata) document.metadata = this.metadata 42 | this.gridfs.files.save(document, callback) 43 | } 44 | 45 | /** 46 | * Creates a Writeable Stream for this file 47 | */ 48 | MongolianGridFile.prototype.writeStream = function() { 49 | return new MongolianGridFileWriteStream(this) 50 | } 51 | /** 52 | * Retrieves the nth chunk from this file 53 | */ 54 | MongolianGridFile.prototype.getChunk = function(chunkIndex, callback) { 55 | if (!this._id) throw new Error("Can only read files in the db") 56 | this.gridfs.chunks.findOne({ files_id:this._id, n:chunkIndex }, { data:true }, callback) 57 | } 58 | /** 59 | * Returns the number of chunks this file *should* have 60 | */ 61 | MongolianGridFile.prototype.chunkCount = function() { 62 | var chunkCount = divideCeilLongs(this.length, this.chunkSize) 63 | if (chunkCount instanceof Long) throw new Error("More chunks than we can handle!") 64 | return chunkCount 65 | } 66 | 67 | /** 68 | * Creates a Readable Stream for this file (bind to the 'data' 'error', and 'done' events) 69 | */ 70 | MongolianGridFile.prototype.readStream = function() { 71 | return new MongolianGridFileReadStream(this) 72 | } 73 | 74 | /** 75 | * Remove this file from the gridfs 76 | */ 77 | MongolianGridFile.prototype.remove = function(callback) { 78 | var waiter = new Waiter 79 | this.gridfs.files.remove({ _id:this._id }, waiter()) 80 | this.gridfs.chunks.remove({ files_id:this._id }, waiter()) 81 | waiter.waitForAll(callback) 82 | } 83 | 84 | MongolianGridFile.prototype.toString = function() { 85 | return this.gridfs + ":" + this.document 86 | } 87 | 88 | ////////////////////////////////////////////////////////////////////////////////// 89 | // Internal 90 | 91 | /** Converts Numbers to Longs */ 92 | function toLong(l) { 93 | return l instanceof Long ? l : Long.fromNumber(l) 94 | } 95 | /** Converts a Long to a Number if possible */ 96 | function flattenLong(l) { 97 | return l instanceof Long && !l.high_ ? l.low_ : l 98 | } 99 | function addLongs(a,b) { 100 | return flattenLong(toLong(a).add(toLong(b))) 101 | 102 | } 103 | function divideCeilLongs(a,b) { 104 | if (a instanceof Long || b instanceof Long) { 105 | var r = toLong(a).div(toLong(b)) 106 | return flattenLong(r.multiply(b).lessThan(a) ? r.add(Long.ONE) : r) 107 | } 108 | return Math.ceil(a / b) 109 | } 110 | 111 | 112 | /** 113 | * Treats a MongolianGridFile as a node.js Writeable Stream 114 | */ 115 | function MongolianGridFileWriteStream(file) { 116 | if (file.chunkSize instanceof Long) throw new Error("Long (64bit) chunkSize unsupported") 117 | if (file.chunkSize <= 0) throw new Error("File has invalid chunkSize: "+file.chunkSize) 118 | stream.Stream.call(this) 119 | this.file = file 120 | this.writable = true 121 | this.encoding = 'binary' 122 | this._hash = crypto.createHash('md5') 123 | this._chunkIndex = 0 124 | file.length = 0 125 | this._chunkSize = file.chunkSize 126 | } 127 | util.inherits(MongolianGridFileWriteStream,stream.Stream) 128 | 129 | MongolianGridFileWriteStream.prototype.write = function(data, encoding, callback) { 130 | if (!this.writable) throw new Error('Stream not writable') 131 | var file = this.file 132 | if (this._chunkSize != file.chunkSize) throw new Error("Chunk size changed between writes!") 133 | 134 | if (typeof encoding === 'function') { 135 | callback = encoding 136 | encoding = "utf8" 137 | } 138 | if (!Buffer.isBuffer(data)) { 139 | data = new Buffer(data, encoding) 140 | } 141 | 142 | for (var index = 0; index < data.length; ) { 143 | if (!this._partialChunk) { 144 | this._partialChunk = new Buffer(this._chunkSize) 145 | this._partialIndex = 0 146 | } 147 | var copySize = Math.min(this._partialChunk.length - this._partialIndex, data.length - index) 148 | data.copy(this._partialChunk, this._partialIndex, index, index + copySize) 149 | this._hash.update(data.slice(index, index + copySize)) 150 | 151 | this._partialIndex += copySize 152 | index += copySize 153 | file.length = addLongs(file.length, copySize) 154 | 155 | delete file.md5 156 | file.uploadDate = new Date 157 | 158 | if (this._partialIndex === this._partialChunk.length) { 159 | this.flush() 160 | } 161 | } 162 | } 163 | MongolianGridFileWriteStream.prototype.flush = function(callback) { 164 | var waiter = new Waiter 165 | this.file.save(waiter()) 166 | if (this._partialIndex) { 167 | this.file.gridfs.chunks.upsert({ 168 | files_id:this.file._id, 169 | n:this._chunkIndex 170 | },{ 171 | data:this._partialChunk.slice(0, this._partialIndex), 172 | files_id:this.file._id, 173 | n:this._chunkIndex 174 | }, waiter()) 175 | 176 | if (this._partialIndex === this._partialChunk.length) { 177 | this._chunkIndex++ 178 | delete this._partialIndex 179 | delete this._partialChunk 180 | } 181 | } 182 | waiter.waitForAll(callback) 183 | } 184 | MongolianGridFileWriteStream.prototype.end = function(data, encoding) { 185 | if (!this.writable) throw new Error("Stream is not writable") 186 | if (data) this.write(data,encoding) 187 | this.writable = false 188 | if (this.file.length) this.file.md5 = this._hash.digest('hex') 189 | var self = this 190 | this.flush(function(error) { 191 | if (self.destroyed) return 192 | if (error) { 193 | self.emit('error',error) 194 | self.destroy() 195 | } else { 196 | self.emit('close') 197 | } 198 | }) 199 | } 200 | /** 201 | * Destroys the stream, without flushing pending data 202 | */ 203 | MongolianGridFileWriteStream.prototype.destroy = function() { 204 | this.writable = false 205 | this.destroyed = true 206 | } 207 | 208 | /** 209 | * Treats a MongolianGridFile as a node.js Readable Stream 210 | */ 211 | function MongolianGridFileReadStream(file) { 212 | if (!file._id) throw new Error("Can only read files retrieved from the database") 213 | if (file.chunkSize instanceof Long) throw new Error("Long (64bit) chunkSize unsupported") 214 | if (file.chunkSize <= 0) throw new Error("File has invalid chunkSize: "+file.chunkSize) 215 | 216 | stream.Stream.call(this) 217 | 218 | this.file = file 219 | this.readable = true 220 | this._chunkCount = file.chunkCount() 221 | this._chunkIndex = 0 222 | 223 | process.nextTick(this._nextChunk.bind(this)) 224 | } 225 | util.inherits(MongolianGridFileReadStream,stream.Stream) 226 | 227 | MongolianGridFileReadStream.prototype._nextChunk = function() { 228 | var self = this; 229 | if (self._chunkIndex < self._chunkCount) { 230 | var chunkIndex = self._chunkIndex++ 231 | self.file.getChunk(chunkIndex, function(error, chunk) { 232 | if (!self.readable) return 233 | if (error || !chunk) { 234 | self.emit('error', error || new Error("Chunk not found: "+chunkIndex+"/"+self._chunkIndex)) 235 | self.destroy() 236 | } else { 237 | if (self.paused) { 238 | self._pauseData = chunk.data 239 | } else { 240 | self.emit('data', chunk.data) 241 | self._nextChunk() 242 | } 243 | } 244 | }) 245 | } else { 246 | this.readable = false 247 | self.emit('end') 248 | } 249 | } 250 | MongolianGridFileReadStream.prototype.destroy = function() { 251 | this.readable = false 252 | } 253 | MongolianGridFileReadStream.prototype.pause = function() { 254 | if (!this.paused) { 255 | this.paused = true 256 | this.emit('pause') 257 | } 258 | } 259 | MongolianGridFileReadStream.prototype.resume = function() { 260 | if (this.paused) { 261 | this.paused = false 262 | this.emit('resume') 263 | if (this._pauseData) { 264 | this.emit('data', this._pauseData) 265 | delete this._pauseData 266 | this._nextChunk() 267 | } 268 | } 269 | } -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | var Waiter = require('waiter') 3 | var taxman = require('taxman') 4 | var EventEmitter = require('events').EventEmitter 5 | var buffalo = require('buffalo') 6 | var mongo = buffalo.mongo 7 | 8 | var safetyNet = require('./util').safetyNet 9 | var extend = require('./util').extend 10 | var MongolianDB = require('./db') 11 | var Connection = require('./connection') 12 | 13 | // [mongo://][username:password@]hostname[:port][/databasename] 14 | 15 | var UrlMatcher = /^(?:mongo(?:db)?:\/\/)?(?:([^:]+):([^@]+)@)?(.+?)(?::([0-9]+))?(?:\/(.*))?$/ 16 | 17 | function parseUrl(url) { 18 | var match = UrlMatcher.exec(url) 19 | if (!match || !match[3]) throw new Error("Invalid connection string: " + url) 20 | url = {} 21 | if (match[1]) url.user = decodeURIComponent(match[1]) 22 | if (match[2]) url.password = decodeURIComponent(match[2]) 23 | url.host = match[3] || 'localhost' 24 | url.port = (+match[4]) || 27017 25 | if (match[5]) url.database = decodeURIComponent(match[5]) 26 | return url 27 | } 28 | 29 | /** 30 | * Constructs a new MongolianServer object 31 | */ 32 | var Mongolian = module.exports = function(serversOrOptions) { 33 | var inlineDatabase 34 | 35 | this._replicaSet = new EventEmitter 36 | this._servers = [] 37 | this._serverNames = {} 38 | this._dbs = {} 39 | this._callbacks = {} 40 | this._callbackCount = 0 41 | 42 | var self = this 43 | var waitingServers = 0 44 | var scanErrors = [] 45 | 46 | function scanServer(server) { 47 | waitingServers++ 48 | // Swap out the send command method temporarily 49 | self.sendCommand = function(command,callback) { 50 | delete self.sendCommand 51 | server.sendCommand(command,callback) 52 | } 53 | self.db('admin').runCommand({ ismaster:1 }, function(error, result) { 54 | if (error) { 55 | scanErrors.push(error) 56 | } else { 57 | server._type = result.ismaster ? 'primary' : 58 | result.secondary ? 'secondary' : 59 | result.arbiterOnly ? 'arbiter' : 'unknown' 60 | 61 | self.log.debug(server+": Initialized as "+server._type) 62 | result.primary && addServer(parseUrl(result.primary)) 63 | result.hosts && result.hosts.map(parseUrl).forEach(addServer) 64 | 65 | if (result.ismaster) { 66 | self._primary = server 67 | self.log.info(server+": Connected to primary") 68 | self._replicaSet.emit('primary',server) 69 | } else { 70 | server.close() 71 | } 72 | } 73 | waitingServers-- 74 | if (!waitingServers) { 75 | self.log.debug("Finished scanning... primary? " + (self._primary || 'no')) 76 | self._replicaSet.emit('scanned', scanErrors) 77 | scanErrors = [] 78 | } 79 | }) 80 | } 81 | 82 | this._replicaSet.on('lost',function(server) { 83 | if (server != self._primary) return 84 | self.log.warn(server+": Lost primary") 85 | delete self._primary 86 | }) 87 | this._replicaSet.setMaxListeners(0) 88 | 89 | function addServer(url) { 90 | var key = url.host+':'+url.port 91 | if (!self._serverNames[key]) { 92 | self._serverNames[key] = true 93 | var server = new MongolianServer(self, url) 94 | self._servers.push(server) 95 | scanServer(server) 96 | } 97 | } 98 | 99 | // Browse the constructor arguments 100 | for (var i=0; i>> "+require('util').inspect(command,undefined,5,true).slice(0,5000)+'\n'+new Error().stack) 255 | connection.write(command) 256 | })) 257 | } 258 | 259 | MongolianServer.prototype.toString = function() { 260 | return 'mongo://' + this.url.host + ':' + this.url.port 261 | } 262 | 263 | /** 264 | * Closes the current connection, passing the optional error object to any pending request callbacks. 265 | */ 266 | MongolianServer.prototype.close = function(error) { 267 | error = error || new Error("Connection closed") 268 | if (this._connection.value) { 269 | var callbacks = this._callbacks 270 | this._callbacks = {} 271 | this._callbackCount = 0 272 | this._connection.value.close() 273 | this._connection.reset() 274 | delete this._type 275 | for (var requestId in callbacks) { 276 | callbacks[requestId](error) 277 | } 278 | this.mongolian._replicaSet.emit('lost', this) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /lib/collection.js: -------------------------------------------------------------------------------- 1 | /* Mongolian DeadBeef by Marcello Bastea-Forte - zlib license */ 2 | var util = require('util') 3 | var buffalo = require('buffalo') 4 | var mongo = buffalo.mongo 5 | 6 | var safetyNet = require('./util').safetyNet 7 | var extend = require('./util').extend 8 | var MongolianCursor = require('./cursor') 9 | 10 | var MongolianCollection = module.exports = function(db, name) { 11 | this.server = db.server 12 | this.db = db 13 | this.name = name 14 | this.fullName = db.name + "." + name 15 | this._indexCache = {} 16 | } 17 | 18 | /** 19 | * Returns a new MongolianCursor object 20 | */ 21 | MongolianCollection.prototype.find = function(criteria, fields) { 22 | return new MongolianCursor(this, criteria, fields) 23 | } 24 | 25 | /** 26 | * Shorthand for collection.find(criteria).count(callback) 27 | * function(callback): count all documents 28 | * function(criteria, callback): count documents matching criteria 29 | */ 30 | MongolianCollection.prototype.count = function(criteria, callback) { 31 | if (!callback) { 32 | callback = criteria 33 | criteria = undefined 34 | } 35 | this.find(criteria).count(callback) 36 | } 37 | 38 | /** 39 | * Shorthand for collection.find(criteria,fields).limit(1).next(callback) 40 | * 41 | * function(callback) 42 | * function(criteria, callback) 43 | * function(criteria, fields, callback) 44 | */ 45 | MongolianCollection.prototype.findOne = function(criteria, fields, callback) { 46 | if (!callback && typeof fields === 'function') { 47 | callback = fields 48 | fields = undefined 49 | } 50 | if (!callback && typeof criteria === 'function') { 51 | callback = criteria 52 | criteria = undefined 53 | } 54 | this.find(criteria, fields).limit(1).next(callback) 55 | } 56 | 57 | /** 58 | * Insert an object or array of objects 59 | */ 60 | MongolianCollection.prototype.insert = function(object, callback) { 61 | if (!object) throw new Error("No object to insert!") 62 | if (callback && typeof callback !== 'function') throw new Error("callback is not a function!") 63 | var objects = Array.isArray(object) ? object : [object] 64 | 65 | // Assign ids 66 | objects.forEach(function(object) { 67 | if (!object._id) object._id = new buffalo.ObjectId 68 | }) 69 | this.db.sendCommand(mongo.serializeInsert(this.fullName, objects, false)) 70 | if (callback) { 71 | this.db.lastError(safetyNet(callback, function() { 72 | callback(null, object) 73 | })) 74 | } 75 | } 76 | 77 | /** 78 | * Update an existing object 79 | * function(criteria, objNew, callback) 80 | * function(criteria, objNew, upsert, callback) 81 | * function(criteria, objNew, upsert, multi, callback) 82 | */ 83 | MongolianCollection.prototype.update = function(criteria, objNew, upsert, multi, callback) { 84 | if (!callback && typeof multi === 'function') { 85 | callback = multi 86 | multi = false 87 | } 88 | if (!callback && typeof upsert === 'function') { 89 | callback = upsert 90 | upsert = false 91 | } 92 | if (callback && typeof callback !== 'function') throw new Error("callback is not a function!") 93 | 94 | this.db.sendCommand(mongo.serializeUpdate(this.fullName, criteria, objNew, upsert, multi)) 95 | if (callback) { 96 | this.db.lastError(safetyNet(callback, function(result) { 97 | callback(null, result.n) 98 | })) 99 | } 100 | } 101 | 102 | /** 103 | * Shorthand update(criteria, objNew, true, false, callback) 104 | */ 105 | MongolianCollection.prototype.upsert = function(criteria, objNew, callback) { 106 | this.update(criteria, objNew, true, false, callback) 107 | } 108 | 109 | /** 110 | * Shorthand update(criteria, objNew, false, true, callback) 111 | */ 112 | MongolianCollection.prototype.updateAll = function(criteria, objNew, callback) { 113 | this.update(criteria, objNew, false, true, callback) 114 | } 115 | 116 | /** 117 | * Shorthand for update if object has an _id, otherwise insert 118 | */ 119 | MongolianCollection.prototype.save = function(object, callback) { 120 | if (object._id) { 121 | this.upsert({_id:object._id}, object, safetyNet(callback, function(rowsUpdated) { 122 | if (callback) { 123 | if (!rowsUpdated) return callback(new Error("No rows updated!")) 124 | callback(null, object) 125 | } 126 | })) 127 | } else { 128 | this.insert(object, callback) 129 | } 130 | } 131 | /** 132 | * Removes documents from this collection using the given criteria 133 | * function(callback) 134 | * function(criteria, callback) 135 | */ 136 | MongolianCollection.prototype.remove = function(criteria, callback) { 137 | if (!callback && typeof criteria === 'function') { 138 | callback = criteria 139 | criteria = {} 140 | } 141 | if (callback && typeof callback !== 'function') throw new Error("callback is not a function!") 142 | this.db.sendCommand(mongo.serializeDelete(this.fullName, criteria)) 143 | if (callback) { 144 | this.db.lastError(safetyNet(callback, function(result) { 145 | callback(null, result.n) 146 | })) 147 | } 148 | } 149 | 150 | /** 151 | * Creates a new index on this collection (can be slow) 152 | */ 153 | MongolianCollection.prototype.createIndex = function(keys, options, callback) { 154 | if (!callback && typeof options === 'function') { 155 | callback = options 156 | options = undefined 157 | } 158 | if (callback && typeof callback !== 'function') throw new Error("callback is not a function!") 159 | var self = this 160 | var index = this._indexSpec(keys, options) 161 | self._indexCache[index.name] = index 162 | this.db.collection("system.indexes").insert(index, callback) 163 | } 164 | 165 | /** 166 | * Creates a new index on this collection (uses a local cache to avoid multiple creates) 167 | * 168 | * Possible options: 169 | * name 170 | * unique 171 | */ 172 | MongolianCollection.prototype.ensureIndex = function(keys, options, callback) { 173 | if (!callback && typeof options === 'function') { 174 | callback = options 175 | options = undefined 176 | } 177 | if (callback && typeof callback !== 'function') throw new Error("callback is not a function!") 178 | var name = this._indexSpec(keys).name 179 | if (this._indexCache[name]) { 180 | callback(null, this._indexCache[name]) 181 | return 182 | } 183 | this.createIndex(keys, options, callback) 184 | } 185 | 186 | /** 187 | * Clears the local index cache used by ensureIndex 188 | */ 189 | MongolianCollection.prototype.resetIndexCache = function(){ 190 | this._indexCache = {} 191 | } 192 | 193 | /** 194 | * Removes a given index from the database 195 | */ 196 | MongolianCollection.prototype.dropIndex = function(indexName, callback) { 197 | this.resetIndexCache() 198 | 199 | this.db.runCommand({ 200 | deleteIndexes: this.name, 201 | index: indexName 202 | }, callback) 203 | } 204 | 205 | /** 206 | * Returns all indexes for this collection 207 | */ 208 | MongolianCollection.prototype.indexes = function(callback) { 209 | this.db.collection("system.indexes").find({ ns:this.fullName }).toArray(callback) 210 | } 211 | 212 | /** 213 | * !!! Removes this collection from the database !!! 214 | */ 215 | MongolianCollection.prototype.drop = function(callback) { 216 | this.db.runCommand({ drop:this.name }, callback) 217 | } 218 | 219 | MongolianCollection.prototype.runCommand = function(commandNameOrCommand, options, callback) { 220 | if (!callback && typeof options === 'function') { 221 | callback = options 222 | options = undefined 223 | } 224 | var command = commandNameOrCommand 225 | if (typeof commandNameOrCommand === 'string') { 226 | command = {} 227 | command[commandNameOrCommand] = this.name 228 | extend(command, options) 229 | } 230 | this.db.runCommand(command, callback) 231 | } 232 | 233 | MongolianCollection.prototype.mapReduce = function(mapFunction, reduceFunction, options, callback) { 234 | var command = { 235 | mapreduce:this.name, 236 | map:mapFunction, 237 | reduce:reduceFunction 238 | } 239 | if (typeof options === 'string') { 240 | options = { out:options } 241 | } 242 | extend(command, options) 243 | 244 | var self = this 245 | this.db.runCommand(command, safetyNet(callback, function(result) { 246 | callback(null, new MapReduceResult(self.db, result)) 247 | })) 248 | } 249 | 250 | function MapReduceResult(db, result) { 251 | if (result.results) this.results = result.results 252 | this.timeMillis = result.timeMillis 253 | this.counts = result.counts 254 | if (result.db) db = db.server.db(result.db) 255 | if (result.result) this.collection = db.collection(result.result) 256 | } 257 | MapReduceResult.prototype.find = function(criteria, fields) { 258 | if (!this.collection) throw new Error("Map reduce returned no collection") 259 | return this.collection.find(criteria, fields) 260 | } 261 | MapReduceResult.prototype.drop = function(){ 262 | this.collection && this.collection.drop() 263 | } 264 | 265 | /** 266 | * 267 | */ 268 | MongolianCollection.prototype.findAndModify = function(options, callback) { 269 | var command = { 270 | findandmodify:this.name 271 | } 272 | for (var key in options) command[key] = options[key] 273 | this.db.runCommand(command, safetyNet(callback, function(result) { 274 | callback(null, result.value) 275 | })) 276 | } 277 | 278 | MongolianCollection.prototype.group = function (command, callback) { 279 | this.db.runCommand({group: command}, callback); 280 | } 281 | 282 | MongolianCollection.prototype.distinct = function(key, query, callback) { 283 | if (!callback && typeof query === 'function') { 284 | callback = query 285 | query = undefined 286 | } 287 | var command = { 288 | distinct: this.name, 289 | query: query, 290 | key: key 291 | } 292 | this.db.runCommand(command, safetyNet(callback, function(result) { 293 | callback(null, result.values) 294 | })) 295 | } 296 | 297 | MongolianCollection.prototype.isCapped = function(callback) { 298 | collName = this.db.name + "." + this.name; 299 | this.db.collection("system.namespaces").findOne({name:collName}, function(err, result) { 300 | if (typeof callback !== 'function') return; 301 | if (err) { 302 | callback(err) 303 | } 304 | if (!result || !result.options) { 305 | callback("Could not find this Collection.") 306 | } else { 307 | callback(null, result.options.capped, result.options.size) 308 | } 309 | }) 310 | } 311 | 312 | MongolianCollection.prototype.toString = function() { 313 | return this.db + "." + this.name 314 | } 315 | 316 | ////////////////////////////////////////////////////////////////////////////////// 317 | // Internal 318 | 319 | MongolianCollection.prototype._indexSpec = function(keys, options) { 320 | var index = { 321 | ns: this.fullName, 322 | key: keys 323 | } 324 | if (options) { 325 | for (var x in options) { 326 | index[x] = options[x] 327 | } 328 | } 329 | if (!index.name) { 330 | index.name = "" 331 | for (var key in keys) { 332 | if (index.name.length) { 333 | index.name += "_" 334 | } 335 | index.name += key + "_" + keys[key] 336 | } 337 | } 338 | return index 339 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Project Inactive 2 | ---------------- 3 | 4 | I am no longer have the time to work on this project. Feel free to fork! 5 | 6 | Mongolian DeadBeef 7 | ================== 8 | Mongolian DeadBeef is an awesome Mongo DB node.js driver that attempts to closely approximate the [mongodb shell][1]. 9 | 10 | [![Build Status](https://secure.travis-ci.org/marcello3d/node-mongolian.png)](http://travis-ci.org/marcello3d/node-mongolian) 11 | 12 | Introduction 13 | ------------ 14 | Mongolian DeadBeef and its documentation is super under construction! Go check out [examples/mongolian_trainer.js][2] 15 | and the rest of the source! 16 | 17 | Unlike other MongoDB node.js drivers, Mongolian DeadBeef is built from the ground up for node.js, using 18 | [node-buffalo][3] for BSON/message serialization. 19 | 20 | v0.1.15 Upgrade notes 21 | --------------------- 22 | 0.1.15 uses [node-buffalo][3] instead of mongodb-native for serialization, this means a few incompatibilities: 23 | 24 | + The helper methods on `ObjectId` are removed, use the `ObjectId` constructor to parse hex strings 25 | + `Code` type is removed, use vanilla function instances instead 26 | + `DBRef` is not supported 27 | + Error messages may be different 28 | 29 | Installation 30 | ------------ 31 | **DISCLAIMER: The API is experimental (but stabilizing). I will be adding, removing, and changing the API in the 32 | interest of a solid API. Use at your own risk** 33 | 34 | You can either clone the source and install with `npm link`, or install the latest published version from npm with 35 | `npm install mongolian`. 36 | 37 | Running Tests 38 | ------------- 39 | Run the tests with `npm test`. 40 | 41 | Motivation 42 | ---------- 43 | Not a fan of existing asynchronous mongodb apis for node.js, I set out to write my own. To avoid completely reinventing 44 | the wheel, much of the Mongolian DeadBeef API is inspired by the [mongodb shell][1]. 45 | 46 | High level principles: 47 | 48 | * Less is more 49 | * Nothing is added without careful consideration 50 | * Remove everything but the essentials 51 | * Each refactor should remove as much unnecessary lines of code as possible 52 | * Fail early and often 53 | * If I can easily detect a programmer error, an exception will be thrown 54 | 55 | Notes: 56 | 57 | * mongodb is pretty simple, much of its functionality is defined as queries on special databases 58 | * This allows for lots of code reuse 59 | * Avoid callbacks unless they are absolutely necessary 60 | 61 | Basics 62 | ------ 63 | Most of the work in MongolianDeadBeef doesn't occur until a query is actually made. This means that simple operations 64 | are fast and synchronous. Currently there is one connection per server. 65 | 66 | Examples 67 | -------- 68 | ```javascript 69 | var Mongolian = require("mongolian") 70 | 71 | // Create a server instance with default host and port 72 | var server = new Mongolian 73 | 74 | // Get database 75 | var db = server.db("awesome_blog") 76 | 77 | // Get some collections 78 | var posts = db.collection("posts") 79 | var comments = db.collection("comments") 80 | 81 | // Insert some data 82 | posts.insert({ 83 | pageId: "hallo", 84 | title: "Hallo", 85 | created: new Date, 86 | body: "Welcome to my new blog!" 87 | }) 88 | 89 | // Get a single document 90 | posts.findOne({ pageId: "hallo" }, function(err, post) { 91 | ... 92 | }) 93 | 94 | // Document cursors 95 | posts.find().limit(5).sort({ created: 1 }).toArray(function (err, array) { 96 | // do something with the array 97 | }) 98 | posts.find({ title: /^hal/ }).forEach(function (post) { 99 | // do something with a single post 100 | }, function(err) { 101 | // handle errors/completion 102 | }) 103 | ``` 104 | Connections and Authentication 105 | ------------------------------ 106 | ```javascript 107 | // Create a server with a specific host/port 108 | var server = new Mongolian("mongo.example.com:12345") 109 | 110 | 111 | // Authenticate a database 112 | db.auth(username, password) 113 | 114 | 115 | // Supported connection url format: [mongo://][username:password@]hostname[:port][/databasename] 116 | // Use uri-encoding for special characters in the username/password/database name 117 | 118 | // Database/auth shorthand (equivalent to calling db() and auth() on the resulting server) 119 | var db = new Mongolian("mongo://username:password@mongo.example.com:12345/database") 120 | 121 | // Connecting to replicasets: 122 | var server = new Monglian( 123 | "server1.local", 124 | "server2.local", 125 | "server3.local:27018" 126 | ) 127 | ``` 128 | Logging 129 | ------- 130 | By default, Mongolian logs to console.log, but you can override this by specifying your own log object (any object that 131 | provides `debug`, `info`, `warn`, and `error` methods): 132 | ```javascript 133 | var server = new Mongolian({ 134 | log: { 135 | debug: function(message) { ... }, 136 | info: function(message) { ... }, 137 | warn: function(message) { ... }, 138 | error: function(message) { ... } 139 | } 140 | }) 141 | 142 | var server = new Mongolian('server1.local', 'server2.local', { 143 | log: { ... } 144 | }) 145 | ``` 146 | BSON Data Types 147 | --------------- 148 | Mongolian DeadBeef uses [node-buffalo][3]'s BSON serialization code. Most BSON types map directly to JavaScript types, 149 | here are the ones that don't: 150 | ```javascript 151 | var Long = require('mongolian').Long // goog.math.Long - http://closure-library.googlecode.com/svn/docs/class_goog_math_Long.html 152 | var ObjectId = require('mongolian').ObjectId // new ObjectId(byteBuffer or hexString) 153 | var Timestamp = require('mongolian').Timestamp // == Long 154 | var DBRef = require('mongolian').DBRef // not supported yet 155 | ``` 156 | GridFS 157 | ------ 158 | The Mongo shell doesn't support gridfs, so Mongolian DeadBeef provides a custom Stream-based GridFS implementation. 159 | It consists of two main classes, `MongolianGridFS` and `MongolianGridFile`. You can get a MongolianGridFS object from a 160 | database with the `gridfs([gridfs name])` function. 161 | ```javascript 162 | // Get a GridFS from a database 163 | var gridfs = db.gridfs() // name defaults to 'fs' 164 | 165 | // Writing to GridFS consists of creating a GridFS file: 166 | var file = gridfs.create({ 167 | filename:"License", 168 | contentType:"text/plain" 169 | }) 170 | // And getting writable Stream (see http://nodejs.org/docs/v0.4/api/streams.html#writable_Stream ) 171 | var stream = file.writeStream() 172 | 173 | // You can then pipe a local file to that stream easily with: 174 | fs.createReadStream('LICENSE').pipe(stream) 175 | 176 | // Reading a file from GridFS is similar: 177 | gridfs.findOne("License", function (err, file) { 178 | if (!err && file) { 179 | // Get the read stream: 180 | var stream = file.readStream() 181 | 182 | // You could then pipe the file out to a http response, for example: 183 | stream.pipe(httpResponse) 184 | } 185 | }) 186 | 187 | // You can access metadata fields from the file object: 188 | file.length // might be a Long 189 | file.chunkSize 190 | file.md5 191 | file.filename 192 | file.contentType // mime-type 193 | file.uploadDate 194 | // These two are optional and may not be defined: 195 | file.metadata 196 | file.aliases 197 | 198 | // If you make any changes, save them: 199 | file.save() 200 | ``` 201 | Mongodb Shell Command Support 202 | ----------------------------- 203 | 204 | Nearly all commands are identical in syntax to the mongodb shell. However, asynchronous commands that go to the server 205 | will have an _optional_ node.js style callback parameter. 206 | 207 | Currently most commands starting with `get` are named without the `get`. Some of the getters are implemented as values 208 | instead of functions. 209 | 210 | + Bold functions are supported 211 | + Italicized functions are supported with different syntax 212 | + Everything else is currently unsupported 213 | 214 | There will likely be methods below that are never supported by Mongolian DeadBeef, since I'm targetting a slightly 215 | different use case. 216 | 217 | ### Databases 218 | From http://api.mongodb.org/js/1.8.1/symbols/src/shell_db.js.html 219 | 220 | + db.addUser(username, password[, readOnly=false][, callback]) 221 | + db.auth(username, password) 222 | + db.cloneDatabase(fromhost) 223 | + db.commandHelp(name) returns the help for the command 224 | + db.copyDatabase(fromdb, todb, fromhost) 225 | + db.createCollection(name, { size : ..., capped : ..., max : ... } ) 226 | + db.currentOp() displays the current operation in the db 227 | + db.dropDatabase() - see callback note below 228 | + db.eval(func[, arg1, arg2, ...][, callback]) run code server-side - see callback note below 229 | + db.getCollection(cname) implemented as db.collection(cname) 230 | + db.getCollectionNames() implemented as db.collectionNames(callback) 231 | + db.getLastError() - just returns the err msg string 232 | + db.getLastErrorObj() implemented as db.lastError(callback) - return full status object 233 | + db.getMongo() get the server connection object implemented as db.server 234 | + db.getMongo().setSlaveOk() allow this connection to read from the nonmaster member of a replica pair 235 | + db.getName() implemented as db.name 236 | + db.getPrevError() _(deprecated?)_ 237 | + db.getProfilingStatus() - returns if profiling is on and slow threshold 238 | + db.getReplicationInfo() 239 | + db.getSiblingDB(name) get the db at the same server as this one 240 | + db.isMaster() check replica primary status 241 | + db.killOp(opid) kills the current operation in the db 242 | + db.listCommands() lists all the db commands 243 | + db.printCollectionStats() 244 | + db.printReplicationInfo() 245 | + db.printSlaveReplicationInfo() 246 | + db.printShardingStatus() 247 | + db.removeUser(username[, callback]) - see callback note below 248 | + db.repairDatabase() 249 | + db.resetError() 250 | + db.runCommand(cmdObj[, callback]) run a database command. if cmdObj is a string, turns it into { cmdObj : 1 } 251 | + db.serverStatus() 252 | + db.setProfilingLevel(level,) 0=off 1=slow 2=all 253 | + db.shutdownServer() 254 | + db.stats() 255 | + db.version() current version of the server 256 | 257 | ### Collections 258 | From http://api.mongodb.org/js/1.8.1/symbols/src/shell_collection.js.html 259 | 260 | + collection.find().help() - show DBCursor help 261 | + collection.count(callback) 262 | + collection.dataSize() 263 | + collection.distinct(key[, query], callback) - eg. collection.distinct( 'x' ) 264 | + collection.drop([callback]) drop the collection - see callback note below 265 | + collection.dropIndex(name[, callback]) - see callback note below 266 | + collection.dropIndexes() 267 | + collection.ensureIndex(keypattern[,options][, callback]) - options is an object with these possible fields: name, unique, dropDups - see callback note below 268 | + collection.reIndex() 269 | + collection.find([query],[fields]) - query is an optional query filter. fields is optional set of fields to return. 270 | e.g. collection.find( {x:77} , {name:1, x:1} ) - returns a cursor object 271 | + collection.find(...).count() 272 | + collection.find(...).limit(n) 273 | + collection.find(...).skip(n) 274 | + collection.find(...).sort(...) 275 | + collection.findOne([query][callback]) 276 | + collection.findAndModify( { update : ... , remove : bool [, query: {}, sort: {}, 'new': false] } ) 277 | ex: finds document with comment value 0, increase its 'count' field by 1, and return the updated document. 278 | collection.findAndModify( {query: {comment:'0'}, update : {"$inc":{"count":1}}, 'new': true}, function (err, doc) { 279 | console.log(doc) 280 | }) 281 | + collection.getDB() get DB object associated with collection implemented as collection.db 282 | + collection.getIndexes() implemented as collection.indexes(callback) 283 | + collection.group( { key : ..., initial: ..., reduce : ...[, cond: ...] } ) 284 | + collection.mapReduce( mapFunction , reduceFunction , [optional params][, callback]) 285 | + collection.remove(query[, callback]) - see callback note below 286 | + collection.renameCollection( newName , [dropTarget] ) renames the collection. 287 | + collection.runCommand( name , [options][, callback]) runs a db command with the given name where the first param is the collection name 288 | + collection.save(obj[, callback]) - see callback note below 289 | + collection.stats() 290 | + collection.storageSize() - includes free space allocated to this collection 291 | + collection.totalIndexSize() - size in bytes of all the indexes 292 | + collection.totalSize() - storage allocated for all data and indexes 293 | + collection.update(query, object[, upsert\_bool, multi\_bool][, callback]) - see callback note below 294 | + collection.validate() - SLOW 295 | + collection.getShardVersion() - only for use with sharding 296 | 297 | ### Cursors 298 | From http://api.mongodb.org/js/1.8.1/symbols/src/shell_query.js.html 299 | 300 | + cursor.sort( {...} ) 301 | + cursor.limit( n ) 302 | + cursor.skip( n ) 303 | + cursor.count() - total # of objects matching query, ignores skip,limit 304 | + cursor.size() - total # of objects cursor would return, honors skip,limit 305 | + cursor.explain([verbose]) 306 | + cursor.hint(...) 307 | + cursor.showDiskLoc() - adds a $diskLoc field to each returned object 308 | + cursor.toArray(callback) - unique to Mongolian DeadBeef 309 | + cursor.forEach(func, callback) - calls func for each document, and callback upon completion or error 310 | + cursor.print() - output to console in full pretty format 311 | + cursor.map( func ) - map documents before they're returned in next, toArray, forEach 312 | + cursor.hasNext() 313 | + cursor.next([callback]) - returns the next document or null if there are no more 314 | 315 | 316 | ### Callbacks 317 | Callbacks take the standard node.js format: `function(error, value)` 318 | 319 | Mongodb handles write operations (insert, update, save, drop, etc.) asynchronously. If you pass a callback into one of 320 | these methods, this is equivalent to immediately calling `db.lastError(callback)` on the same server/connection. Passing 321 | a null value will not send a getLastError command to the server. 322 | 323 | Currently there is no way to specify the write concern on these inlined callbacks. 324 | 325 | Todo 326 | ---- 327 | 328 | * Connection pooling 329 | * Various utility methods 330 | * More unit tests 331 | * Documentation 332 | * Cleanup 333 | 334 | Contributing 335 | ------------ 336 | Try it out and send me feedback! That's the best help I could use right now. Unit tests are good, too. 337 | 338 | License 339 | ------- 340 | Mongolian DeadBeef is open source software under the [zlib license][4]. 341 | 342 | [1]: http://www.mongodb.org/display/DOCS/dbshell+Reference 343 | [2]: https://github.com/marcello3d/node-mongolian/blob/master/examples/mongolian_trainer.js 344 | [3]: https://github.com/marcello3d/node-buffalo 345 | [4]: https://github.com/marcello3d/node-mongolian/blob/master/LICENSE 346 | --------------------------------------------------------------------------------