UNPKG

20.5 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 // copied from loopback-connector/lib/sql.js
419 if (typeof args === 'function' && options === undefined && callback === undefined) {
420 // execute(sql, callback)
421 options = {};
422 callback = args;
423 args = [];
424 }
425
426 called = {command, args, options};
427 callback(null, 'a-result');
428 };
429
430 const result = await ds.execute('command');
431 result.should.be.equal('a-result');
432 called.should.be.eql({
433 command: 'command',
434 args: [],
435 options: {},
436 });
437 });
438
439 it('supports shorthand version (cmd, args)', async () => {
440 let called = 'not called';
441 ds.connector.execute = function(command, args, options, callback) {
442 // copied from loopback-connector/lib/sql.js
443 if (typeof options === 'function' && callback === undefined) {
444 // execute(sql, params, callback)
445 callback = options;
446 options = {};
447 }
448
449 called = {command, args, options};
450 callback(null, 'a-result');
451 };
452
453 await ds.execute('command', ['arg1', 'arg2']);
454 called.should.be.eql({
455 command: 'command',
456 args: ['arg1', 'arg2'],
457 options: {},
458 });
459 });
460
461 it('converts multiple callbacks arguments into a promise resolved with an array', async () => {
462 ds.connector.execute = function() {
463 const callback = arguments[arguments.length - 1];
464 callback(null, 'result1', 'result2');
465 };
466 const result = await ds.execute('command');
467 result.should.eql(['result1', 'result2']);
468 });
469
470 it('allows args as object', async () => {
471 let called = 'not called';
472 ds.connector.execute = function(command, args, options, callback) {
473 called = {command, args, options};
474 callback();
475 };
476
477 // See https://www.npmjs.com/package/loopback-connector-neo4j-graph
478 const command = 'MATCH (u:User {email: {email}}) RETURN u';
479 await ds.execute(command, {email: 'alice@example.com'}, {options: true});
480 called.should.be.eql({
481 command,
482 args: {email: 'alice@example.com'},
483 options: {options: true},
484 });
485 });
486
487 it('supports MongoDB version (collection, cmd, args, options)', async () => {
488 let called = 'not called';
489 ds.connector.execute = function(...params) {
490 const callback = params.pop();
491 called = params;
492 callback(null, 'a-result');
493 };
494
495 const result = await ds.execute(
496 'collection',
497 'command',
498 ['arg1', 'arg2'],
499 {options: true},
500 );
501
502 result.should.equal('a-result');
503 called.should.be.eql([
504 'collection',
505 'command',
506 ['arg1', 'arg2'],
507 {options: true},
508 ]);
509 });
510
511 it('supports free-form version (...params)', async () => {
512 let called = 'not called';
513 ds.connector.execute = function(...params) {
514 const callback = params.pop();
515 called = params;
516 callback(null, 'a-result');
517 };
518
519 const result = await ds.execute(
520 'arg1',
521 'arg2',
522 'arg3',
523 'arg4',
524 {options: true},
525 );
526
527 result.should.equal('a-result');
528 called.should.be.eql([
529 'arg1',
530 'arg2',
531 'arg3',
532 'arg4',
533 {options: true},
534 ]);
535 });
536
537 it('throws NOT_IMPLEMENTED when no connector is provided', () => {
538 ds.connector = undefined;
539 return ds.execute('command').should.be.rejectedWith({
540 code: 'NOT_IMPLEMENTED',
541 });
542 });
543
544 it('throws NOT_IMPLEMENTED for connectors not implementing execute', () => {
545 ds.connector.execute = undefined;
546 return ds.execute('command').should.be.rejectedWith({
547 code: 'NOT_IMPLEMENTED',
548 });
549 });
550 });
551
552 describe('automigrate', () => {
553 it('reports connection errors (immediate connect)', async () => {
554 const dataSource = new DataSource({
555 connector: givenConnectorFailingOnConnect(),
556 });
557 dataSource.define('MyModel');
558 await dataSource.automigrate().should.be.rejectedWith(/test failure/);
559 });
560
561 it('reports connection errors (lazy connect)', () => {
562 const dataSource = new DataSource({
563 connector: givenConnectorFailingOnConnect(),
564 lazyConnect: true,
565 });
566 dataSource.define('MyModel');
567 return dataSource.automigrate().should.be.rejectedWith(/test failure/);
568 });
569
570 function givenConnectorFailingOnConnect() {
571 return givenMockConnector({
572 connect: function(cb) {
573 process.nextTick(() => cb(new Error('test failure')));
574 },
575 automigrate: function(models, cb) {
576 cb(new Error('automigrate should not have been called'));
577 },
578 });
579 }
580 });
581
582 describe('autoupdate', () => {
583 it('reports connection errors (immediate connect)', async () => {
584 const dataSource = new DataSource({
585 connector: givenConnectorFailingOnConnect(),
586 });
587 dataSource.define('MyModel');
588 await dataSource.autoupdate().should.be.rejectedWith(/test failure/);
589 });
590
591 it('reports connection errors (lazy connect)', () => {
592 const dataSource = new DataSource({
593 connector: givenConnectorFailingOnConnect(),
594 lazyConnect: true,
595 });
596 dataSource.define('MyModel');
597 return dataSource.autoupdate().should.be.rejectedWith(/test failure/);
598 });
599
600 function givenConnectorFailingOnConnect() {
601 return givenMockConnector({
602 connect: function(cb) {
603 process.nextTick(() => cb(new Error('test failure')));
604 },
605 autoupdate: function(models, cb) {
606 cb(new Error('autoupdate should not have been called'));
607 },
608 });
609 }
610 });
611
612 describe('deleteAllModels', () => {
613 it('removes all model definitions', () => {
614 const ds = new DataSource({connector: 'memory'});
615 ds.define('Category');
616 ds.define('Product');
617
618 Object.keys(ds.modelBuilder.definitions)
619 .should.deepEqual(['Category', 'Product']);
620 Object.keys(ds.modelBuilder.models)
621 .should.deepEqual(['Category', 'Product']);
622 Object.keys(ds.connector._models)
623 .should.deepEqual(['Category', 'Product']);
624
625 ds.deleteAllModels();
626
627 Object.keys(ds.modelBuilder.definitions).should.be.empty();
628 Object.keys(ds.modelBuilder.models).should.be.empty();
629 Object.keys(ds.connector._models).should.be.empty();
630 });
631
632 it('preserves the connector instance', () => {
633 const ds = new DataSource({connector: 'memory'});
634 const connector = ds.connector;
635 ds.deleteAllModels();
636 ds.connector.should.equal(connector);
637 });
638 });
639
640 describe('getMaxOfflineRequests', () => {
641 let ds;
642 beforeEach(() => ds = new DataSource('ds', {connector: 'memory'}));
643
644 it('sets the default maximum number of event listeners to 16', () => {
645 ds.getMaxOfflineRequests().should.be.eql(16);
646 });
647
648 it('uses provided number of listeners', () => {
649 ds.settings.maxOfflineRequests = 17;
650 ds.getMaxOfflineRequests().should.be.eql(17);
651 });
652
653 it('throws an error if a non-number is provided for the max number of listeners', () => {
654 ds.settings.maxOfflineRequests = '17';
655
656 (function() {
657 return ds.getMaxOfflineRequests();
658 }).should.throw('maxOfflineRequests must be a number');
659 });
660 });
661});
662
663function givenMockConnector(props) {
664 const connector = {
665 name: 'loopback-connector-mock',
666 initialize: function(ds, cb) {
667 ds.connector = connector;
668 if (ds.settings.lazyConnect) {
669 cb(null, false);
670 } else {
671 connector.connect(cb);
672 }
673 },
674 ...props,
675 };
676 return connector;
677}