UNPKG

18.8 kBJavaScriptView Raw
1// Copyright IBM Corp. 2016,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7
8const should = require('./init.js');
9const DataSource = require('../lib/datasource.js').DataSource;
10
11describe('DataSource', function() {
12 it('clones settings to prevent surprising changes in passed args', () => {
13 const config = {connector: 'memory'};
14
15 const ds = new DataSource(config);
16 ds.settings.extra = true;
17
18 config.should.eql({connector: 'memory'});
19 });
20
21 it('reports helpful error when connector init throws', function() {
22 const throwingConnector = {
23 name: 'loopback-connector-throwing',
24 initialize: function(ds, cb) {
25 throw new Error('expected test error');
26 },
27 };
28
29 (function() {
30 // this is what LoopBack does
31 return new DataSource({
32 name: 'dsname',
33 connector: throwingConnector,
34 });
35 }).should.throw(/loopback-connector-throwing/);
36 });
37
38 it('reports helpful error when connector init via short name throws', function() {
39 (function() {
40 // this is what LoopBack does
41 return new DataSource({
42 name: 'dsname',
43 connector: 'throwing',
44 });
45 }).should.throw(/expected test error/);
46 });
47
48 it('reports helpful error when connector init via long name throws', function() {
49 (function() {
50 // this is what LoopBack does
51 return new DataSource({
52 name: 'dsname',
53 connector: 'loopback-connector-throwing',
54 });
55 }).should.throw(/expected test error/);
56 });
57
58 /**
59 * new DataSource(dsName, settings) without settings.name
60 */
61 it('should retain the name assigned to it', function() {
62 const dataSource = new DataSource('myDataSource', {
63 connector: 'memory',
64 });
65
66 dataSource.name.should.equal('myDataSource');
67 });
68
69 /**
70 * new DataSource(dsName, settings)
71 */
72 it('should allow the name assigned to it to take precedence over the settings name', function() {
73 const dataSource = new DataSource('myDataSource', {
74 name: 'defaultDataSource',
75 connector: 'memory',
76 });
77
78 dataSource.name.should.equal('myDataSource');
79 });
80
81 /**
82 * new DataSource(settings) with settings.name
83 */
84 it('should retain the name from the settings if no name is assigned', function() {
85 const dataSource = new DataSource({
86 name: 'defaultDataSource',
87 connector: 'memory',
88 });
89
90 dataSource.name.should.equal('defaultDataSource');
91 });
92
93 /**
94 * new DataSource(undefined, settings)
95 */
96 it('should retain the name from the settings if name is undefined', function() {
97 const dataSource = new DataSource(undefined, {
98 name: 'defaultDataSource',
99 connector: 'memory',
100 });
101
102 dataSource.name.should.equal('defaultDataSource');
103 });
104
105 /**
106 * new DataSource(settings) without settings.name
107 */
108 it('should use the connector name if no name is provided', function() {
109 const dataSource = new DataSource({
110 connector: 'memory',
111 });
112
113 dataSource.name.should.equal('memory');
114 });
115
116 /**
117 * new DataSource(connectorInstance)
118 */
119 it('should accept resolved connector', function() {
120 const mockConnector = {
121 name: 'loopback-connector-mock',
122 initialize: function(ds, cb) {
123 ds.connector = mockConnector;
124 return cb(null);
125 },
126 };
127 const dataSource = new DataSource(mockConnector);
128
129 dataSource.name.should.equal('loopback-connector-mock');
130 dataSource.connector.should.equal(mockConnector);
131 });
132
133 /**
134 * new DataSource(dsName, connectorInstance)
135 */
136 it('should accept dsName and resolved connector', function() {
137 const mockConnector = {
138 name: 'loopback-connector-mock',
139 initialize: function(ds, cb) {
140 ds.connector = mockConnector;
141 return cb(null);
142 },
143 };
144 const dataSource = new DataSource('myDataSource', mockConnector);
145
146 dataSource.name.should.equal('myDataSource');
147 dataSource.connector.should.equal(mockConnector);
148 });
149
150 /**
151 * new DataSource(connectorInstance, settings)
152 */
153 it('should accept resolved connector and settings', function() {
154 const mockConnector = {
155 name: 'loopback-connector-mock',
156 initialize: function(ds, cb) {
157 ds.connector = mockConnector;
158 return cb(null);
159 },
160 };
161 const dataSource = new DataSource(mockConnector, {name: 'myDataSource'});
162
163 dataSource.name.should.equal('myDataSource');
164 dataSource.connector.should.equal(mockConnector);
165 });
166
167 it('should set states correctly with eager connect', function(done) {
168 const mockConnector = {
169 name: 'loopback-connector-mock',
170 initialize: function(ds, cb) {
171 ds.connector = mockConnector;
172 this.connect(cb);
173 },
174
175 connect: function(cb) {
176 process.nextTick(function() {
177 cb(null);
178 });
179 },
180 };
181 const dataSource = new DataSource(mockConnector);
182 // DataSource is instantiated
183 // connected: false, connecting: false, initialized: false
184 dataSource.connected.should.be.false();
185 dataSource.connecting.should.be.false();
186 dataSource.initialized.should.be.false();
187
188 dataSource.on('initialized', function() {
189 // DataSource is initialized with lazyConnect
190 // connected: false, connecting: false, initialized: true
191 dataSource.connected.should.be.false();
192 dataSource.connecting.should.be.false();
193 dataSource.initialized.should.be.true();
194 });
195
196 dataSource.on('connected', function() {
197 // DataSource is now connected
198 // connected: true, connecting: false
199 dataSource.connected.should.be.true();
200 dataSource.connecting.should.be.false();
201 });
202
203 // Call connect() in next tick so that we'll receive initialized event
204 // first
205 process.nextTick(function() {
206 // At this point, the datasource is already connected by
207 // connector's (mockConnector) initialize function
208 dataSource.connect(function() {
209 // DataSource is now connected
210 // connected: true, connecting: false
211 dataSource.connected.should.be.true();
212 dataSource.connecting.should.be.false();
213 done();
214 });
215 // As the datasource is already connected, no connecting will happen
216 // connected: true, connecting: false
217 dataSource.connected.should.be.true();
218 dataSource.connecting.should.be.false();
219 });
220 });
221
222 it('should set states correctly with deferred connect', function(done) {
223 const mockConnector = {
224 name: 'loopback-connector-mock',
225 initialize: function(ds, cb) {
226 ds.connector = mockConnector;
227 // Explicitly call back with false to denote connection is not ready
228 process.nextTick(function() {
229 cb(null, false);
230 });
231 },
232
233 connect: function(cb) {
234 process.nextTick(function() {
235 cb(null);
236 });
237 },
238 };
239 const dataSource = new DataSource(mockConnector);
240 // DataSource is instantiated
241 // connected: false, connecting: false, initialized: false
242 dataSource.connected.should.be.false();
243 dataSource.connecting.should.be.false();
244 dataSource.initialized.should.be.false();
245
246 dataSource.on('initialized', function() {
247 // DataSource is initialized with lazyConnect
248 // connected: false, connecting: false, initialized: true
249 dataSource.connected.should.be.false();
250 dataSource.connecting.should.be.false();
251 dataSource.initialized.should.be.true();
252 });
253
254 dataSource.on('connected', function() {
255 // DataSource is now connected
256 // connected: true, connecting: false
257 dataSource.connected.should.be.true();
258 dataSource.connecting.should.be.false();
259 });
260
261 // Call connect() in next tick so that we'll receive initialized event
262 // first
263 process.nextTick(function() {
264 dataSource.connect(function() {
265 // DataSource is now connected
266 // connected: true, connecting: false
267 dataSource.connected.should.be.true();
268 dataSource.connecting.should.be.false();
269 done();
270 });
271 // As the datasource is not connected, connecting will happen
272 // connected: false, connecting: true
273 dataSource.connected.should.be.false();
274 dataSource.connecting.should.be.true();
275 });
276 });
277
278 it('should set states correctly with lazyConnect = true', function(done) {
279 const mockConnector = {
280 name: 'loopback-connector-mock',
281 initialize: function(ds, cb) {
282 ds.connector = mockConnector;
283 process.nextTick(function() {
284 cb(null);
285 });
286 },
287
288 connect: function(cb) {
289 process.nextTick(function() {
290 cb(null);
291 });
292 },
293 };
294 const dataSource = new DataSource(mockConnector, {lazyConnect: true});
295 // DataSource is instantiated
296 // connected: false, connecting: false, initialized: false
297 dataSource.connected.should.be.false();
298 dataSource.connecting.should.be.false();
299 dataSource.initialized.should.be.false();
300
301 dataSource.on('initialized', function() {
302 // DataSource is initialized with lazyConnect
303 // connected: false, connecting: false, initialized: true
304 dataSource.connected.should.be.false();
305 dataSource.connecting.should.be.false();
306 dataSource.initialized.should.be.true();
307 });
308
309 dataSource.on('connected', function() {
310 // DataSource is now connected
311 // connected: true, connecting: false
312 dataSource.connected.should.be.true();
313 dataSource.connecting.should.be.false();
314 });
315
316 // Call connect() in next tick so that we'll receive initialized event
317 // first
318 process.nextTick(function() {
319 dataSource.connect(function() {
320 // DataSource is now connected
321 // connected: true, connecting: false
322 dataSource.connected.should.be.true();
323 dataSource.connecting.should.be.false();
324 done();
325 });
326 // DataSource is now connecting
327 // connected: false, connecting: true
328 dataSource.connected.should.be.false();
329 dataSource.connecting.should.be.true();
330 });
331 });
332
333 it('provides stop() API calling disconnect', function(done) {
334 const mockConnector = {
335 name: 'loopback-connector-mock',
336 initialize: function(ds, cb) {
337 ds.connector = mockConnector;
338 process.nextTick(function() {
339 cb(null);
340 });
341 },
342 };
343
344 const dataSource = new DataSource(mockConnector);
345 dataSource.on('connected', function() {
346 // DataSource is now connected
347 // connected: true, connecting: false
348 dataSource.connected.should.be.true();
349 dataSource.connecting.should.be.false();
350
351 dataSource.stop(() => {
352 dataSource.connected.should.be.false();
353 done();
354 });
355 });
356 });
357
358 describe('deleteModelByName()', () => {
359 it('removes the model from ModelBuilder registry', () => {
360 const ds = new DataSource('ds', {connector: 'memory'});
361
362 ds.createModel('TestModel');
363 Object.keys(ds.modelBuilder.models)
364 .should.containEql('TestModel');
365 Object.keys(ds.modelBuilder.definitions)
366 .should.containEql('TestModel');
367
368 ds.deleteModelByName('TestModel');
369
370 Object.keys(ds.modelBuilder.models)
371 .should.not.containEql('TestModel');
372 Object.keys(ds.modelBuilder.definitions)
373 .should.not.containEql('TestModel');
374 });
375
376 it('removes the model from connector registry', () => {
377 const ds = new DataSource('ds', {connector: 'memory'});
378
379 ds.createModel('TestModel');
380 Object.keys(ds.connector._models)
381 .should.containEql('TestModel');
382
383 ds.deleteModelByName('TestModel');
384
385 Object.keys(ds.connector._models)
386 .should.not.containEql('TestModel');
387 });
388 });
389
390 describe('execute', () => {
391 let ds;
392 beforeEach(() => ds = new DataSource('ds', {connector: 'memory'}));
393
394 it('calls connnector to execute the command', async () => {
395 let called = 'not called';
396 ds.connector.execute = function(command, args, options, callback) {
397 called = {command, args, options};
398 callback(null, 'a-result');
399 };
400
401 const result = await ds.execute(
402 'command',
403 ['arg1', 'arg2'],
404 {'a-flag': 'a-value'},
405 );
406
407 result.should.be.equal('a-result');
408 called.should.be.eql({
409 command: 'command',
410 args: ['arg1', 'arg2'],
411 options: {'a-flag': 'a-value'},
412 });
413 });
414
415 it('supports shorthand version (cmd)', async () => {
416 let called = 'not called';
417 ds.connector.execute = function(command, args, options, callback) {
418 called = {command, args, options};
419 callback(null, 'a-result');
420 };
421
422 const result = await ds.execute('command');
423 result.should.be.equal('a-result');
424 called.should.be.eql({
425 command: 'command',
426 args: [],
427 options: {},
428 });
429 });
430
431 it('supports shorthand version (cmd, args)', async () => {
432 let called = 'not called';
433 ds.connector.execute = function(command, args, options, callback) {
434 called = {command, args, options};
435 callback(null, 'a-result');
436 };
437
438 await ds.execute('command', ['arg1', 'arg2']);
439 called.should.be.eql({
440 command: 'command',
441 args: ['arg1', 'arg2'],
442 options: {},
443 });
444 });
445
446 it('converts multiple callbacks arguments into a promise resolved with an array', async () => {
447 ds.connector.execute = function(command, args, options, callback) {
448 callback(null, 'result1', 'result2');
449 };
450 const result = await ds.execute('command');
451 result.should.eql(['result1', 'result2']);
452 });
453
454 it('allows args as object', async () => {
455 let called = 'not called';
456 ds.connector.execute = function(command, args, options, callback) {
457 called = {command, args, options};
458 callback();
459 };
460
461 // See https://www.npmjs.com/package/loopback-connector-neo4j-graph
462 const command = 'MATCH (u:User {email: {email}}) RETURN u';
463 await ds.execute(command, {email: 'alice@example.com'});
464 called.should.be.eql({
465 command,
466 args: {email: 'alice@example.com'},
467 options: {},
468 });
469 });
470
471 it('throws NOT_IMPLEMENTED when no connector is provided', () => {
472 ds.connector = undefined;
473 return ds.execute('command').should.be.rejectedWith({
474 code: 'NOT_IMPLEMENTED',
475 });
476 });
477
478 it('throws NOT_IMPLEMENTED for connectors not implementing execute', () => {
479 ds.connector.execute = undefined;
480 return ds.execute('command').should.be.rejectedWith({
481 code: 'NOT_IMPLEMENTED',
482 });
483 });
484 });
485
486 describe('automigrate', () => {
487 it('reports connection errors (immediate connect)', async () => {
488 const dataSource = new DataSource({
489 connector: givenConnectorFailingOnConnect(),
490 });
491 dataSource.define('MyModel');
492 await dataSource.automigrate().should.be.rejectedWith(/test failure/);
493 });
494
495 it('reports connection errors (lazy connect)', () => {
496 const dataSource = new DataSource({
497 connector: givenConnectorFailingOnConnect(),
498 lazyConnect: true,
499 });
500 dataSource.define('MyModel');
501 return dataSource.automigrate().should.be.rejectedWith(/test failure/);
502 });
503
504 function givenConnectorFailingOnConnect() {
505 return givenMockConnector({
506 connect: function(cb) {
507 process.nextTick(() => cb(new Error('test failure')));
508 },
509 automigrate: function(models, cb) {
510 cb(new Error('automigrate should not have been called'));
511 },
512 });
513 }
514 });
515
516 describe('autoupdate', () => {
517 it('reports connection errors (immediate connect)', async () => {
518 const dataSource = new DataSource({
519 connector: givenConnectorFailingOnConnect(),
520 });
521 dataSource.define('MyModel');
522 await dataSource.autoupdate().should.be.rejectedWith(/test failure/);
523 });
524
525 it('reports connection errors (lazy connect)', () => {
526 const dataSource = new DataSource({
527 connector: givenConnectorFailingOnConnect(),
528 lazyConnect: true,
529 });
530 dataSource.define('MyModel');
531 return dataSource.autoupdate().should.be.rejectedWith(/test failure/);
532 });
533
534 function givenConnectorFailingOnConnect() {
535 return givenMockConnector({
536 connect: function(cb) {
537 process.nextTick(() => cb(new Error('test failure')));
538 },
539 autoupdate: function(models, cb) {
540 cb(new Error('autoupdate should not have been called'));
541 },
542 });
543 }
544 });
545
546 describe('deleteAllModels', () => {
547 it('removes all model definitions', () => {
548 const ds = new DataSource({connector: 'memory'});
549 ds.define('Category');
550 ds.define('Product');
551
552 Object.keys(ds.modelBuilder.definitions)
553 .should.deepEqual(['Category', 'Product']);
554 Object.keys(ds.modelBuilder.models)
555 .should.deepEqual(['Category', 'Product']);
556 Object.keys(ds.connector._models)
557 .should.deepEqual(['Category', 'Product']);
558
559 ds.deleteAllModels();
560
561 Object.keys(ds.modelBuilder.definitions).should.be.empty();
562 Object.keys(ds.modelBuilder.models).should.be.empty();
563 Object.keys(ds.connector._models).should.be.empty();
564 });
565
566 it('preserves the connector instance', () => {
567 const ds = new DataSource({connector: 'memory'});
568 const connector = ds.connector;
569 ds.deleteAllModels();
570 ds.connector.should.equal(connector);
571 });
572 });
573
574 describe('getMaxOfflineRequests', () => {
575 let ds;
576 beforeEach(() => ds = new DataSource('ds', {connector: 'memory'}));
577
578 it('sets the default maximum number of event listeners to 16', () => {
579 ds.getMaxOfflineRequests().should.be.eql(16);
580 });
581
582 it('uses provided number of listeners', () => {
583 ds.settings.maxOfflineRequests = 17;
584 ds.getMaxOfflineRequests().should.be.eql(17);
585 });
586
587 it('throws an error if a non-number is provided for the max number of listeners', () => {
588 ds.settings.maxOfflineRequests = '17';
589
590 (function() {
591 return ds.getMaxOfflineRequests();
592 }).should.throw('maxOfflineRequests must be a number');
593 });
594 });
595});
596
597function givenMockConnector(props) {
598 const connector = {
599 name: 'loopback-connector-mock',
600 initialize: function(ds, cb) {
601 ds.connector = connector;
602 if (ds.settings.lazyConnect) {
603 cb(null, false);
604 } else {
605 connector.connect(cb);
606 }
607 },
608 ...props,
609 };
610 return connector;
611}