alt-fh-db
Version:
Alternative for the FeedHenry `.db` API in `fh-mbaas-api`
805 lines (749 loc) • 25.1 kB
JavaScript
const mongoUrl = process.env.MONGODB_CONN_URL || 'mongodb://localhost:27017/db';
process.env.FH_MONGODB_CONN_URL = mongoUrl;
const expect = require('chai').expect;
const mongo = require('./index.js').client;
const mongodb = require('mongodb').MongoClient;
const testCollection = 'TEST';
const mongoOptions = { useNewUrlParser: true };
const emptyFunc = function() {};
/*
* Helper functions to prepare the dateabase
*/
const insertDoc = function(doc, callback) {
let mongoClient;
return mongodb.connect(mongoUrl, mongoOptions)
.then(client => {
mongoClient = client;
let db = client.db();
return db.collection(testCollection).insertOne(doc);
})
.then(result => {
mongoClient.close();
return result;
})
.then(result => {
callback(null, result.ops[0]);
})
.catch(error => {
callback(error);
});
};
const insertNDocs = function(numDocs, callback) {
let mongoClient;
return mongodb.connect(mongoUrl, mongoOptions)
.then(client => {
mongoClient = client;
let docs = [];
let strings = ['some', 'random', 'strings', 'to', 'be', 'used', 'for', 'testing'];
for (let i = 0; i < numDocs; i++) {
let str = strings[i] || null;
docs.push({[i.toString()]: i, num: i, str: str});
}
let db = client.db();
return db.collection(testCollection).insertMany(docs);
})
.then(result => {
mongoClient.close();
return result;
})
.then(result => {
callback(null, result.ops);
})
.catch(error => {
callback(error);
});
};
const countDocs = function(callback) {
let mongoClient;
return mongodb.connect(mongoUrl, mongoOptions)
.then(client => {
mongoClient = client;
let db = client.db();
return db.collection(testCollection).countDocuments();
})
.then(result => {
mongoClient.close();
return result;
})
.then(result => {
callback(null, result);
})
.catch(error => {
callback(error);
});
};
const deleteAll = function(callback) {
let db;
let mongoClient;
return mongodb.connect(mongoUrl, mongoOptions)
.then(client => {
mongoClient = client;
db = client.db();
return db.collection(testCollection).estimatedDocumentCount()
})
.then(result => {
if (result > 0) {
// console.log(`Collection ${testCollection} has ${result} documents, cleaning up...`);
return db.collection(testCollection).deleteMany()
}
else {
// console.log(`Collection ${testCollection} is empty`);
return true;
}
})
.then(result => {
mongoClient.close();
return result;
})
.then((result) => {
if (result.deletedCount) {
// console.log(`${result.deletedCount} documents deleted from collection ${testCollection}`);
}
callback(null, true);
})
.catch(error => {
console.log(error);
});
}
const filterObjectKeep = function(object, keysToKeep) {
return Object.keys(object)
.filter(key => keysToKeep.includes(key))
.reduce((obj, key) => {
return {
...obj,
[key]: object[key]
};
}, {});
};
const formatDocs = function(docs) {
return docs.map(doc => {
let id = doc._id.toHexString();
delete doc._id;
return { type: testCollection, guid: id, fields: doc };
});
}
// See https://access.redhat.com/documentation/en-us/red_hat_mobile_application_platform_hosted/3/html/cloud_api/fh-db for the behavior of fh-db
describe('mongoDBClient', function() {
context('using callbacks', function() {
describe('db', function() {
it("should throw an error if the 'act' option is not present", function(done){
expect(() => mongo.db({
type: 'some-type',
fields: {}
}, emptyFunc)).to.throw("'act' undefined in params");
done();
});
it("should return an error if the 'act' option is not correct", function(done){
mongo.db({
act: 'invalid',
type: 'some-type',
fields: {}
}, function(err, result){
expect(err).not.null;
expect(err.toString()).to.contain('Unknown fh.db action');
expect(result).to.be.undefined;
done();
});
});
describe("action 'create'", function() {
it('should create a single document and return the result', function(done) {
const type = testCollection;
let doc = {
one: 1,
two: 2
};
mongo.db({
act: 'create',
type: type,
fields: doc
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result.type).to.equal(type);
// Delete the _id because the mongo driver adds one
delete doc._id;
expect(result.fields).to.deep.equal(doc);
done();
});
});
it('should create multiple recods in one call and return the status', function(done) {
var type = testCollection;
var docs = [{
one: 1,
two: 2
},{
three: 3,
four: 4
}];
mongo.db({
act: 'create',
type: type,
fields: docs
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result).to.have.keys(['Status', 'Count']);
expect(result.Status).to.equal('OK');
expect(result.Count).to.equal(docs.length);
done();
});
});
it("should throw an error if 'type' is not provided", function(done) {
expect(() => mongo.db({
act: 'create',
fields: {}
}, emptyFunc)).to.throw("'type' undefined in params");
done();
});
});
describe("action 'read'", function() {
it('should read a single document and return the result', function(done) {
const type = testCollection;
let doc = {
one: 1,
two: 2
};
insertDoc(doc, function(err, result){
if (err) return done(err);
let id = result._id.toHexString();
mongo.db({
act: 'read',
type: type,
guid: id
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result.type).to.equal(type);
expect(result.guid).to.equal(id);
// Delete the _id because the mongo driver adds one
delete doc._id;
expect(result.fields).to.deep.equal(doc);
done();
});
})
});
it("should read a single document and return filtered results if 'fields' option is provided", function(done) {
let type = testCollection;
let doc = {
one: 1,
two: 2,
three: 3
};
let fields = ['one', 'three'];
insertDoc(doc, function(err, result){
if (err) return done(err);
let id = result._id.toHexString();
mongo.db({
act: 'read',
type: type,
guid: id,
fields: fields
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result.type).to.equal(type);
expect(result.guid).to.equal(id);
let filtered = filterObjectKeep(doc, fields);
expect(result.fields).to.deep.equal(filtered);
done();
});
})
});
it("should throw an error if 'type' is not provided", function(done) {
expect(() => mongo.db({
act: 'read',
fields: {}
}, emptyFunc)).to.throw("'type' undefined in params");
done();
});
it("should return an empty object if 'guid' is not provided", function(done) {
let type = testCollection;
mongo.db({
act: 'read',
type: type
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result).to.be.instanceof(Object);
expect(result).to.be.empty;
done();
});
});
it("should return an emtpy object if the collection doesn't exist", function(done) {
mongo.db({
act: 'read',
type: 'SOME-COLLECTION',
guid: 'some-guid'
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result).to.be.instanceof(Object);
expect(result).to.be.empty;
done();
});
});
});
describe("action 'update'", function() {
it('should update an entire document and return the result', function(done) {
const type = testCollection;
let doc = {
one: 1,
two: 2
};
let newDoc = {
three: 3,
four: 4,
five: 'five'
}
insertDoc(doc, function(err, result){
if (err) return done(err);
let id = result._id.toHexString();
mongo.db({
act: 'update',
type: type,
guid: id,
fields: newDoc
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result.type).to.equal(type);
expect(result.guid).to.equal(id);
// Delete the _id because the mongo driver adds one
delete doc._id;
expect(result.fields).to.deep.equal(newDoc);
done();
});
})
});
it("should throw an error if 'type' is not provided", function(done) {
expect(() => mongo.db({
act: 'update',
fields: {}
}, emptyFunc)).to.throw("'type' undefined in params");
done();
});
it("should return an error if 'fields' is not provided", function(done) {
let type = testCollection;
mongo.db({
act: 'update',
type: type,
guid: 'some-guid'
}, function(err, result){
expect(err).not.null;
expect(err.toString()).to.contain("Invalid Params - 'fields' object required");
expect(result).to.be.undefined;
done();
});
});
it("should return an error if 'guid' is not provided (failure in FeedHenry is expected)", function(done) {
let type = testCollection;
mongo.db({
act: 'update',
type: type,
fields: {}
}, function(err, result){
expect(err).not.null;
// NOTE: fh.db actually throws an unhandled exception here: `Uncaught TypeError: Cannot read property 'toString' of undefined`
// This is not really nice, so changing this to a callback error
expect(err.toString()).to.contain("Invalid Params - 'guid' is required");
expect(result).to.be.undefined;
done();
});
});
});
describe("action 'delete'", function() {
it('should delete an entry and return the original document', function(done) {
const type = testCollection;
let doc = {
one: 1,
two: 2
};
insertDoc(doc, function(err, insertedDoc){
if (err) return done(err);
let id = insertedDoc._id.toHexString();
mongo.db({
act: 'delete',
type: type,
guid: id
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result.guid).to.equal(id);
expect(result.type).to.equal(type);
// Delete the _id because the mongo driver adds one
delete doc._id;
expect(result.fields).to.deep.equal(doc);
done();
});
})
});
it("should throw an error if 'type' is not provided", function(done) {
expect(() => mongo.db({
act: 'delete',
fields: {}
}, emptyFunc)).to.throw("'type' undefined in params");
done();
});
it("should return an empty object if 'guid' is not provided", function(done) {
let type = testCollection;
mongo.db({
act: 'delete',
type: type
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result).to.be.instanceof(Object);
expect(result).to.be.empty;
done();
});
});
});
describe("action 'deleteall'", function() {
it('should delete all documents in the collection', function(done) {
const type = testCollection;
countDocs(function(err, numDocs){
if (err) return done(err);
mongo.db({
act: 'deleteall',
type: type
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result.status).to.equal('ok');
expect(result.count).to.equal(numDocs);
done();
});
});
});
it("should throw an error if 'type' is not provided", function(done) {
expect(() => mongo.db({
act: 'deleteall'
}, emptyFunc)).to.throw("'type' undefined in params");
done();
});
});
describe("action 'list'", function() {
it('should list all documents in the collection', function(done) {
const type = testCollection;
deleteAll(function(err, status){
if (err) return done(err);
let numDocs = 5;
insertNDocs(5, function(err, insertedDocs) {
mongo.db({
act: 'list',
type: type
}, function(err, result){
if (err) return done(err);
let expectedDocs = formatDocs(insertedDocs);
expect(err).to.be.undefined;
expect(result.count).to.equal(numDocs);
expect(result.list).to.deep.equal(expectedDocs);
done();
});
});
});
});
it("should list all documents and sort them", function(done) {
const type = testCollection;
deleteAll(function(err, status){
if (err) return done(err);
let numDocs = 5;
insertNDocs(5, function(err, insertedDocs) {
mongo.db({
act: 'list',
type: type,
sort: {
str: 1
}
}, function(err, result){
if (err) return done(err);
let expectedDocs = formatDocs(insertedDocs);
// sort by 'fields.str'
expectedDocs = expectedDocs.sort((a,b) => a.fields.str.localeCompare(b.fields.str));
expect(err).to.be.undefined;
expect(result.count).to.equal(numDocs);
expect(result.list).to.deep.equal(expectedDocs);
done();
});
});
});
});
it("should list all documents and sort them in reverse order", function(done) {
const type = testCollection;
deleteAll(function(err, status){
if (err) return done(err);
let numDocs = 5;
insertNDocs(5, function(err, insertedDocs) {
mongo.db({
act: 'list',
type: type,
sort: {
str: -1
}
}, function(err, result){
if (err) return done(err);
let expectedDocs = formatDocs(insertedDocs);
// sort by 'fields.str', reverse
expectedDocs = expectedDocs.sort((a,b) => b.fields.str.localeCompare(a.fields.str));
expect(err).to.be.undefined;
expect(result.count).to.equal(numDocs);
expect(result.list).to.deep.equal(expectedDocs);
done();
});
});
});
});
it("should list all documents with pagination", function(done) {
const type = testCollection;
deleteAll(function(err, status){
if (err) return done(err);
let options = {
skip: 2,
limit: 3
};
insertNDocs(12, function(err, insertedDocs) {
mongo.db({
act: 'list',
type: type,
...options
}, function(err, result){
if (err) return done(err);
let expectedDocs = formatDocs(insertedDocs);
// skip the first elements and take only 'limit' elements
expectedDocs = expectedDocs.slice(options.skip, options.skip+options.limit);
expect(err).to.be.undefined;
expect(result.count).to.equal(expectedDocs.length);
expect(result.list).to.deep.equal(expectedDocs);
done();
});
});
});
});
it("should list all documents with multiple restrictions", function(done) {
const type = testCollection;
deleteAll(function(err, status){
if (err) return done(err);
let query = {
eq: { str: 'some' },
ne: { num: 10 },
in: { '0': [0, 1] }
};
insertNDocs(5, function(err, insertedDocs) {
mongo.db({
act: 'list',
type: type,
...query
}, function(err, result){
if (err) return done(err);
let expectedDocs = formatDocs(insertedDocs);
// skip the first elements and take only 'limit' elements
expect(err).to.be.undefined;
expect(result.count).to.equal(1);
expect(result.list[0]).to.deep.equal(expectedDocs[0]);
done();
});
});
});
});
it("should list all documents with multiple restrictions, and limited fields", function(done) {
const type = testCollection;
deleteAll(function(err, status){
if (err) return done(err);
let query = {
eq: { str: 'some' },
ne: { num: 10 },
in: { '0': [0, 1] }
};
let fields = ['str'];
insertNDocs(5, function(err, insertedDocs) {
mongo.db({
act: 'list',
type: type,
fields: fields,
...query
}, function(err, result){
if (err) return done(err);
let expectedDocs = formatDocs(insertedDocs);
expectedDocs.forEach(function(doc){
doc.fields = filterObjectKeep(doc.fields, fields);
});
expect(err).to.be.undefined;
expect(result.count).to.equal(1);
expect(result.list[0]).to.deep.equal(expectedDocs[0]);
done();
});
});
});
});
it("should list all documents filtered by regex matching", function(done) {
const type = testCollection;
let doc = { username: 'this-is-username', password: 'abc' };
insertDoc(doc, function(err){
if (err) return done(err);
let query = {
like: { username: new RegExp('user', 'i') },
eq: { password: 'abc' }
};
mongo.db({
act: 'list',
type: type,
...query
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result.count).to.equal(1);
delete doc._id; // mongodb driver adds the _id to the original object
expect(result.list[0].fields).to.deep.equal(doc);
done();
});
});
});
// TODO: list with Geo search
});
describe("action 'index'", function() {
it('should delete all documents in the collection', function(done) {
mongo.db({
act: 'index',
type: testCollection,
index: {
location: "2D",
str: "ASC"
}
}, function(err, result){
if (err) return done(err);
expect(err).to.be.undefined;
expect(result.status).to.equal('OK');
expect(result.indexName).to.equal('location_2d_str_1');
done();
});
});
it("should return an error if '2d' index is not first", function(done) {
mongo.db({
act: 'index',
type: testCollection,
index: {
str: "ASC",
location: "2D"
}
}, function(err, result){
expect(err).not.null;
expect(err.toString()).to.contain('2d has to be first in index');
expect(result).to.be.undefined;
done();
});
});
// TODO: test for creating the same index twice
});
});
});
// the 'mongo' instance does not belong to 'fh-mbaas-api'
if (!mongo.mbaasExpress) {
context('using promises', () => {
describe('db', () => {
it("should return a promise", () => {
let p = mongo.db({
act: 'read',
type: 'test',
guid: 'fake'
});
expect(p).to.be.a('promise');
return p;
});
it("reject promise with an error if the 'act' option is not present", () => {
return mongo.db({
type: 'some-type',
fields: {}
})
.then(() => {throw new Error('unexpected promise resolution');})
.catch(err => {
expect(err).to.be.an('Error');
expect(err.message).to.equal("'act' undefined in params");
});
});
it("reject promise with an error if the 'act'option is not correct", () => {
return mongo.db({
act: 'invalid',
type: 'some-type',
fields: {}
})
.then(() => {throw new Error('unexpected promise resolution');})
.catch(err => {
expect(err).to.be.an('Error');
expect(err.message).to.equal('Unknown fh.db action');
});
});
describe("action 'create'", () => {
it('should create a single document and resolve the promise with the result', () => {
const type = testCollection;
let doc = {
one: 1,
two: 2
};
return mongo.db({
act: 'create',
type: type,
fields: doc
})
.then((result) => {
expect(result.type).to.equal(type);
// Delete the _id because the mongo driver adds one
delete doc._id;
expect(result.fields).to.deep.equal(doc);
})
.catch(err => {
throw(new Error(`unexpected promise rejection with error: ${err}`));
});
});
});
});
});
}
// TODO: add test for connection close
});
/*
* Clean up the database collection before starting the test suite
*/
before(function() {
deleteAll(function(err){
if (err) {
console.log(err);
}
else {
console.log(`Collection ${testCollection} initialized`);
}
});
});
function dropCollection(db, collectionName) {
return db.collection(collectionName).drop()
.then(result => {
if (result) {
console.log(`Collection ${testCollection} dropped`);
}
})
.catch(error => {
if (error.codeName === 'NamespaceNotFound') {
console.log(`Collection ${testCollection} doesn't exist, ignoring the error`);
}
});
}
/*
* Drop the database collection before starting the test suite
*/
after(function() {
let mongoClient;
mongodb.connect(mongoUrl, mongoOptions)
.then(client => {
mongoClient = client;
return dropCollection;
})
.then(() => {
return mongoClient.close();
})
.then(() => {
console.log('MongoDB connection closed');
return mongo.close();
})
.catch(error => {
console.log(`Unexpected error error: ${error}`);
})
});