loopback-connector-couchdb2
Version:
LoopBack Connector for CouchDB 2.0
612 lines (577 loc) • 19.6 kB
JavaScript
// Copyright IBM Corp. 2017,2019. All Rights Reserved.
// Node module: loopback-connector-couchdb2
// This file is licensed under the Apache License 2.0.
// License text available at https://opensource.org/licenses/Apache-2.0
;
require('./init.js');
var CouchDB = require('../lib/couchdb');
var _ = require('lodash');
var should = require('should');
var testUtil = require('./lib/test-util');
var url = require('url');
var db, Product, CustomerSimple, SimpleEmployee;
describe('CouchDB2 connector', function() {
before(function(done) {
db = global.getDataSource();
Product = db.define('Product', {
name: {type: String},
description: {type: String},
price: {type: Number},
releases: {type: ['number']},
type: {type: [String]},
foo: {type: [Object]},
}, {forceId: false});
// CustomerSimple means some nested property defs are missing in modelDef,
// tests for CustomerSimple are created to make sure the typeSearch algorithm
// won't crash when iterating
CustomerSimple = db.define('CustomerSimple', {
name: {
type: String,
},
seq: {
type: Number,
},
address: {
street: String,
state: String,
zipCode: String,
tags: [],
},
friends: [],
favorate: {
labels: [
{label: String},
],
},
}, {
indexes: {
'address_city_index': {
keys: {
'address.city': 1,
},
},
'missing_property_index': {
keys: {
'missingProperty': 1,
},
},
},
});
SimpleEmployee = db.define('SimpleEmployee', {
id: {
type: Number,
id: true,
required: true,
generated: false,
},
name: {
type: String,
},
age: {
type: Number,
},
});
db.automigrate(done);
});
describe('model with array props gets updated properly', function() {
var prod1, prod2;
before('create Product', function(done) {
Product.create({
id: 1,
name: 'bread',
price: 100,
releases: [1, 2, 3],
type: ['plain', 'sesame', 'whole wheat'],
foo: [{id: 1, name: 'bread1'}, {id: 2, name: 'bread2'}],
}, function(err, product) {
if (err) return done(err);
prod1 = product;
Product.create({
id: 2,
name: 'bagel',
price: 100,
releases: [1, 2, 3],
type: ['plain', 'sesame', 'whole wheat'],
foo: [{id: 1, name: 'bagel1'}, {id: 2, name: 'bagel2'}],
}, function(err, product) {
if (err) return done(err);
prod2 = product;
delete prod1._rev;
delete prod2._rev;
done();
});
});
});
after(function(done) {
Product.destroyAll(null, {limit: testUtil.QUERY_MAX}, done);
});
it('updates a single instance with array props',
function(done) {
prod1.setAttribute('type', ['cinnamon raisin']);
prod1.setAttribute('releases', [4, 5, 6]);
prod1.setAttribute('foo', [{id: 3, name: 'bread3'}]);
Product.updateAll({id: 1}, prod1, function(err, res) {
if (err) return done(err);
Product.findById('1', function(err, res) {
if (err) done(err);
res.name.should.equal(prod1.name);
res.price.should.equal(prod1.price);
res.releases.should.deepEqual([4, 5, 6]);
res.type.should.deepEqual(['cinnamon raisin']);
res.foo.should.deepEqual([{id: 3, name: 'bread3'}]);
Product.findById('2', function(err, res) {
if (err) done(err);
res.name.should.equal(prod2.name);
res.price.should.equal(prod2.price);
res.releases.should.deepEqual(prod2.releases);
res.type.should.deepEqual(prod2.type);
res.foo.should.deepEqual(prod2.foo);
done();
});
});
});
});
it('updates all matching instances with array props',
function(done) {
var data = {
price: 200,
releases: [7],
type: ['everything'],
foo: [{id: 1, name: 'bar'}],
};
Product.updateAll({price: 100}, data, function(err, res) {
if (err) done(err);
Product.find(function(err, res) {
if (err) done(err);
res.length.should.equal(2);
res[0].name.should.oneOf(prod1.name, prod2.name);
res[0].price.should.equal(data.price);
res[0].releases.should.deepEqual(data.releases);
res[0].type.should.deepEqual(data.type);
res[0].foo.should.deepEqual(data.foo);
res[1].name.should.oneOf(prod1.name, prod2.name);
res[1].price.should.equal(data.price);
res[1].releases.should.deepEqual(data.releases);
res[1].type.should.deepEqual(data.type);
res[1].foo.should.deepEqual(data.foo);
done();
});
});
});
});
// the test suite is to make sure when
// user queries against a non existing property
// the app won't crash
describe('nested property', function() {
var seedCount = 0;
before(function createSampleData(done) {
var seedItems = seed();
seedCount = seedItems.length;
CustomerSimple.create(seedItems, done);
});
after(function(done) {
CustomerSimple.destroyAll(null, {limit: seedCount}, done);
});
describe('missing in modelDef', function() {
it('returns result when nested property is not an array type',
function(done) {
CustomerSimple.find({where: {'address.city': 'San Jose'}},
function(err, customers) {
if (err) return done(err);
customers.length.should.be.equal(1);
customers[0].address.city.should.be.eql('San Jose');
done();
});
});
it('returns null when first level property is array', function(done) {
CustomerSimple.find({where: {'friends.name': {regexp: /^Ringo/}}},
function(err, customers) {
if (err) return done(err);
customers.should.be.empty();
done();
});
});
it('returns result when first level property is array type' +
' and $elemMatch provided', function(done) {
CustomerSimple.find({where: {
'friends.$elemMatch.name': {regexp: /^Ringo/}}},
function(err, customers) {
if (err) return done(err);
customers.length.should.be.equal(2);
var expected1 = ['John Lennon', 'Paul McCartney'];
var expected2 = ['Paul McCartney', 'John Lennon'];
var actual = customers.map(function(c) { return c.name; });
should(actual).be.oneOf(expected1, expected2);
done();
});
});
it('returns null when multi-level nested property' +
' contains array type', function(done) {
CustomerSimple.find({where: {'address.tags.tag': 'business'}},
function(err, customers) {
if (err) return done(err);
customers.should.be.empty();
done();
});
});
it('returns result when multi-level nested property contains array type' +
' and $elemMatch provided', function(done) {
CustomerSimple.find({
where: {'address.tags.$elemMatch.tag': 'business'}},
function(err, customers) {
if (err) return done(err);
customers.length.should.be.equal(1);
customers[0].address.tags[0].tag.should.be.equal('business');
customers[0].address.tags[1].tag.should.be.equal('rent');
done();
});
});
it('returns error missing data type when sorting', function(done) {
CustomerSimple.find({where: {'address.state': 'CA'},
order: 'address.state DESC'},
function(err, customers) {
should.exist(err);
err.message.should.match(/no_usable_index,missing_sort_index/);
done();
});
});
it('returns result when sorting type provided - ' +
'missing first level property', function(done) {
// Similar test case exist in juggler, but since it takes time to
// recover them, I temporarily add it here
CustomerSimple.find({where: {'address.state': 'CA'},
order: 'missingProperty'}, function(err, customers) {
if (err) return done(err);
customers.length.should.be.equal(2);
var expected1 = ['San Mateo', 'San Jose'];
var expected2 = ['San Jose', 'San Mateo'];
var actual = customers.map(function(c) { return c.address.city; });
should(actual).be.oneOf(expected1, expected2);
done();
});
});
// To ensure sorting in CouchDB, it must follow some specifics:
// - At least one of the sort fields is included in the selector.
// - There is an index already defined, with all the sort fields in the same order.
// - Each object in the sort array has a single key.
// http://docs.couchdb.org/en/2.0.0/api/database/find.html#sort-syntax
it('returns result when sorting type provided - nested property',
function(done) {
CustomerSimple.find({where: {'address.city': {gt: null}},
order: 'address.city DESC'},
function(err, customers) {
if (err) return done(err);
customers.length.should.be.equal(2);
customers[0].address.city.should.be.eql('San Mateo');
customers[1].address.city.should.be.eql('San Jose');
done();
});
});
});
describe('defined in modelDef', function() {
it('returns result when complete query of' +
' multi-level nested property provided', function(done) {
CustomerSimple.find({
where: {'favorate.labels.$elemMatch.label': 'food'}},
function(err, customers) {
if (err) return done(err);
customers.length.should.be.equal(1);
customers[0].favorate.labels[0].label.should.be.equal('food');
customers[0].favorate.labels[1].label.should.be.equal('drink');
done();
});
});
});
});
describe('allow numerical `id` value', function() {
var data = [{
id: 1,
name: 'John Chow',
age: 45,
}, {
id: 5,
name: 'Kelly Johnson',
age: 25,
}, {
id: 12,
name: 'Michael Santer',
age: 30,
}];
var rev;
before(function(done) {
SimpleEmployee.create(data, function(err, result) {
should.not.exist(err);
rev = result[1]._rev;
done();
});
});
after(function(done) {
SimpleEmployee.destroyAll(null, {limit: testUtil.QUERY_MAX}, done);
});
it('find instances with numeric id (findById)', function(done) {
SimpleEmployee.findById(data[1].id, function(err, result) {
should.not.exist(err);
should.exist(result);
testUtil.checkData(data[1], result.__data);
done();
});
});
it('find instances with "where" filter', function(done) {
SimpleEmployee.find({where: {id: data[0].id}}, function(err, result) {
should.not.exist(err);
should.exist(result);
should.equal(result.length, 1);
testUtil.checkData(data[0], result[0].__data);
done();
});
});
it('find instances with "order" filter (ASC)', function(done) {
SimpleEmployee.find({order: 'id ASC'}, function(err, result) {
should.not.exist(err);
should.exist(result);
should(result[0].id).equal(data[0].id);
should(result[1].id).equal(data[1].id);
should(result[2].id).equal(data[2].id);
done();
});
});
it('find instances with "order" filter (DESC)', function(done) {
SimpleEmployee.find({order: 'id DESC'}, function(err, result) {
should.not.exist(err);
should.exist(result);
should(result[0].id).equal(data[2].id);
should(result[1].id).equal(data[1].id);
should(result[2].id).equal(data[0].id);
done();
});
});
it('replace instances with numerical id (replaceById)',
function(done) {
var updatedData = {
id: data[1].id,
name: 'Christian Thompson',
age: 32,
_rev: rev,
};
data[1].name = updatedData.name;
data[1].age = updatedData.age;
SimpleEmployee.replaceById(data[1].id, updatedData,
function(err, result) {
should.not.exist(err);
should.exist(result);
should.equal(result.id, data[1].id);
should.equal(result.name, updatedData.name);
should.equal(result.age, updatedData.age);
SimpleEmployee.find(function(err, result) {
should.not.exist(err);
should.exist(result);
should.equal(result.length, 3);
// checkData ignoring its order
data.forEach(function(item, index) {
var r = _.find(result, function(o) {
return o.__data.id === item.id;
});
testUtil.checkData(data[index], r.__data);
});
done();
});
});
});
it('destroy instances with numerical id (destroyById)', function(done) {
SimpleEmployee.destroyById(data[1].id, function(err, result) {
should.not.exist(err);
should.exist(result);
should(result).have.property('count');
should.equal(result.count, 1);
SimpleEmployee.find(function(err, result) {
should.not.exist(err);
should.exist(result);
should.equal(result.length, 2);
testUtil.checkData(data[0], result[0].__data);
testUtil.checkData(data[2], result[1].__data);
done();
});
});
});
it('destroy instances with "where" filter', function(done) {
SimpleEmployee.destroyAll({id: data[2].id}, {limit: testUtil.QUERY_MAX},
function(err, result) {
should.not.exist(err);
should.exist(result);
should(result).have.property('count');
should.equal(result.count, 1);
SimpleEmployee.find(function(err, result) {
should.not.exist(err);
should.exist(result);
should.equal(result.length, 1);
testUtil.checkData(data[0], result[0].__data);
done();
});
});
});
after(function(done) {
SimpleEmployee.destroyAll(null, {limit: 1000}, function(err) {
return done(err);
});
});
});
});
describe('CouchDB2 constructor', function() {
it('should allow passthrough of properties in the settings object',
function() {
var ds = global.getDataSource();
ds.settings = _.clone(ds.settings) || {};
var result = {};
ds.settings.Driver = function(options) {
result = options;
var fakedb = {db: {}};
fakedb.db.get = function(opts, cb) {
cb();
};
return fakedb;
};
ds.settings.foobar = {
foo: 'bar',
};
ds.settings.plugin = 'whack-a-mole';
ds.settings.requestDefault = {proxy: 'http://localhost:8080'};
var connector = CouchDB.initialize(ds, function(err) {
should.not.exist(err);
should.exist(result.foobar);
result.foobar.foo.should.be.equal('bar');
result.plugin.should.be.equal(ds.settings.plugin);
should.exist(result.requestDefault);
result.requestDefault.proxy.should.be.equal('http://localhost:8080');
});
});
it('should pass the url as an object property', function() {
var ds = global.getDataSource();
ds.settings = _.clone(ds.settings) || {};
var result = {};
ds.settings.Driver = function(options) {
result = options;
var fakedb = {db: {}};
fakedb.db.get = function(opts, cb) {
cb();
};
return fakedb;
};
ds.settings.url = 'https://totallyfakeuser:fakepass@definitelynotreal.cloudant.com';
var connector = CouchDB.initialize(ds, function() {
// The url will definitely cause a connection error, so ignore.
should.exist(result.url);
result.url.should.equal(ds.settings.url);
});
});
it('should convert first part of url path to database name', function(done) {
var myConfig = _.clone(global.config);
myConfig.url = myConfig.url + '/some/random/path';
myConfig.database = '';
var result = {};
myConfig.Driver = function(options) {
result = options;
var fakedb = {db: {}};
fakedb.db.get = function(opts, cb) {
cb();
};
return fakedb;
};
var ds = global.getDataSource(myConfig);
result.url.should.equal(global.config.url);
result.database.should.equal('some');
done();
});
it('should give 401 error for wrong creds', function(done) {
var myConfig = _.clone(global.config);
var parsedUrl = url.parse(myConfig.url);
parsedUrl.auth = 'foo:bar';
myConfig.url = parsedUrl.format();
var ds = global.getDataSource(myConfig);
ds.once('error', function(err) {
should.exist(err);
err.statusCode.should.equal(401);
err.error.should.equal('unauthorized');
err.reason.should.equal('Name or password is incorrect.');
done();
});
});
it('should give 404 error for nonexistant db', function(done) {
var myConfig = _.clone(global.config);
var parsedUrl = url.parse(myConfig.url);
parsedUrl.path = '';
myConfig.url = parsedUrl.format();
myConfig.database = 'idontexist';
var ds = global.getDataSource(myConfig);
ds.once('error', function(err) {
should.exist(err);
err.statusCode.should.equal(404);
err.error.should.equal('not_found');
err.reason.should.equal('Database does not exist.');
done();
});
});
});
function seed() {
var beatles = [
{
seq: 0,
name: 'John Lennon',
email: 'john@b3atl3s.co.uk',
role: 'lead',
birthday: new Date('1980-12-08'),
order: 2,
vip: true,
address: {
street: '123 A St',
city: 'San Jose',
state: 'CA',
zipCode: '95131',
tags: [
{tag: 'business'},
{tag: 'rent'},
],
},
friends: [
{name: 'Paul McCartney'},
{name: 'George Harrison'},
{name: 'Ringo Starr'},
],
},
{
seq: 1,
name: 'Paul McCartney',
email: 'paul@b3atl3s.co.uk',
role: 'lead',
birthday: new Date('1942-06-18'),
order: 1,
vip: true,
address: {
street: '456 B St',
city: 'San Mateo',
state: 'CA',
zipCode: '94065',
},
friends: [
{name: 'John Lennon'},
{name: 'George Harrison'},
{name: 'Ringo Starr'},
],
},
{
seq: 2,
name: 'George Harrison',
order: 5,
vip: false,
favorate: {
labels: [
{label: 'food'},
{label: 'drink'},
],
},
},
{seq: 3, name: 'Ringo Starr', order: 6, vip: false},
{seq: 4, name: 'Pete Best', order: 4},
{seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true},
];
return beatles;
}