alt-fh-db
Version:
Alternative for the FeedHenry `.db` API in `fh-mbaas-api`
494 lines (438 loc) • 13.2 kB
JavaScript
var MongoClient = require('mongodb').MongoClient;
var ObjectId = require('mongodb').ObjectId;
var MAX_COLLECTION_NAME = 70;
var mongoClientOptions = {
useNewUrlParser: true,
useUnifiedTopology: true
};
var MongoDbClient = function (options) {
var localMongoUrl = process.env.MONGODB_CONN_URL || 'mongodb://localhost:27017/db'
var localDb = null;
var localClient = null;
this.connect = function (callback) {
// console.log('MongoDbClient prototype.mongoConnect ' + this.getMongoUrl()); // move to 'debug' level
var self = this;
// Check DB connection is already open or not
if (self.getDb() === null) {
// console.log('Creating a new DB connection'); // move to 'debug' level
MongoClient.connect(self.getMongoUrl(), options, function (err, client) {
if (err) {
console.log('Error connecting to MongoDB');
return callback(err);
}
localClient = client;
// Use the database name from the connection string
var database = client.db();
self.setDb(database);
callback(null, self.getDb());
});
} else {
// console.log('Reusing DB connection'); // move to 'debug' level
callback(null, self.getDb());
}
};
this.close = function(callback) {
var client = this.getClient();
if (client !== null) {
return client.close(false, callback);
} else {
if (!callback || typeof callback !== 'function'){
return Promise.resolve();
}
else {
return callback();
}
}
}
/**
* Get the mongo DB URL
*/
this.getMongoUrl = function() {
return localMongoUrl;
};
/**
* Returns the DB instance that was stored from the initial Mongo connect
*/
this.getDb = function() {
return localDb;
};
/**
* Sets the DB instance that was stored from the initial Mongo connect
*/
this.setDb = function setDb (db) {
localDb = db;
};
/**
* Returns the Mongo client instance
*/
this.getClient = function getClient () {
return localClient;
};
};
/**
* Helper object for building queries for list call
* Taken from fh_ditch
*/
var critOps = {
eq: function (query, fields) {
var field;
if (fields !== null) {
for (field in fields) {
if (fields.hasOwnProperty(field)) {
query[field] = fields[field];
}
}
}
},
ne: function (query, fields) {
buildQuery(query, fields, '$ne');
},
lt: function (query, fields) {
buildQuery(query, fields, '$lt');
},
le: function (query, fields) {
buildQuery(query, fields, '$lte');
},
gt: function (query, fields) {
buildQuery(query, fields, '$gt');
},
ge: function (query, fields) {
buildQuery(query, fields, '$gte');
},
like: function (query, fields) {
buildQuery(query, fields, '$regex');
},
'in': function (query, fields) {
buildQuery(query, fields, '$in');
},
geo: function (query, fields) {
if (fields !== null) {
var field;
var earthRadius = 6378; // km
for (field in fields) {
if (fields.hasOwnProperty(field)) {
var queryField = {};
if (typeof query[field] !== 'undefined') {
queryField = query[field];
}
queryField['$within'] = {
'$centerSphere': [ // supported by mongodb V1.8 & above
fields[field].center,
fields[field].radius / earthRadius
]
};
query[field] = queryField;
}
}
}
}
};
/**
* Helper method for building queries for list call
* Taken from fh_ditch
* @param {*} query
* @param {*} fields
* @param {*} expression
*/
function buildQuery (query, fields, expression) {
var field;
if (fields !== null) {
for (field in fields) {
if (fields.hasOwnProperty(field)) {
var queryField = {};
if (typeof query[field] !== 'undefined') {
queryField = query[field];
}
queryField[expression] = fields[field];
query[field] = queryField;
}
}
}
}
/**
* Helper method for returning the correct response
* Taken from fh_ditch
* @param {*} document
* @param {*} type
*/
function generateReturn (document, type) {
var retDoc = {};
if (document !== null && typeof (document) !== 'undefined') {
if (document._id) {
retDoc.type = type;
retDoc.guid = document._id.toString(); // switched from .toHexString() to prevent failing when _id is not ObjecdId
}
var i = 0;
for (var field in document) {
if (field !== '_id') {
if (i === 0) {
retDoc.fields = {};
i = 1;
}
retDoc.fields[field] = document[field];
}
}
}
return retDoc;
}
function createObjectIdFromHexString (str) {
var response;
try {
response = ObjectId.createFromHexString(str);
return response;
} catch (err) {
// console.log('Caught error converting ID', str, ': ', err); // move to 'debug' level
return str;
}
};
/**
* Mimic the create action from fh.db
* Inserts one or many records in a collection
*/
MongoDbClient.prototype.create = function (db, options, callback) {
var fields = options.fields;
if (!fields || typeof fields !== 'object' || (Array.isArray(fields) && fields.length === 0)) {
return callback(new Error("Fields need to be set as an object or array for '" + options.act + "' action."));
}
if (!Array.isArray(fields)) {
fields = [fields];
}
fields.forEach(function(doc){
if (doc.hasOwnProperty('_id') && doc._id.length === 24){
doc._id = createObjectIdFromHexString(doc._id);
}
});
db.collection(options.type).insertMany(fields, {}, function (err, result) {
if (null !== err) {
console.log("Error in MongoDB create:", err);
return callback(err);
}
var count = result.insertedCount;
var ret;
if (count === 1) {
ret = generateReturn(result.ops[0], options.type);
} else {
ret = {
'Status': 'OK',
'Count': count
};
}
callback(undefined, ret);
});
};
/**
* Mimic the read action from fh.db
* Reads a single row from the collection
*/
MongoDbClient.prototype.read = function (db, options, callback) {
var guid = options.guid;
if (!guid) {
// fh.db actually queries the DB with empty guid and formats the null response as {}, but we'll just use shortcut
return callback(undefined, {});
}
var query = {'_id': createObjectIdFromHexString(guid)};
var mongoOptions = {};
if (options.fields) {
var fields = {};
for (var i = 0; i < options.fields.length; i += 1) {
fields[options.fields[i]] = 1;
}
mongoOptions.projection = fields;
}
db.collection(options.type).findOne(query, mongoOptions, function (err, result) {
if (null !== err) {
console.log("Error in MongoDB read:", err);
return callback(err);
}
var ret = generateReturn(result, options.type);
callback(undefined, ret);
});
};
/**
* Mimic the list action from fh.db
* Reads multiple rows from the collection
*/
MongoDbClient.prototype.list = function (db, options, callback) {
var query = {};
for (var op in critOps) {
var fieldsValues = options[op];
if (fieldsValues) {
critOps[op](query, fieldsValues);
}
}
var mongoOptions = {};
if (options.fields) {
var fields = {};
for (var i = 0; i < options.fields.length; i += 1) {
fields[options.fields[i]] = 1;
}
mongoOptions.projection = fields;
}
if (options.skip && typeof options.skip === 'number' && options.skip >= 0) {
mongoOptions.skip = options.skip;
}
if (options.limit && typeof options.limit === 'number' && options.limit > 0) {
mongoOptions.limit = options.limit;
}
if (options.sort && typeof options.sort === 'object') {
mongoOptions.sort = options.sort;
}
db.collection(options.type).find(query, mongoOptions).toArray(function (err, docs) {
if (null !== err) {
console.log("Error in MongoDB list:", err);
return callback(err);
}
var retDocs = [];
for (var i = 0; i < docs.length; i += 1) {
retDocs.push(generateReturn(docs[i], options.type));
}
var listResp = {count: retDocs.length, list: retDocs};
callback(undefined, listResp);
});
};
/**
* Mimic the update action from fh.db
* Reads multiple rows from the collection
*/
MongoDbClient.prototype.update = function (db, options, callback) {
var self = this;
var guid = options.guid;
var fields = options.fields;
if (!fields) {
return callback(new Error("Invalid Params - 'fields' object required"));
}
if (!guid) {
return callback(new Error("Invalid Params - 'guid' is required"));
}
var query = {'_id': createObjectIdFromHexString(guid)};
db.collection(options.type).findOneAndReplace(query, fields, {}, function (err, result) {
if (null !== err) {
console.log("Error in MongoDB update:", err);
return callback(err);
}
// TODO process the response of the update operation instead of making another read operation
self.read(db, options, callback);
});
};
/**
* Mimic the delete action from fh.db
* Delete a row from the collection
*/
MongoDbClient.prototype.delete = function (db, options, callback) {
var guid = options.guid;
if (!guid) {
// fh.db actually tries to delete from the DB with empty guid and formats the null response as {}, but we'll just use shortcut
return callback(undefined, {});
}
var query = {'_id': createObjectIdFromHexString(guid)};
db.collection(options.type).findOneAndDelete(query, {}, function (err, result) {
if (null !== err) {
console.log("Error in MongoDB delete:", err);
return callback(err);
}
var ret = generateReturn(result.value, options.type);
callback(undefined, ret);
});
};
/**
* Mimic the deleteall action from fh.db
* Delete all collection entries
*/
MongoDbClient.prototype.deleteall = function (db, options, callback) {
db.collection(options.type).deleteMany({}, {}, function (err, result) {
if (null !== err) {
console.log("Error in MongoDB deleteall:", err);
return callback(err);
}
// TODO: check if result.result.n returns the same
var status = { status: 'ok', count: result.deletedCount };
callback(undefined, status);
});
};
/**
* Mimic the 'index' action from fh.db
* Add an index to field/fields
*/
MongoDbClient.prototype.index = function (db, options, callback) {
var indexes = options.index;
if (typeof indexes === "undefined") {
return callback(new Error("Invalid Params - 'index' object required for " + options.act + " action."));
}
var mapObj = {
"ASC": 1,
"DESC": -1,
"2D": "2d"
};
for (var indx in indexes) {
var type = indexes[indx].toString().toUpperCase();
var mongoType = mapObj[type] || 1;
indexes[indx] = mongoType;
}
db.collection(options.type, function (err, collection) {
if (null === collection) return callback(new Error("Collection doesn't exist"));
if (err) {
return callback(err);
}
collection.createIndex(indexes, function (err, indexName) {
if (err) return callback(err);
callback(undefined, {"status": "OK", "indexName": indexName});
});
});
};
/**
* Check fh.db options are valid before processing the DB action
*/
MongoDbClient.prototype.validateOptions = function (options, callback) {
if (!options.act) {
throw new Error("'act' undefined in params");
}
if (!(options.type) && ['close', 'list', 'export', 'import'].indexOf(options.act) === -1) {
throw new Error("'type' undefined in params");
}
if (options.type && options.type.length > MAX_COLLECTION_NAME) {
return callback("Error: 'type' name too long: '" + options.type + "'. Collection name cannot be greater than: " + MAX_COLLECTION_NAME);
}
};
MongoDbClient.prototype.processAction = function (options, callback) {
this.validateOptions(options, callback);
var action = this[options.act];
// Check that a function passed in 'action' exists
if (typeof action !== 'function') {
return callback(new Error("Unknown fh.db action"));
} else {
action = action.bind(this);
}
this.connect(function(err, db) {
if (null !== err) {
console.log('Error connecting to DB:', err);
return callback(err);
} else {
action(db, options, callback);
}
});
};
/**
* Sets the db method on MongoDbClient which will allow for easy swap of fh.db
* Allows for promise or callback
*/
MongoDbClient.prototype.db = function (options, callback) {
var self = this;
if (callback && typeof callback === 'function'){
return self.processAction(options, callback);
}
else {
return new Promise(function(resolve, reject) {
try {
self.processAction(options, function(err, result){
if (err) {
return reject(err);
}
resolve(result);
})
} catch (error) {
reject(error);
}
});
}
};
exports.client = new MongoDbClient(mongoClientOptions);