678 lines
20 KiB
JavaScript
678 lines
20 KiB
JavaScript
// Copyright IBM Corp. 2016,2019. All Rights Reserved.
|
|
// Node module: loopback-datasource-juggler
|
|
// This file is licensed under the MIT License.
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
'use strict';
|
|
|
|
const should = require('./init.js');
|
|
const DataSource = require('../lib/datasource.js').DataSource;
|
|
|
|
describe('DataSource', function() {
|
|
it('clones settings to prevent surprising changes in passed args', () => {
|
|
const config = {connector: 'memory'};
|
|
|
|
const ds = new DataSource(config);
|
|
ds.settings.extra = true;
|
|
|
|
config.should.eql({connector: 'memory'});
|
|
});
|
|
|
|
it('reports helpful error when connector init throws', function() {
|
|
const throwingConnector = {
|
|
name: 'loopback-connector-throwing',
|
|
initialize: function(ds, cb) {
|
|
throw new Error('expected test error');
|
|
},
|
|
};
|
|
|
|
(function() {
|
|
// this is what LoopBack does
|
|
return new DataSource({
|
|
name: 'dsname',
|
|
connector: throwingConnector,
|
|
});
|
|
}).should.throw(/loopback-connector-throwing/);
|
|
});
|
|
|
|
it('reports helpful error when connector init via short name throws', function() {
|
|
(function() {
|
|
// this is what LoopBack does
|
|
return new DataSource({
|
|
name: 'dsname',
|
|
connector: 'throwing',
|
|
});
|
|
}).should.throw(/expected test error/);
|
|
});
|
|
|
|
it('reports helpful error when connector init via long name throws', function() {
|
|
(function() {
|
|
// this is what LoopBack does
|
|
return new DataSource({
|
|
name: 'dsname',
|
|
connector: 'loopback-connector-throwing',
|
|
});
|
|
}).should.throw(/expected test error/);
|
|
});
|
|
|
|
/**
|
|
* new DataSource(dsName, settings) without settings.name
|
|
*/
|
|
it('should retain the name assigned to it', function() {
|
|
const dataSource = new DataSource('myDataSource', {
|
|
connector: 'memory',
|
|
});
|
|
|
|
dataSource.name.should.equal('myDataSource');
|
|
});
|
|
|
|
/**
|
|
* new DataSource(dsName, settings)
|
|
*/
|
|
it('should allow the name assigned to it to take precedence over the settings name', function() {
|
|
const dataSource = new DataSource('myDataSource', {
|
|
name: 'defaultDataSource',
|
|
connector: 'memory',
|
|
});
|
|
|
|
dataSource.name.should.equal('myDataSource');
|
|
});
|
|
|
|
/**
|
|
* new DataSource(settings) with settings.name
|
|
*/
|
|
it('should retain the name from the settings if no name is assigned', function() {
|
|
const dataSource = new DataSource({
|
|
name: 'defaultDataSource',
|
|
connector: 'memory',
|
|
});
|
|
|
|
dataSource.name.should.equal('defaultDataSource');
|
|
});
|
|
|
|
/**
|
|
* new DataSource(undefined, settings)
|
|
*/
|
|
it('should retain the name from the settings if name is undefined', function() {
|
|
const dataSource = new DataSource(undefined, {
|
|
name: 'defaultDataSource',
|
|
connector: 'memory',
|
|
});
|
|
|
|
dataSource.name.should.equal('defaultDataSource');
|
|
});
|
|
|
|
/**
|
|
* new DataSource(settings) without settings.name
|
|
*/
|
|
it('should use the connector name if no name is provided', function() {
|
|
const dataSource = new DataSource({
|
|
connector: 'memory',
|
|
});
|
|
|
|
dataSource.name.should.equal('memory');
|
|
});
|
|
|
|
/**
|
|
* new DataSource(connectorInstance)
|
|
*/
|
|
it('should accept resolved connector', function() {
|
|
const mockConnector = {
|
|
name: 'loopback-connector-mock',
|
|
initialize: function(ds, cb) {
|
|
ds.connector = mockConnector;
|
|
return cb(null);
|
|
},
|
|
};
|
|
const dataSource = new DataSource(mockConnector);
|
|
|
|
dataSource.name.should.equal('loopback-connector-mock');
|
|
dataSource.connector.should.equal(mockConnector);
|
|
});
|
|
|
|
/**
|
|
* new DataSource(dsName, connectorInstance)
|
|
*/
|
|
it('should accept dsName and resolved connector', function() {
|
|
const mockConnector = {
|
|
name: 'loopback-connector-mock',
|
|
initialize: function(ds, cb) {
|
|
ds.connector = mockConnector;
|
|
return cb(null);
|
|
},
|
|
};
|
|
const dataSource = new DataSource('myDataSource', mockConnector);
|
|
|
|
dataSource.name.should.equal('myDataSource');
|
|
dataSource.connector.should.equal(mockConnector);
|
|
});
|
|
|
|
/**
|
|
* new DataSource(connectorInstance, settings)
|
|
*/
|
|
it('should accept resolved connector and settings', function() {
|
|
const mockConnector = {
|
|
name: 'loopback-connector-mock',
|
|
initialize: function(ds, cb) {
|
|
ds.connector = mockConnector;
|
|
return cb(null);
|
|
},
|
|
};
|
|
const dataSource = new DataSource(mockConnector, {name: 'myDataSource'});
|
|
|
|
dataSource.name.should.equal('myDataSource');
|
|
dataSource.connector.should.equal(mockConnector);
|
|
});
|
|
|
|
it('should set states correctly with eager connect', function(done) {
|
|
const mockConnector = {
|
|
name: 'loopback-connector-mock',
|
|
initialize: function(ds, cb) {
|
|
ds.connector = mockConnector;
|
|
this.connect(cb);
|
|
},
|
|
|
|
connect: function(cb) {
|
|
process.nextTick(function() {
|
|
cb(null);
|
|
});
|
|
},
|
|
};
|
|
const dataSource = new DataSource(mockConnector);
|
|
// DataSource is instantiated
|
|
// connected: false, connecting: false, initialized: false
|
|
dataSource.connected.should.be.false();
|
|
dataSource.connecting.should.be.false();
|
|
dataSource.initialized.should.be.false();
|
|
|
|
dataSource.on('initialized', function() {
|
|
// DataSource is initialized with lazyConnect
|
|
// connected: false, connecting: false, initialized: true
|
|
dataSource.connected.should.be.false();
|
|
dataSource.connecting.should.be.false();
|
|
dataSource.initialized.should.be.true();
|
|
});
|
|
|
|
dataSource.on('connected', function() {
|
|
// DataSource is now connected
|
|
// connected: true, connecting: false
|
|
dataSource.connected.should.be.true();
|
|
dataSource.connecting.should.be.false();
|
|
});
|
|
|
|
// Call connect() in next tick so that we'll receive initialized event
|
|
// first
|
|
process.nextTick(function() {
|
|
// At this point, the datasource is already connected by
|
|
// connector's (mockConnector) initialize function
|
|
dataSource.connect(function() {
|
|
// DataSource is now connected
|
|
// connected: true, connecting: false
|
|
dataSource.connected.should.be.true();
|
|
dataSource.connecting.should.be.false();
|
|
done();
|
|
});
|
|
// As the datasource is already connected, no connecting will happen
|
|
// connected: true, connecting: false
|
|
dataSource.connected.should.be.true();
|
|
dataSource.connecting.should.be.false();
|
|
});
|
|
});
|
|
|
|
it('should set states correctly with deferred connect', function(done) {
|
|
const mockConnector = {
|
|
name: 'loopback-connector-mock',
|
|
initialize: function(ds, cb) {
|
|
ds.connector = mockConnector;
|
|
// Explicitly call back with false to denote connection is not ready
|
|
process.nextTick(function() {
|
|
cb(null, false);
|
|
});
|
|
},
|
|
|
|
connect: function(cb) {
|
|
process.nextTick(function() {
|
|
cb(null);
|
|
});
|
|
},
|
|
};
|
|
const dataSource = new DataSource(mockConnector);
|
|
// DataSource is instantiated
|
|
// connected: false, connecting: false, initialized: false
|
|
dataSource.connected.should.be.false();
|
|
dataSource.connecting.should.be.false();
|
|
dataSource.initialized.should.be.false();
|
|
|
|
dataSource.on('initialized', function() {
|
|
// DataSource is initialized with lazyConnect
|
|
// connected: false, connecting: false, initialized: true
|
|
dataSource.connected.should.be.false();
|
|
dataSource.connecting.should.be.false();
|
|
dataSource.initialized.should.be.true();
|
|
});
|
|
|
|
dataSource.on('connected', function() {
|
|
// DataSource is now connected
|
|
// connected: true, connecting: false
|
|
dataSource.connected.should.be.true();
|
|
dataSource.connecting.should.be.false();
|
|
});
|
|
|
|
// Call connect() in next tick so that we'll receive initialized event
|
|
// first
|
|
process.nextTick(function() {
|
|
dataSource.connect(function() {
|
|
// DataSource is now connected
|
|
// connected: true, connecting: false
|
|
dataSource.connected.should.be.true();
|
|
dataSource.connecting.should.be.false();
|
|
done();
|
|
});
|
|
// As the datasource is not connected, connecting will happen
|
|
// connected: false, connecting: true
|
|
dataSource.connected.should.be.false();
|
|
dataSource.connecting.should.be.true();
|
|
});
|
|
});
|
|
|
|
it('should set states correctly with lazyConnect = true', function(done) {
|
|
const mockConnector = {
|
|
name: 'loopback-connector-mock',
|
|
initialize: function(ds, cb) {
|
|
ds.connector = mockConnector;
|
|
process.nextTick(function() {
|
|
cb(null);
|
|
});
|
|
},
|
|
|
|
connect: function(cb) {
|
|
process.nextTick(function() {
|
|
cb(null);
|
|
});
|
|
},
|
|
};
|
|
const dataSource = new DataSource(mockConnector, {lazyConnect: true});
|
|
// DataSource is instantiated
|
|
// connected: false, connecting: false, initialized: false
|
|
dataSource.connected.should.be.false();
|
|
dataSource.connecting.should.be.false();
|
|
dataSource.initialized.should.be.false();
|
|
|
|
dataSource.on('initialized', function() {
|
|
// DataSource is initialized with lazyConnect
|
|
// connected: false, connecting: false, initialized: true
|
|
dataSource.connected.should.be.false();
|
|
dataSource.connecting.should.be.false();
|
|
dataSource.initialized.should.be.true();
|
|
});
|
|
|
|
dataSource.on('connected', function() {
|
|
// DataSource is now connected
|
|
// connected: true, connecting: false
|
|
dataSource.connected.should.be.true();
|
|
dataSource.connecting.should.be.false();
|
|
});
|
|
|
|
// Call connect() in next tick so that we'll receive initialized event
|
|
// first
|
|
process.nextTick(function() {
|
|
dataSource.connect(function() {
|
|
// DataSource is now connected
|
|
// connected: true, connecting: false
|
|
dataSource.connected.should.be.true();
|
|
dataSource.connecting.should.be.false();
|
|
done();
|
|
});
|
|
// DataSource is now connecting
|
|
// connected: false, connecting: true
|
|
dataSource.connected.should.be.false();
|
|
dataSource.connecting.should.be.true();
|
|
});
|
|
});
|
|
|
|
it('provides stop() API calling disconnect', function(done) {
|
|
const mockConnector = {
|
|
name: 'loopback-connector-mock',
|
|
initialize: function(ds, cb) {
|
|
ds.connector = mockConnector;
|
|
process.nextTick(function() {
|
|
cb(null);
|
|
});
|
|
},
|
|
};
|
|
|
|
const dataSource = new DataSource(mockConnector);
|
|
dataSource.on('connected', function() {
|
|
// DataSource is now connected
|
|
// connected: true, connecting: false
|
|
dataSource.connected.should.be.true();
|
|
dataSource.connecting.should.be.false();
|
|
|
|
dataSource.stop(() => {
|
|
dataSource.connected.should.be.false();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('deleteModelByName()', () => {
|
|
it('removes the model from ModelBuilder registry', () => {
|
|
const ds = new DataSource('ds', {connector: 'memory'});
|
|
|
|
ds.createModel('TestModel');
|
|
Object.keys(ds.modelBuilder.models)
|
|
.should.containEql('TestModel');
|
|
Object.keys(ds.modelBuilder.definitions)
|
|
.should.containEql('TestModel');
|
|
|
|
ds.deleteModelByName('TestModel');
|
|
|
|
Object.keys(ds.modelBuilder.models)
|
|
.should.not.containEql('TestModel');
|
|
Object.keys(ds.modelBuilder.definitions)
|
|
.should.not.containEql('TestModel');
|
|
});
|
|
|
|
it('removes the model from connector registry', () => {
|
|
const ds = new DataSource('ds', {connector: 'memory'});
|
|
|
|
ds.createModel('TestModel');
|
|
Object.keys(ds.connector._models)
|
|
.should.containEql('TestModel');
|
|
|
|
ds.deleteModelByName('TestModel');
|
|
|
|
Object.keys(ds.connector._models)
|
|
.should.not.containEql('TestModel');
|
|
});
|
|
});
|
|
|
|
describe('execute', () => {
|
|
let ds;
|
|
beforeEach(() => ds = new DataSource('ds', {connector: 'memory'}));
|
|
|
|
it('calls connnector to execute the command', async () => {
|
|
let called = 'not called';
|
|
ds.connector.execute = function(command, args, options, callback) {
|
|
called = {command, args, options};
|
|
callback(null, 'a-result');
|
|
};
|
|
|
|
const result = await ds.execute(
|
|
'command',
|
|
['arg1', 'arg2'],
|
|
{'a-flag': 'a-value'},
|
|
);
|
|
|
|
result.should.be.equal('a-result');
|
|
called.should.be.eql({
|
|
command: 'command',
|
|
args: ['arg1', 'arg2'],
|
|
options: {'a-flag': 'a-value'},
|
|
});
|
|
});
|
|
|
|
it('supports shorthand version (cmd)', async () => {
|
|
let called = 'not called';
|
|
ds.connector.execute = function(command, args, options, callback) {
|
|
// copied from loopback-connector/lib/sql.js
|
|
if (typeof args === 'function' && options === undefined && callback === undefined) {
|
|
// execute(sql, callback)
|
|
options = {};
|
|
callback = args;
|
|
args = [];
|
|
}
|
|
|
|
called = {command, args, options};
|
|
callback(null, 'a-result');
|
|
};
|
|
|
|
const result = await ds.execute('command');
|
|
result.should.be.equal('a-result');
|
|
called.should.be.eql({
|
|
command: 'command',
|
|
args: [],
|
|
options: {},
|
|
});
|
|
});
|
|
|
|
it('supports shorthand version (cmd, args)', async () => {
|
|
let called = 'not called';
|
|
ds.connector.execute = function(command, args, options, callback) {
|
|
// copied from loopback-connector/lib/sql.js
|
|
if (typeof options === 'function' && callback === undefined) {
|
|
// execute(sql, params, callback)
|
|
callback = options;
|
|
options = {};
|
|
}
|
|
|
|
called = {command, args, options};
|
|
callback(null, 'a-result');
|
|
};
|
|
|
|
await ds.execute('command', ['arg1', 'arg2']);
|
|
called.should.be.eql({
|
|
command: 'command',
|
|
args: ['arg1', 'arg2'],
|
|
options: {},
|
|
});
|
|
});
|
|
|
|
it('converts multiple callbacks arguments into a promise resolved with an array', async () => {
|
|
ds.connector.execute = function() {
|
|
const callback = arguments[arguments.length - 1];
|
|
callback(null, 'result1', 'result2');
|
|
};
|
|
const result = await ds.execute('command');
|
|
result.should.eql(['result1', 'result2']);
|
|
});
|
|
|
|
it('allows args as object', async () => {
|
|
let called = 'not called';
|
|
ds.connector.execute = function(command, args, options, callback) {
|
|
called = {command, args, options};
|
|
callback();
|
|
};
|
|
|
|
// See https://www.npmjs.com/package/loopback-connector-neo4j-graph
|
|
const command = 'MATCH (u:User {email: {email}}) RETURN u';
|
|
await ds.execute(command, {email: 'alice@example.com'}, {options: true});
|
|
called.should.be.eql({
|
|
command,
|
|
args: {email: 'alice@example.com'},
|
|
options: {options: true},
|
|
});
|
|
});
|
|
|
|
it('supports MongoDB version (collection, cmd, args, options)', async () => {
|
|
let called = 'not called';
|
|
ds.connector.execute = function(...params) {
|
|
const callback = params.pop();
|
|
called = params;
|
|
callback(null, 'a-result');
|
|
};
|
|
|
|
const result = await ds.execute(
|
|
'collection',
|
|
'command',
|
|
['arg1', 'arg2'],
|
|
{options: true},
|
|
);
|
|
|
|
result.should.equal('a-result');
|
|
called.should.be.eql([
|
|
'collection',
|
|
'command',
|
|
['arg1', 'arg2'],
|
|
{options: true},
|
|
]);
|
|
});
|
|
|
|
it('supports free-form version (...params)', async () => {
|
|
let called = 'not called';
|
|
ds.connector.execute = function(...params) {
|
|
const callback = params.pop();
|
|
called = params;
|
|
callback(null, 'a-result');
|
|
};
|
|
|
|
const result = await ds.execute(
|
|
'arg1',
|
|
'arg2',
|
|
'arg3',
|
|
'arg4',
|
|
{options: true},
|
|
);
|
|
|
|
result.should.equal('a-result');
|
|
called.should.be.eql([
|
|
'arg1',
|
|
'arg2',
|
|
'arg3',
|
|
'arg4',
|
|
{options: true},
|
|
]);
|
|
});
|
|
|
|
it('throws NOT_IMPLEMENTED when no connector is provided', () => {
|
|
ds.connector = undefined;
|
|
return ds.execute('command').should.be.rejectedWith({
|
|
code: 'NOT_IMPLEMENTED',
|
|
});
|
|
});
|
|
|
|
it('throws NOT_IMPLEMENTED for connectors not implementing execute', () => {
|
|
ds.connector.execute = undefined;
|
|
return ds.execute('command').should.be.rejectedWith({
|
|
code: 'NOT_IMPLEMENTED',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('automigrate', () => {
|
|
it('reports connection errors (immediate connect)', async () => {
|
|
const dataSource = new DataSource({
|
|
connector: givenConnectorFailingOnConnect(),
|
|
});
|
|
dataSource.define('MyModel');
|
|
await dataSource.automigrate().should.be.rejectedWith(/test failure/);
|
|
});
|
|
|
|
it('reports connection errors (lazy connect)', () => {
|
|
const dataSource = new DataSource({
|
|
connector: givenConnectorFailingOnConnect(),
|
|
lazyConnect: true,
|
|
});
|
|
dataSource.define('MyModel');
|
|
return dataSource.automigrate().should.be.rejectedWith(/test failure/);
|
|
});
|
|
|
|
function givenConnectorFailingOnConnect() {
|
|
return givenMockConnector({
|
|
connect: function(cb) {
|
|
process.nextTick(() => cb(new Error('test failure')));
|
|
},
|
|
automigrate: function(models, cb) {
|
|
cb(new Error('automigrate should not have been called'));
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('autoupdate', () => {
|
|
it('reports connection errors (immediate connect)', async () => {
|
|
const dataSource = new DataSource({
|
|
connector: givenConnectorFailingOnConnect(),
|
|
});
|
|
dataSource.define('MyModel');
|
|
await dataSource.autoupdate().should.be.rejectedWith(/test failure/);
|
|
});
|
|
|
|
it('reports connection errors (lazy connect)', () => {
|
|
const dataSource = new DataSource({
|
|
connector: givenConnectorFailingOnConnect(),
|
|
lazyConnect: true,
|
|
});
|
|
dataSource.define('MyModel');
|
|
return dataSource.autoupdate().should.be.rejectedWith(/test failure/);
|
|
});
|
|
|
|
function givenConnectorFailingOnConnect() {
|
|
return givenMockConnector({
|
|
connect: function(cb) {
|
|
process.nextTick(() => cb(new Error('test failure')));
|
|
},
|
|
autoupdate: function(models, cb) {
|
|
cb(new Error('autoupdate should not have been called'));
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('deleteAllModels', () => {
|
|
it('removes all model definitions', () => {
|
|
const ds = new DataSource({connector: 'memory'});
|
|
ds.define('Category');
|
|
ds.define('Product');
|
|
|
|
Object.keys(ds.modelBuilder.definitions)
|
|
.should.deepEqual(['Category', 'Product']);
|
|
Object.keys(ds.modelBuilder.models)
|
|
.should.deepEqual(['Category', 'Product']);
|
|
Object.keys(ds.connector._models)
|
|
.should.deepEqual(['Category', 'Product']);
|
|
|
|
ds.deleteAllModels();
|
|
|
|
Object.keys(ds.modelBuilder.definitions).should.be.empty();
|
|
Object.keys(ds.modelBuilder.models).should.be.empty();
|
|
Object.keys(ds.connector._models).should.be.empty();
|
|
});
|
|
|
|
it('preserves the connector instance', () => {
|
|
const ds = new DataSource({connector: 'memory'});
|
|
const connector = ds.connector;
|
|
ds.deleteAllModels();
|
|
ds.connector.should.equal(connector);
|
|
});
|
|
});
|
|
|
|
describe('getMaxOfflineRequests', () => {
|
|
let ds;
|
|
beforeEach(() => ds = new DataSource('ds', {connector: 'memory'}));
|
|
|
|
it('sets the default maximum number of event listeners to 16', () => {
|
|
ds.getMaxOfflineRequests().should.be.eql(16);
|
|
});
|
|
|
|
it('uses provided number of listeners', () => {
|
|
ds.settings.maxOfflineRequests = 17;
|
|
ds.getMaxOfflineRequests().should.be.eql(17);
|
|
});
|
|
|
|
it('throws an error if a non-number is provided for the max number of listeners', () => {
|
|
ds.settings.maxOfflineRequests = '17';
|
|
|
|
(function() {
|
|
return ds.getMaxOfflineRequests();
|
|
}).should.throw('maxOfflineRequests must be a number');
|
|
});
|
|
});
|
|
});
|
|
|
|
function givenMockConnector(props) {
|
|
const connector = {
|
|
name: 'loopback-connector-mock',
|
|
initialize: function(ds, cb) {
|
|
ds.connector = connector;
|
|
if (ds.settings.lazyConnect) {
|
|
cb(null, false);
|
|
} else {
|
|
connector.connect(cb);
|
|
}
|
|
},
|
|
...props,
|
|
};
|
|
return connector;
|
|
}
|