@e22m4u/js-repository-mongodb-adapter 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e22m4u/js-repository-mongodb-adapter",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "MongoDB адаптер для @e22m4u/js-repository",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,12 +1,10 @@
1
1
  /* eslint no-unused-vars: 0 */
2
2
  import {ObjectId} from 'mongodb';
3
3
  import {MongoClient} from 'mongodb';
4
- import {EventEmitter} from 'events';
5
- import {waitAsync} from './utils/index.js';
4
+ import {isIsoDate} from './utils/index.js';
6
5
  import {isObjectId} from './utils/index.js';
7
6
  import {Adapter} from '@e22m4u/js-repository';
8
7
  import {DataType} from '@e22m4u/js-repository';
9
- import {isIsoDate} from './utils/is-iso-date.js';
10
8
  import {capitalize} from '@e22m4u/js-repository';
11
9
  import {createMongodbUrl} from './utils/index.js';
12
10
  import {ServiceContainer} from '@e22m4u/js-service';
@@ -70,50 +68,14 @@ const MONGODB_OPTION_NAMES = [
70
68
  'zlibCompressionLevel',
71
69
  ];
72
70
 
73
- /**
74
- * Mongo client events.
75
- * 5.8.1
76
- *
77
- * @type {string[]}
78
- */
79
- const MONGO_CLIENT_EVENTS = [
80
- 'connectionPoolCreated',
81
- 'connectionPoolReady',
82
- 'connectionPoolCleared',
83
- 'connectionPoolClosed',
84
- 'connectionCreated',
85
- 'connectionReady',
86
- 'connectionClosed',
87
- 'connectionCheckOutStarted',
88
- 'connectionCheckOutFailed',
89
- 'connectionCheckedOut',
90
- 'connectionCheckedIn',
91
- 'commandStarted',
92
- 'commandSucceeded',
93
- 'commandFailed',
94
- 'serverOpening',
95
- 'serverClosed',
96
- 'serverDescriptionChanged',
97
- 'topologyOpening',
98
- 'topologyClosed',
99
- 'topologyDescriptionChanged',
100
- 'error',
101
- 'timeout',
102
- 'close',
103
- 'serverHeartbeatStarted',
104
- 'serverHeartbeatSucceeded',
105
- 'serverHeartbeatFailed',
106
- ];
107
-
108
71
  /**
109
72
  * Default settings.
110
73
  *
111
- * @type {{connectTimeoutMS: number}}
74
+ * @type {object}
112
75
  */
113
76
  const DEFAULT_SETTINGS = {
114
- reconnectInterval: 2000, // adapter specific option
115
- connectTimeoutMS: 2000,
116
- serverSelectionTimeoutMS: 2000,
77
+ // connectTimeoutMS: 2500,
78
+ // serverSelectionTimeoutMS: 2500,
117
79
  };
118
80
 
119
81
  /**
@@ -123,74 +85,27 @@ export class MongodbAdapter extends Adapter {
123
85
  /**
124
86
  * Mongodb instance.
125
87
  *
88
+ * @type {MongoClient}
126
89
  * @private
127
90
  */
128
91
  _client;
129
92
 
130
93
  /**
131
- * Collections.
132
- *
133
- * @type {Map<any, any>}
134
- * @private
135
- */
136
- _collections = new Map();
137
-
138
- /**
139
- * Connected.
94
+ * Client.
140
95
  *
141
- * @type {boolean}
142
- * @private
96
+ * @returns {MongoClient}
143
97
  */
