webgme-engine
Version:
WebGME server and Client API without a GUI
571 lines (502 loc) • 20.2 kB
JavaScript
/*globals requireJS*/
/*eslint-env node*/
/**
* @module Server:Storage:Mongo
* @author mmaroti / https://github.com/mmaroti
* @author pmeijer / https://github.com/pmeijer
*/
;
var mongodb = require('mongodb'),
Q = require('q'),
mongoUri = require('mongo-uri'),
CONSTANTS = requireJS('common/storage/constants'),
CANON = requireJS('common/util/canon'),
REGEXP = requireJS('common/regexp');
function Mongo(mainLogger, gmeConfig) {
var self = this,
connectionCnt = 0,
connectDeferred,
disconnectDeferred,
logger = mainLogger.fork('mongo');
this.client = null;
this.db = null;
this.dbName = mongoUri.parse(gmeConfig.mongo.uri).database;
this.CONSTANTS = {
TAGS: 'TAGS'
};
/**
* Provides methods related to a specific project.
*
* @param {string} projectId - identifier of the project (ownerId + '.' + projectName).
* @param {object} collection - Mongo collection connected to database.
* @constructor
* @private
*/
function MongoProject(projectId, collection) {
this.projectId = projectId;
this._collection = collection;
this.closeProject = function (callback) {
var deferred = Q.defer();
//TODO: Does this really do something?
collection = null;
deferred.resolve();
return deferred.promise.nodeify(callback);
};
this.loadObject = function (hash, callback) {
var deferred = Q.defer();
if (typeof hash !== 'string') {
deferred.reject(new Error('loadObject - given hash is not a string : ' + typeof hash));
} else if (!REGEXP.HASH.test(hash)) {
deferred.reject(new Error('loadObject - invalid hash :' + hash));
} else {
logger.debug('loadObject ' + hash);
collection.findOne({_id: hash}, function (err, obj) {
if (err) {
logger.error(err);
deferred.reject(err);
} else if (obj) {
deferred.resolve(obj);
} else {
logger.error('object does not exist ' + hash);
deferred.reject(new Error('object does not exist ' + hash));
}
});
}
return deferred.promise.nodeify(callback);
};
this.insertObject = function (object, callback) {
var deferred = Q.defer(),
rejected = false;
if (object === null || typeof object !== 'object') {
deferred.reject(new Error('object is not an object'));
rejected = true;
} else if (typeof object._id !== 'string' || !REGEXP.HASH.test(object._id)) {
deferred.reject(new Error('object._id is not a valid hash.'));
rejected = true;
}
if (rejected === false) {
collection.insertOne(object, function (err) {
// manually check duplicate keys
if (err && err.code === 11000) {
collection.findOne({
_id: object._id
}, function (err2, data) {
var errMsg;
if (err2) {
deferred.reject(err2);
} else {
if (CANON.stringify(object) === CANON.stringify(data)) {
logger.debug('tried to insert existing hash - the two objects were equal',
object._id);
deferred.resolve();
} else {
errMsg = 'tried to insert existing hash - the two objects were NOT equal ';
logger.error(errMsg, {
metadata: {
newObject: object,
oldObject: data
}
});
deferred.reject(new Error(errMsg + object._id));
}
}
});
} else if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
}
return deferred.promise.nodeify(callback);
};
this.getBranches = function (callback) {
var mongoFind = collection.find({
_id: {
$regex: REGEXP.RAW_BRANCH.source
}
});
return Q.ninvoke(mongoFind, 'toArray')
.then(function (docs) {
var branches = {};
for (var i = 0; i < docs.length; ++i) {
branches[docs[i]._id.slice(1)] = docs[i].hash;
}
return Q(branches);
})
.nodeify(callback);
};
this.getBranchHash = function (branch, callback) {
branch = '*' + branch;
return Q.ninvoke(collection, 'findOne', {_id: branch})
.then(function (branchObj) {
// FIXME: This behaviour of return empty string rather than an error is the same as before.
// FIXME: Consider returning with an error in style with 'Branch does not exist'.
return Q((branchObj && branchObj.hash) || '');
}).nodeify(callback);
};
this.setBranchHash = function (branch, oldhash, newhash, callback) {
var deferred = Q.defer();
branch = '*' + branch;
if (oldhash === newhash) {
collection.findOne({
_id: branch
}, function (err, obj) {
if (!err && oldhash !== ((obj && obj.hash) || '')) {
err = new Error('branch hash mismatch');
}
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
} else if (newhash === '') {
collection.deleteOne({
_id: branch,
hash: oldhash
}, function (err, result) {
if (!err && result.deletedCount !== 1) {
collection.findOne({_id: branch}, function (err, obj) {
if (!err && obj) {
err = new Error('branch hash mismatch');
}
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
} else if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
} else if (oldhash === '') {
collection.insertOne({
_id: branch,
hash: newhash
}, function (err) {
if (err) {
if (err.code === 11000) {
// insertDocument :: caused by :: 11000 E11000 duplicate key error...
deferred.reject(new Error('branch hash mismatch'));
} else {
deferred.reject(err);
}
} else {
deferred.resolve();
}
});
} else {
collection.updateOne({
_id: branch,
hash: oldhash
}, {
$set: {
hash: newhash
}
}, function (err, result) {
if (!err && result.modifiedCount !== 1) {
err = new Error('branch hash mismatch');
}
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
}
return deferred.promise.nodeify(callback);
};
this.getCommits = function (before, number, callback) {
var mongoFind = collection.find({
type: CONSTANTS.COMMIT_TYPE,
time: {
$lt: before
}
}).limit(number).sort({
time: -1
});
return Q.ninvoke(mongoFind, 'toArray')
.then(function (docs) {
return Q(docs);
})
.nodeify(callback);
};
this.createTag = function (name, commitHash, callback) {
var deferred = Q.defer(),
query = {
_id: self.CONSTANTS.TAGS,
},
update = {
$set: {}
};
query[name] = {
$exists: false
};
update.$set[name] = commitHash;
collection.updateOne(query, update, {upsert: true}, function (err/*, num*/) {
if (err) {
if (err.code === 11000) {
deferred.reject(new Error('Tag already exists [' + name + ']'));
} else {
deferred.reject(err);
}
} else {
deferred.resolve();
}
});
return deferred.promise.nodeify(callback);
};
this.deleteTag = function (name, callback) {
var deferred = Q.defer(),
query = {
_id: self.CONSTANTS.TAGS,
},
update = {
$unset: {}
};
update.$unset[name] = '';
collection.updateOne(query, update, function (err/*, num*/) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
return deferred.promise.nodeify(callback);
};
this.getTags = function (callback) {
var deferred = Q.defer();
collection.findOne({_id: self.CONSTANTS.TAGS}, {}, function (err, result) {
if (err) {
deferred.reject(err);
} else if (result) {
delete result._id;
deferred.resolve(result);
} else {
deferred.resolve({});
}
});
return deferred.promise.nodeify(callback);
};
this.traverse = function (visitFn, callback) {
var deferred = Q.defer(),
cursor = collection.find(),
finished = false,
ongoingVisits = 0,
error = null,
next = function (err) {
error = error || err;
ongoingVisits -= 1;
if (finished && ongoingVisits === 0) {
if (error) {
deferred.reject(error);
} else {
deferred.resolve();
}
}
};
cursor.batchSize(1000).each(function (err, object) {
error = error || err;
if (err === null) {
if (object === null) {
finished = true;
} else {
ongoingVisits += 1;
visitFn(object, next);
}
} else {
finished = true;
}
if (finished && ongoingVisits === 0) {
if (error) {
deferred.reject(error);
} else {
deferred.resolve();
}
}
});
return deferred.promise.nodeify(callback);
};
}
function openDatabase(callback) {
connectionCnt += 1;
logger.debug('openDatabase, connection counter:', connectionCnt);
if (connectionCnt === 1) {
if (self.client === null) {
logger.info('connecting to:', gmeConfig.mongo.uri);
logger.debug('mongdb options', gmeConfig.mongo.uri, JSON.stringify(gmeConfig.mongo.options));
connectDeferred = Q.defer();
// connect to mongo
mongodb.MongoClient.connect(gmeConfig.mongo.uri, gmeConfig.mongo.options, function (err, client) {
if (!err && client) {
self.client = client;
self.db = client.db(self.dbName);
disconnectDeferred = null;
logger.debug('Connected.');
connectDeferred.resolve();
} else {
self.client = null;
connectionCnt -= 1;
logger.error('Failed to connect.', {metadata: err});
connectDeferred.reject(err);
}
});
} else {
logger.debug('Count is 1 but mongo is not null');
}
} else {
logger.debug('Reusing mongo connection.');
// we are already connected
}
return connectDeferred.promise.nodeify(callback);
}
function closeDatabase(callback) {
connectionCnt -= 1;
logger.debug('closeDatabase, connection counter:', connectionCnt);
if (connectionCnt < 0) {
logger.error('connection counter became negative, too many closeDatabase. Setting it to 0.', connectionCnt);
connectionCnt = 0;
}
if (!disconnectDeferred) {
disconnectDeferred = Q.defer();
}
if (connectionCnt === 0) {
if (self.client) {
logger.debug('Closing connection to mongo...');
self.client.close(function () {
self.client = null;
logger.debug('Closed.');
disconnectDeferred.resolve();
});
} else {
disconnectDeferred.resolve();
}
} else {
logger.debug('Connections still alive.');
}
return disconnectDeferred.promise.nodeify(callback);
}
function deleteProject(projectId, callback) {
var deferred = Q.defer();
if (self.db) {
Q.ninvoke(self.db, 'dropCollection', projectId)
.then(function () {
deferred.resolve(true);
})
.catch(function (err) {
if (err.ok === 0) { // http://docs.mongodb.org/manual/reference/method/db.collection.drop/
logger.debug('deleteProject, project does not exist', projectId);
deferred.resolve(false);
} else {
deferred.reject(err);
}
});
} else {
deferred.reject(new Error('Database is not open.'));
}
return deferred.promise.nodeify(callback);
}
function openProject(projectId, callback) {
var deferred = Q.defer();
logger.debug('openProject', projectId);
if (self.db) {
Q.ninvoke(self.db, 'collection', projectId, {strict: true})
.then(function (collection) {
deferred.resolve(new MongoProject(projectId, collection));
})
.catch(function (err) {
if (err.message.indexOf('does not exist') > -1) {
deferred.reject(new Error('Project does not exist ' + projectId));
} else {
deferred.reject(err);
}
})
.done();
} else {
deferred.reject(new Error('Database is not open.'));
}
return deferred.promise.nodeify(callback);
}
function createProject(projectId, callback) {
var collection,
deferred = Q.defer();
logger.debug('createProject', projectId);
if (self.db) {
Q.ninvoke(self.db, 'collection', projectId)
.then(function (result) {
collection = result;
return Q.ninvoke(collection, 'insertMany', [
{_id: CONSTANTS.EMPTY_PROJECT_DATA},
{_id: self.CONSTANTS.TAGS}
]);
})
.then(function () {
deferred.resolve(new MongoProject(projectId, collection));
})
.catch(function (err) {
if (err.code === 11000) {
deferred.reject(new Error('Project already exists ' + projectId));
} else {
deferred.reject(err);
}
})
.done();
} else {
deferred.reject(new Error('Database is not open.'));
}
return deferred.promise.nodeify(callback);
}
function renameProject(projectId, newProjectId, callback) {
var deferred = Q.defer();
if (self.db) {
Q.ninvoke(self.db, 'renameCollection', projectId, newProjectId)
.then(function () {
deferred.resolve();
})
.catch(function (err) {
err = err instanceof Error ? err : new Error(err);
if (err.message.indexOf('target namespace exists') > -1) {
deferred.reject(new Error('Project already exists ' + newProjectId));
} else if (err.message.indexOf('does not exist') > -1) {
deferred.reject(new Error('Project does not exist ' + projectId));
} else {
deferred.reject(err);
}
});
} else {
deferred.reject(new Error('Database is not open.'));
}
return deferred.promise.nodeify(callback);
}
function duplicateProject(projectId, newProjectId, callback) {
var project,
newProject;
logger.debug('duplicateProject', projectId);
return self.openProject(projectId)
.then(function (project_) {
project = project_;
return self.createProject(newProjectId);
})
.then(function (newProject_) {
newProject = newProject_;
const pipelineStages = [{ $out: `${newProjectId}` }];
return project._collection.aggregate(pipelineStages, {}).toArray();
})
.then(function () {
return newProject;
})
.nodeify(callback);
}
this.openDatabase = openDatabase;
this.closeDatabase = closeDatabase;
this.openProject = openProject;
this.deleteProject = deleteProject;
this.createProject = createProject;
this.renameProject = renameProject;
this.duplicateProject = duplicateProject;
}
module.exports = Mongo;