(function(module) { 'use strict'; var winston = require('winston'), async = require('async'), nconf = require('nconf'), express = require('express'), db, mongoClient, mongoStore; try { mongoClient = require('mongodb').MongoClient; mongoStore = require('connect-mongo')(express); } catch (err) { winston.error('Unable to initialize MongoDB! Is MongoDB installed? Error :' + err.message); process.exit(); } module.init = function(callback) { mongoClient.connect('mongodb://'+ nconf.get('mongo:host') + ':' + nconf.get('mongo:port') + '/' + nconf.get('mongo:database'), function(err, _db) { if(err) { winston.error("NodeBB could not connect to your Mongo database. Mongo returned the following error: " + err.message); process.exit(); } db = _db; module.client = db; module.sessionStore = new mongoStore({ db: db }); if(nconf.get('mongo:password') && nconf.get('mongo:username')) { db.authenticate(nconf.get('mongo:username'), nconf.get('mongo:password'), function (err) { if(err) { winston.error(err.message); process.exit(); } createIndices(); }); } else { winston.warn('You have no mongo password setup!'); createIndices(); } function createIndices() { db.collection('objects').ensureIndex({_key :1}, {background:true}, function(err) { if(err) { winston.error('Error creating index ' + err.message); } }); db.collection('objects').ensureIndex({'expireAt':1}, {expireAfterSeconds:0, background:true}, function(err) { if(err) { winston.error('Error creating index ' + err.message); } }); db.collection('search').ensureIndex({content:'text'}, {background:true}, function(err) { if(err) { winston.error('Error creating index ' + err.message); } }); if(typeof callback === 'function') { callback(null); } } }); } // // helper functions // function removeHiddenFields(item) { if(item) { if(item._id) { delete item._id; } if(item._key) { delete item._key; } } return item; } function findItem(data, key) { if(!data) { return null; } for(var i=0; i<data.length; ++i) { if(data[i]._key === key) { var item = data.splice(i, 1); if(item && item.length) { return item[0]; } else { return null; } } } return null; } // // Exported functions // module.flushdb = function(callback) { db.dropDatabase(function(err, result) { if(err){ winston.error(error); if(typeof callback === 'function') { return callback(err); } } if(typeof callback === 'function') { callback(null); } }); } module.getFileName = function(callback) { throw new Error('not-implemented'); } module.info = function(callback) { db.stats({scale:1024}, function(err, stats) { if(err) { return callback(err); } // TODO : if this it not deleted the templates break, // it is a nested object inside stats delete stats.dataFileVersion; stats.avgObjSize = (stats.avgObjSize / 1024).toFixed(2); stats.raw = JSON.stringify(stats, null, 4); stats.mongo = true; callback(null, stats); }); } // key module.exists = function(key, callback) { db.collection('objects').findOne({_key:key}, function(err, item) { callback(err, item !== undefined && item !== null); }); } module.delete = function(key, callback) { db.collection('objects').remove({_key:key}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.get = function(key, callback) { module.getObjectField(key, 'value', callback); } module.set = function(key, value, callback) { var data = {value:value}; module.setObject(key, data, callback); } module.keys = function(key, callback) { db.collection('objects').find( { _key: { $regex: key /*, $options: 'i'*/ } }, function(err, result) { callback(err, result); }); } module.rename = function(oldKey, newKey, callback) { db.collection('objects').update({_key: oldKey}, {$set:{_key: newKey}}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.expire = function(key, seconds, callback) { module.expireAt(key, Math.round(Date.now() / 1000) + seconds, callback); } module.expireAt = function(key, timestamp, callback) { module.setObjectField(key, 'expireAt', new Date(timestamp * 1000), callback); } //hashes module.setObject = function(key, data, callback) { data['_key'] = key; db.collection('objects').update({_key:key}, {$set:data}, {upsert:true, w: 1}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.setObjectField = function(key, field, value, callback) { var data = {}; if(typeof field !== 'string') { field = field.toString(); } // if there is a '.' in the field name it inserts subdocument in mongo, replace '.'s with \uff0E field = field.replace(/\./g, '\uff0E'); data[field] = value; db.collection('objects').update({_key:key}, {$set:data}, {upsert:true, w: 1}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.getObject = function(key, callback) { db.collection('objects').findOne({_key:key}, function(err, item) { removeHiddenFields(item); callback(err, item); }); } module.getObjects = function(keys, callback) { db.collection('objects').find({_key:{$in:keys}}, {_id:0}).toArray(function(err, data) { if(err) { return callback(err); } var returnData = []; for(var i=0; i<keys.length; ++i) { returnData.push(findItem(data, keys[i])); } callback(null, returnData); }); } module.getObjectField = function(key, field, callback) { module.getObjectFields(key, [field], function(err, data) { if(err) { return callback(err); } callback(null, data[field]); }); } module.getObjectFields = function(key, fields, callback) { var _fields = {}; for(var i=0; i<fields.length; ++i) { if(typeof fields[i] !== 'string') { _fields[fields[i].toString().replace(/\./g, '\uff0E')] = 1; } else { _fields[fields[i].replace(/\./g, '\uff0E')] = 1; } } db.collection('objects').findOne({_key:key}, _fields, function(err, item) { if(err) { return callback(err); } if(item === null) { item = {}; } for(var i=0; i<fields.length; ++i) { if(item[fields[i]] === null || item[fields[i]] === undefined) { item[fields[i]] = null; } } removeHiddenFields(item); callback(null, item); }); } module.getObjectKeys = function(key, callback) { module.getObject(key, function(err, data) { if(err) { return callback(err); } if(data) { callback(null, Object.keys(data)); } else { callback(null, []); } }); } module.getObjectValues = function(key, callback) { module.getObject(key, function(err, data) { if(err) { return callback(err); } var values = []; for(var key in data) { values.push(data[key]); } callback(null, values); }); } module.isObjectField = function(key, field, callback) { var data = {}; if(typeof field !== 'string') { field = field.toString(); } field = field.replace(/\./g, '\uff0E'); data[field] = ''; db.collection('objects').findOne({_key:key}, {fields:data}, function(err, item) { if(err) { return callback(err); } callback(err, !!item && item[field] !== undefined && item[field] !== null); }); } module.deleteObjectField = function(key, field, callback) { var data = {}; if(typeof field !== 'string') { field = field.toString(); } field = field.replace(/\./g, '\uff0E'); data[field] = ""; db.collection('objects').update({_key:key}, {$unset : data}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.incrObjectField = function(key, field, callback) { module.incrObjectFieldBy(key, field, 1, callback); } module.decrObjectField = function(key, field, callback) { module.incrObjectFieldBy(key, field, -1, callback); } module.incrObjectFieldBy = function(key, field, value, callback) { var data = {}; if(typeof field !== 'string') { field = field.toString(); } field = field.replace(/\./g, '\uff0E'); data[field] = value; db.collection('objects').findAndModify({_key:key}, {}, {$inc: data}, {new:true, upsert:true}, function(err, result) { if(typeof callback === 'function') { callback(err, result ? result[field] : null); } }); } // sets module.setAdd = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } db.collection('objects').update({_key:key}, {$addToSet: { members: value }}, {upsert:true, w: 1}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.setRemove = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } db.collection('objects').update({_key:key, members: value}, {$pull : {members: value}}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.isSetMember = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } db.collection('objects').findOne({_key:key, members: value}, function(err, item) { callback(err, item !== null && item !== undefined); }); } module.isMemberOfSets = function(sets, value, callback) { function iterator(set, next) { module.isSetMember(set, value, function(err, result) { if(err) { return next(err); } next(null, result?1:0); }); } async.map(sets, iterator, callback); } module.getSetMembers = function(key, callback) { db.collection('objects').findOne({_key:key}, {members:1}, function(err, data) { if(err) { return callback(err); } if(!data) { callback(null, []); } else { callback(null, data.members); } }); } module.setCount = function(key, callback) { db.collection('objects').findOne({_key:key}, function(err, data) { if(err) { return callback(err); } if(!data) { return callback(null, 0); } callback(null, data.members.length); }); } module.setRemoveRandom = function(key, callback) { db.collection('objects').findOne({_key:key}, function(err, data) { if(err) { if(typeof callback === 'function') { return callback(err); } else { return winston.error(err.message); } } if(!data) { if(typeof callback === 'function') { callback(null, 0); } } else { var randomIndex = Math.floor(Math.random() * data.members.length); var value = data.members[randomIndex]; module.setRemove(data._key, value, function(err, result) { if(typeof callback === 'function') { callback(err, value); } }); } }); } // sorted sets module.sortedSetAdd = function(key, score, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } var data = { score:score, value:value }; db.collection('objects').update({_key:key, value:value}, {$set:data}, {upsert:true, w: 1}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.sortedSetRemove = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } db.collection('objects').remove({_key:key, value:value}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } function getSortedSetRange(key, start, stop, sort, callback) { db.collection('objects').find({_key:key}, {fields:{value:1}}) .limit(stop - start + 1) .skip(start) .sort({score: sort}) .toArray(function(err, data) { if(err) { return callback(err); } // maybe this can be done with mongo? data = data.map(function(item) { return item.value; }); callback(err, data); }); } module.getSortedSetRange = function(key, start, stop, callback) { getSortedSetRange(key, start, stop, 1, callback); } module.getSortedSetRevRange = function(key, start, stop, callback) { getSortedSetRange(key, start, stop, -1, callback); } module.getSortedSetRevRangeByScore = function(args, callback) { //var args = ['topics:recent', '+inf', timestamp - since, 'LIMIT', start, end - start + 1]; var key = args[0], max = (args[1] === '+inf')?Number.MAX_VALUE:args[1], min = args[2], start = args[4], stop = args[5]; db.collection('objects').find({_key:key, score: {$gte:min, $lte:max}}, {fields:{value:1}}) .limit(stop - start + 1) .skip(start) .sort({score: -1}) .toArray(function(err, data) { if(err) { return callback(err); } // maybe this can be done with mongo? data = data.map(function(item) { return item.value; }); callback(err, data); }); } module.sortedSetCount = function(key, min, max, callback) { db.collection('objects').count({_key:key, score: {$gte:min, $lte:max}}, function(err, count) { if(err) { return callback(err); } if(!count) { return callback(null, 0); } callback(null,count); }); } module.sortedSetCard = function(key, callback) { db.collection('objects').count({_key:key}, function(err, count) { if(err) { return callback(err); } if(!count) { return callback(null, 0); } callback(null, count); }); } module.sortedSetRank = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } module.getSortedSetRange(key, 0, -1, function(err, result) { if(err) { return callback(err); } var rank = result.indexOf(value); if(rank === -1) { return callback(null, null); } callback(null, rank); }); } module.sortedSetRevRank = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } module.getSortedSetRange(key, 0, -1, function(err, result) { if(err) { return callback(err); } var rank = result.indexOf(value); if(rank === -1) { return callback(null, null); } callback(null, result.length - rank - 1); }); } module.sortedSetScore = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } db.collection('objects').findOne({_key:key, value: value}, {fields:{score:1}}, function(err, result) { if(err) { return callback(err); } if(result) { return callback(null, result.score); } callback(err, null); }); } module.isSortedSetMember = function(key, value, callback) { module.sortedSetScore(key, value, function(err, score) { callback(err, !!score); }); } module.sortedSetsScore = function(keys, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } db.collection('objects').find({_key:{$in:keys}, value: value}).toArray(function(err, result) { if(err) { return callback(err); } var returnData = [], item; for(var i=0; i<keys.length; ++i) { item = findItem(result, keys[i]); returnData.push(item ? item.score : null); } callback(null, returnData); }); } // lists module.listPrepend = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } module.isObjectField(key, 'array', function(err, exists) { if(err) { if(typeof callback === 'function') { return callback(err); } else { return winston.error(err.message); } } if(exists) { db.collection('objects').update({_key:key}, {'$set': {'array.-1': value}}, {upsert:true, w:1 }, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } else { module.listAppend(key, value, callback); } }) } module.listAppend = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } db.collection('objects').update({ _key: key }, { $push: { array: value } }, {upsert:true, w:1}, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.listRemoveLast = function(key, callback) { module.getListRange(key, -1, 0, function(err, value) { if(err) { if(typeof callback === 'function') { return callback(err); } return; } db.collection('objects').update({_key: key }, { $pop: { array: 1 } }, function(err, result) { if(typeof callback === 'function') { if(err) { return callback(err); } if(value && value.length) { callback(err, value[0]); } else { callback(err, null); } } }); }); } module.listRemoveAll = function(key, value, callback) { if(value !== null && value !== undefined) { value = value.toString(); } db.collection('objects').update({_key: key }, { $pull: { array: value } }, function(err, result) { if(typeof callback === 'function') { callback(err, result); } }); } module.getListRange = function(key, start, stop, callback) { var skip = start, limit = stop - start + 1, splice = false; if((start < 0 && stop >= 0) || (start >= 0 && stop < 0)) { skip = 0; limit = Math.pow(2, 31) - 2; splice = true; } else if (start > stop) { return callback(null, []); } db.collection('objects').findOne({_key:key}, { array: { $slice: [skip, limit] }}, function(err, data) { if(err) { return callback(err); } if(data && data.array) { if(splice) { if(start < 0) { start = data.array.length - Math.abs(start); } if(stop < 0) { stop = data.array.length - Math.abs(stop); } if(start > stop) { return callback(null, []); } var howMany = stop - start + 1; if(start !== 0 || howMany !== data.array.length) { data.array = data.array.splice(start, howMany); } } callback(null, data.array); } else { callback(null, []); } }); } }(exports));