144
- _connected = false;
145
-
146
- /**
147
- * Connected.
148
- *
149
- * @return {boolean}
150
- */
151
- get connected() {
152
- return this._connected;
98
+ get client() {
99
+ return this._client;
153
100
  }
154
101
 
155
102
  /**
156
- * Connecting.
157
- *
158
- * @type {boolean}
159
- * @private
160
- */
161
- _connecting = false;
162
-
163
- /**
164
- * Connecting.
165
- *
166
- * @return {boolean}
167
- */
168
- get connecting() {
169
- return this._connecting;
170
- }
171
-
172
- /**
173
- * Event emitter.
103
+ * Collections.
174
104
  *
105
+ * @type {Map<any, any>}
175
106
  * @private
176
107
  */
177
- _emitter;
178
-
179
- /**
180
- * Event emitter.
181
- *
182
- * @returns {EventEmitter}
183
- */
184
- get emitter() {
185
- if (this._emitter) return this._emitter;
186
- this._emitter = new EventEmitter();
187
- const emit = this._emitter.emit;
188
- this._emitter.emit = function (name, ...args) {
189
- emit.call(this, '*', name, ...args);
190
- return emit.call(this, name, ...args);
191
- };
192
- return this._emitter;
193
- }
108
+ _collections = new Map();
194
109
 
195
110
  /**
196
111
  * Constructor.
@@ -205,90 +120,9 @@ export class MongodbAdapter extends Adapter {
205
120
  settings.port = settings.port || 27017;
206
121
  settings.database = settings.database || settings.db || 'database';
207
122
  super(container, settings);
208
- }
209
-
210
- /**
211
- * Connect.
212
- *
213
- * @return {Promise<*|undefined>}
214
- * @private
215
- */
216
- async connect() {
217
- if (this._connecting) {
218
- await waitAsync(500);
219
- return this.connect();
220
- }
221
-
222
- if (this._connected) return;
223
- this._connecting = true;
224
-
225
123
  const options = selectObjectKeys(this.settings, MONGODB_OPTION_NAMES);
226
124
  const url = createMongodbUrl(this.settings);
227
-
228
- // console.log(`Connecting to ${url}`);
229
- if (this._client) {
230
- this._client.removeAllListeners();
231
- this._client.close(true);
232
- }
233
125
  this._client = new MongoClient(url, options);
234
- for (const event of MONGO_CLIENT_EVENTS) {
235
- const listener = (...args) => this.emitter.emit(event, ...args);
236
- this._client.on(event, listener);
237
- }
238
-
239
- const {reconnectInterval} = this.settings;
240
- const connectFn = async () => {
241
- if (this._connecting === false) return;
242
- this.emitter.emit('connecting');
243
- try {
244
- await this._client.connect();
245
- } catch (e) {
246
- this.emitter.emit('error', e);
247
- console.error(e);
248
- // console.log('MongoDB connection failed!');
249
- // console.log(`Reconnecting after ${reconnectInterval} ms.`);
250
- await waitAsync(reconnectInterval);
251
- return connectFn();
252
- }
253
- // console.log('MongoDB is connected.');
254
- this._connected = true;
255
- this._connecting = false;
256
- reconnectOnClose();
257
- this.emitter.emit('connected');
258
- };
259
-
260
- const reconnectOnClose = () =>
261
- this._client.once('serverClosed', event => {
262
- this.emitter.emit('disconnected', event);
263
- if (this._connected) {
264
- this._connected = false;
265
- this._connecting = true;
266
- // console.log('MongoDB lost connection!');
267
- // console.log(event);
268
- // console.log(`Reconnecting after ${reconnectInterval} ms.`);
269
- setTimeout(() => connectFn(), reconnectInterval);
270
- } else {
271
- // console.log('MongoDB connection closed.');
272
- }
273
- });
274
-
275
- return connectFn();
276
- }
277
-
278
- /**
279
- * Disconnect.
280
- *
281
- * @return {Promise<undefined>}
282
- */
283
- async disconnect() {
284
- this._connected = false;
285
- this._connecting = false;
286
- if (this._client) {
287
- const client = this._client;
288
- this._client = undefined;
289
- await client.close();
290
- client.removeAllListeners();
291
- }
292
126
  }
293
127
 
294
128
  /**
@@ -326,19 +160,6 @@ export class MongodbAdapter extends Adapter {
326
160
  return value;
327
161
  }
328
162
 
329
- /**
330
- * Coerce iso date.
331
- *
332
- * @param value
333
- * @return {*|Date}
334
- * @private
335
- */
336
- _coerceIsoDate(value) {
337
- if (value === null) return value;
338
- if (isIsoDate(value)) return new Date(value);
339
- return value;
340
- }
341
-
342
163
  /**
343
164
  * To database.
344
165
  *
@@ -419,7 +240,7 @@ export class MongodbAdapter extends Adapter {
419
240
  if (collection) return collection;
420
241
  const tableName =
421
242
  this.getService(ModelDefinitionUtils).getTableNameByModelName(modelName);
422
- collection = this._client.db(this.settings.database).collection(tableName);
243
+ collection = this.client.db(this.settings.database).collection(tableName);
423
244
  this._collections.set(modelName, collection);
424
245
  return collection;
425
246
  }
@@ -716,7 +537,6 @@ export class MongodbAdapter extends Adapter {
716
537
  * @return {Promise<object>}
717
538
  */
718
539
  async create(modelName, modelData, filter = undefined) {
719
- await this.connect();
720
540
  const idPropName = this._getIdPropName(modelName);
721
541
  const idValue = modelData[idPropName];
722
542
  if (idValue == null) {
@@ -752,7 +572,6 @@ export class MongodbAdapter extends Adapter {
752
572
  * @return {Promise<object>}
753
573
  */
754
574
  async replaceById(modelName, id, modelData, filter = undefined) {
755
- await this.connect();
756
575
  id = this._coerceId(id);
757
576
  const idPropName = this._getIdPropName(modelName);
758
577
  modelData[idPropName] = id;
@@ -779,7 +598,6 @@ export class MongodbAdapter extends Adapter {
779
598
  * @return {Promise<object>}
780
599
  */
781
600
  async patchById(modelName, id, modelData, filter = undefined) {
782
- await this.connect();
783
601
  id = this._coerceId(id);
784
602
  const idPropName = this._getIdPropName(modelName);
785
603
  delete modelData[idPropName];
@@ -804,7 +622,6 @@ export class MongodbAdapter extends Adapter {
804
622
  * @return {Promise<object[]>}
805
623
  */
806
624
  async find(modelName, filter = undefined) {
807
- await this.connect();
808
625
  filter = filter || {};
809
626
  const query = this._buildQuery(modelName, filter.where);
810
627
  const sort = this._buildSort(modelName, filter.order);
@@ -826,7 +643,6 @@ export class MongodbAdapter extends Adapter {
826
643
  * @return {Promise<object>}
827
644
  */
828
645
  async findById(modelName, id, filter = undefined) {
829
- await this.connect();
830
646
  id = this._coerceId(id);
831
647
  const table = this._getCollection(modelName);
832
648
  const projection = this._buildProjection(
@@ -847,7 +663,6 @@ export class MongodbAdapter extends Adapter {
847
663
  * @return {Promise<number>}
848
664
  */
849
665
  async delete(modelName, where = undefined) {
850
- await this.connect();
851
666
  const table = this._getCollection(modelName);
852
667
  const query = this._buildQuery(modelName, where);
853
668
  const {deletedCount} = await table.deleteMany(query);
@@ -862,7 +677,6 @@ export class MongodbAdapter extends Adapter {
862
677
  * @return {Promise<boolean>}
863
678
  */
864
679
  async deleteById(modelName, id) {
865
- await this.connect();
866
680
  id = this._coerceId(id);
867
681
  const table = this._getCollection(modelName);
868
682
  const {deletedCount} = await table.deleteOne({_id: id});
@@ -877,7 +691,6 @@ export class MongodbAdapter extends Adapter {
877
691
  * @return {Promise<boolean>}
878
692
  */
879
693
  async exists(modelName, id) {
880
- await this.connect();
881
694
  id = this._coerceId(id);
882
695
  const table = this._getCollection(modelName);
883
696
  const result = await table.findOne({_id: id}, {});
@@ -892,7 +705,6 @@ export class MongodbAdapter extends Adapter {
892
705
  * @return {Promise<number>}
893
706
  */
894
707
  async count(modelName, where = undefined) {
895
- await this.connect();
896
708
  const query = this._buildQuery(modelName, where);
897
709
  const table = this._getCollection(modelName);
898
710
  return await table.count(query);
@@ -1,17 +1,13 @@
1
- import net from 'net';
2
- import chai from 'chai';
3
1
  import {expect} from 'chai';
4
2
  import {ObjectId} from 'mongodb';
5
3
  import {MongoClient} from 'mongodb';
6
4
  import {format} from '@e22m4u/js-format';
7
- import {Service} from '@e22m4u/js-service';
8
5
  import {Schema} from '@e22m4u/js-repository';
9
6
  import {DataType} from '@e22m4u/js-repository';
10
7
  import {createMongodbUrl} from './utils/index.js';
11
8
  import {MongodbAdapter} from './mongodb-adapter.js';
12
9
  import {AdapterRegistry} from '@e22m4u/js-repository';
13
10
  import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '@e22m4u/js-repository';
14
- const sandbox = chai.spy.sandbox();
15
11
 
16
12
  const CONFIG = {
17
13
  host: process.env.MONGODB_HOST || 'localhost',
@@ -35,162 +31,16 @@ describe('MongodbAdapter', function () {
35
31
  this.timeout(15000);
36
32
 
37
33
  afterEach(async function () {
38
- sandbox.restore();
39
34
  await MDB_CLIENT.db(CONFIG.database).dropDatabase();
40
35
  });
41
36
 
42
37
  after(async function () {
43
38
  for await (const adapter of ADAPTERS_STACK) {
44
- await adapter.disconnect();
39
+ await adapter.client.close(true);
45
40
  }
46
41
  await MDB_CLIENT.close(true);
47
42
  });
48
43
 
49
- it('updates "connected" and "connecting" properties', async function () {
50
- const S = new Service();
51
- const events = [];
52
- const adapter = new MongodbAdapter(S.container, CONFIG);
53
- adapter.emitter.addListener('*', name => events.push(name));
54
- expect(adapter.connected).to.be.false;
55
- expect(adapter.connecting).to.be.false;
56
- expect(events).to.be.empty;
57
- const promise = adapter.connect();
58
- expect(adapter.connected).to.be.false;
59
- expect(adapter.connecting).to.be.true;
60
- expect(events).to.include('serverOpening');
61
- expect(events).to.not.include('connectionPoolReady');
62
- expect(events).to.not.include('serverClosed');
63
- await promise;
64
- expect(adapter.connected).to.be.true;
65
- expect(adapter.connecting).to.be.false;
66
- expect(events).to.include('connectionPoolReady');
67
- expect(events).to.not.include('serverClosed');
68
- await adapter.disconnect();
69
- expect(adapter.connected).to.be.false;
70
- expect(adapter.connecting).to.be.false;
71
- expect(events).to.include('serverClosed');
72
- });
73
-
74
- it('emits "connecting", "connected" and "disconnected" events', async function () {
75
- const S = new Service();
76
- const events = [];
77
- const adapter = new MongodbAdapter(S.container, CONFIG);
78
- adapter.emitter.addListener('*', name => events.push(name));
79
- expect(adapter.connected).to.be.false;
80
- expect(adapter.connecting).to.be.false;
81
- expect(events).to.be.empty;
82
- const promise = adapter.connect();
83
- expect(adapter.connected).to.be.false;
84
- expect(adapter.connecting).to.be.true;
85
- expect(events).to.include('connecting');
86
- expect(events).to.not.include('connected');
87
- expect(events).to.not.include('disconnected');
88
- await promise;
89
- expect(adapter.connected).to.be.true;
90
- expect(adapter.connecting).to.be.false;
91
- expect(events).to.include('connected');
92
- expect(events).to.not.include('disconnected');
93
- await adapter.disconnect();
94
- expect(adapter.connected).to.be.false;
95
- expect(adapter.connecting).to.be.false;
96
- expect(events).to.include('disconnected');
97
- });
98
-
99
- it('reconnects on server selection error', function (done) {
100
- const S = new Service();
101
- const server = net.createServer();
102
- let startupCounter = 0;
103
- server.listen(0, 'localhost', 2, () => {
104
- startupCounter++;
105
- expect(startupCounter).to.be.eq(1);
106
- const {address, port} = server.address();
107
- const attemptsLimit = 3;
108
- const serverSelectionTimeoutMS = 50;
109
- const adapter = new MongodbAdapter(S.container, {
110
- port,
111
- host: address,
112
- reconnectInterval: 0,
113
- serverSelectionTimeoutMS,
114
- });
115
- let attempts = 0;
116
- const startTime = new Date();
117
- adapter.emitter.addListener('connecting', () => {
118
- ++attempts;
119
- if (attempts !== attemptsLimit) return;
120
- const duration = new Date() - startTime;
121
- const accuracy = 10;
122
- server.close();
123
- adapter.disconnect();
124
- expect(adapter.connect).to.have.been.called.once;
125
- const attemptMs = duration / (attemptsLimit - 1);
126
- expect(attemptMs).to.be.gte(serverSelectionTimeoutMS - accuracy);
127
- expect(attemptMs).to.be.lte(serverSelectionTimeoutMS + accuracy);
128
- done();
129
- });
130
- adapter.emitter.addListener('error', error => {
131
- expect(error.message).to.be.eq(
132
- 'Server selection timed out after 50 ms',
133
- );
134
- });
135
- sandbox.on(adapter, 'connect');
136
- adapter.connect();
137
- });
138
- });
139
-
140
- it('reconnects on implicit disconnect', function (done) {
141
- const S = new Service();
142
- const reconnectsLimit = 2;
143
- const reconnectInterval = 50;
144
- const adapter = new MongodbAdapter(S.container, {
145
- ...CONFIG,
146
- reconnectInterval,
147
- });
148
- let startTime;
149
- let connects = 0;
150
- let reconnects = 0;
151
- adapter.emitter.on('connected', () => {
152
- ++connects;
153
- if (connects === 1) {
154
- adapter._client.close();
155
- return;
156
- }
157
- ++reconnects;
158
- if (startTime == null) startTime = new Date();
159
- if (reconnects < reconnectsLimit) {
160
- adapter._client.close();
161
- return;
162
- }
163
- const duration = new Date() - startTime;
164
- const accuracy = 10;
165
- adapter.disconnect();
166
- expect(adapter.connect).to.have.been.called.once;
167
- const attemptMs = duration / (reconnectsLimit - 1);
168
- expect(attemptMs).to.be.gt(reconnectInterval - accuracy);
169
- expect(attemptMs).to.be.lt(reconnectInterval + accuracy);
170
- expect(connects).to.be.eq(reconnectsLimit + 1);
171
- done();
172
- });
173
- sandbox.on(adapter, 'connect');
174
- adapter.connect();
175
- });
176
-
177
- it('does not reconnect on explicit disconnect', function (done) {
178
- const S = new Service();
179
- const reconnectInterval = 0;
180
- const adapter = new MongodbAdapter(S.container, {
181
- ...CONFIG,
182
- reconnectInterval,
183
- });
184
- adapter.emitter.once('connected', () => {
185
- adapter.emitter.once('connecting', () => {
186
- throw new Error('Unexpected reconnection');
187
- });
188
- adapter.emitter.once('disconnected', () => setTimeout(() => done(), 50));
189
- adapter.disconnect();
190
- });
191
- adapter.connect();
192
- });
193
-
194
44
  describe('create', function () {
195
45
  it('generates a new identifier when a value of a primary key is not provided', async function () {
196
46
  const schema = createSchema();
@@ -1,4 +1,4 @@
1
- export * from './wait-async.js';
1
+ export * from './is-iso-date.js';
2
2
  export * from './is-object-id.js';
3
3
  export * from './create-mongodb-url.js';
4
4
  export * from './transform-values-deep.js';
@@ -1,21 +0,0 @@
1
- import {InvalidArgumentError} from '@e22m4u/js-repository';
2
-
3
- /**
4
- * Wait.
5
- *
6
- * @example
7
- * ```ts
8
- * await waitAsync(1000); // 1sec
9
- * ```
10
- *
11
- * @param {number} ms Milliseconds
12
- * @returns {Promise<undefined>}
13
- */
14
- export function waitAsync(ms) {
15
- if (typeof ms !== 'number')
16
- throw new InvalidArgumentError(
17
- 'The first argument of "waitAsync" must be a Number, but %v given.',
18
- ms,
19
- );
20
- return new Promise(r => setTimeout(() => r(), ms));
21
- }
@@ -1,37 +0,0 @@
1
- import {expect} from 'chai';
2
- import {format} from '@e22m4u/js-format';
3
- import {waitAsync} from './wait-async.js';
4
-
5
- describe('wait', function () {
6
- it('requires the first argument as a number', function () {
7
- const throwable = v => () => waitAsync(v);
8
- const error = v =>
9
- format(
10
- 'The first argument of "waitAsync" must be a Number, but %s given.',
11
- v,
12
- );
13
- expect(throwable('string')).to.throw(error('"string"'));
14
- expect(throwable('')).to.throw(error('""'));
15
- expect(throwable(true)).to.throw(error('true'));
16
- expect(throwable(false)).to.throw(error('false'));
17
- expect(throwable([])).to.throw(error('Array'));
18
- expect(throwable({})).to.throw(error('Object'));
19
- expect(throwable(undefined)).to.throw(error('undefined'));
20
- expect(throwable(null)).to.throw(error('null'));
21
- throwable(10)();
22
- throwable(0)();
23
- });
24
-
25
- it('returns a promise that resolves after given milliseconds', async function () {
26
- const startTime = new Date();
27
- const delayMs = 15;
28
- const accuracyMs = 5;
29
- const promise = waitAsync(delayMs);
30
- expect(promise).to.be.instanceof(Promise);
31
- await promise;
32
- const endTime = new Date();
33
- const duration = endTime - startTime;
34
- expect(duration).to.be.gte(delayMs - accuracyMs);
35
- expect(duration).to.be.lte(delayMs + accuracyMs);
36
- });
37
- });