1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const should = require('./init.js');
|
9 | const DataSource = require('../lib/datasource.js').DataSource;
|
10 |
|
11 | describe('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 |
|
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 |
|
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 |
|
51 | return new DataSource({
|
52 | name: 'dsname',
|
53 | connector: 'loopback-connector-throwing',
|
54 | });
|
55 | }).should.throw(/expected test error/);
|
56 | });
|
57 |
|
58 | |
59 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
183 |
|
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 |
|
190 |
|
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 |
|
198 |
|
199 | dataSource.connected.should.be.true();
|
200 | dataSource.connecting.should.be.false();
|
201 | });
|
202 |
|
203 |
|
204 |
|
205 | process.nextTick(function() {
|
206 |
|
207 |
|
208 | dataSource.connect(function() {
|
209 |
|
210 |
|
211 | dataSource.connected.should.be.true();
|
212 | dataSource.connecting.should.be.false();
|
213 | done();
|
214 | });
|
215 |
|
216 |
|
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 |
|
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 |
|
241 |
|
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 |
|
248 |
|
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 |
|
256 |
|
257 | dataSource.connected.should.be.true();
|
258 | dataSource.connecting.should.be.false();
|
259 | });
|
260 |
|
261 |
|
262 |
|
263 | process.nextTick(function() {
|
264 | dataSource.connect(function() {
|
265 |
|
266 |
|
267 | dataSource.connected.should.be.true();
|
268 | dataSource.connecting.should.be.false();
|
269 | done();
|
270 | });
|
271 |
|
272 |
|
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 |
|
296 |
|
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 |
|
303 |
|
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 |
|
311 |
|
312 | dataSource.connected.should.be.true();
|
313 | dataSource.connecting.should.be.false();
|
314 | });
|
315 |
|
316 |
|
317 |
|
318 | process.nextTick(function() {
|
319 | dataSource.connect(function() {
|
320 |
|
321 |
|
322 | dataSource.connected.should.be.true();
|
323 | dataSource.connecting.should.be.false();
|
324 | done();
|
325 | });
|
326 |
|
327 |
|
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 |
|
347 |
|
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 |
|
419 | if (typeof args === 'function' && options === undefined && callback === undefined) {
|
420 |
|
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 |
|
443 | if (typeof options === 'function' && callback === undefined) {
|
444 |
|
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 |
|
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 |
|
663 | function 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 | }
|