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

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.14",
3
+ "version": "0.0.16",
4
4
  "description": "MongoDB адаптер для @e22m4u/js-repository",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -38,7 +38,7 @@
38
38
  "peerDependencies": {
39
39
  "@e22m4u/js-format": "*",
40
40
  "@e22m4u/js-service": "*",
41
- "@e22m4u/js-repository": "~0.0.31"
41
+ "@e22m4u/js-repository": "~0.0.32"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@commitlint/cli": "^17.7.1",
@@ -1,6 +1,8 @@
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
6
  import {isObjectId} from './utils/index.js';
5
7
  import {Adapter} from '@e22m4u/js-repository';
6
8
  import {DataType} from '@e22m4u/js-repository';
@@ -68,6 +70,41 @@ const MONGODB_OPTION_NAMES = [
68
70
  'zlibCompressionLevel',
69
71
  ];
70
72
 
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
+
71
108
  /**
72
109
  * Default settings.
73
110
  *
@@ -132,6 +169,29 @@ export class MongodbAdapter extends Adapter {
132
169
  return this._connecting;
133
170
  }
134
171
 
172
+ /**
173
+ * Event emitter.
174
+ *
175
+ * @private
176
+ */
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
+ }
194
+
135
195
  /**
136
196
  * Constructor.
137
197
  *
@@ -140,11 +200,11 @@ export class MongodbAdapter extends Adapter {
140
200
  */
141
201
  constructor(container, settings) {
142
202
  settings = Object.assign({}, DEFAULT_SETTINGS, settings || {});
143
- super(container, settings);
144
203
  settings.protocol = settings.protocol || 'mongodb';
145
204
  settings.hostname = settings.hostname || settings.host || '127.0.0.1';
146
205
  settings.port = settings.port || 27017;
147
- settings.database = settings.database || settings.db || 'test';
206
+ settings.database = settings.database || settings.db || 'database';
207
+ super(container, settings);
148
208
  }
149
209
 
150
210
  /**
@@ -155,8 +215,7 @@ export class MongodbAdapter extends Adapter {
155
215
  */
156
216
  async connect() {
157
217
  if (this._connecting) {
158
- const tryAgainAfter = 500;
159
- await new Promise(r => setTimeout(() => r(), tryAgainAfter));
218
+ await waitAsync(500);
160
219
  return this.connect();
161
220
  }
162
221
 
@@ -167,53 +226,69 @@ export class MongodbAdapter extends Adapter {
167
226
  const url = createMongodbUrl(this.settings);
168
227
 
169
228
  // console.log(`Connecting to ${url}`);
229
+ if (this._client) {
230
+ this._client.removeAllListeners();
231
+ this._client.close(true);
232
+ }
170
233
  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
+ }
171
238
 
172
239
  const {reconnectInterval} = this.settings;
173
240
  const connectFn = async () => {
241
+ if (this._connecting === false) return;
242
+ this.emitter.emit('connecting');
174
243
  try {
175
244
  await this._client.connect();
176
245
  } catch (e) {
246
+ this.emitter.emit('error', e);
177
247
  console.error(e);
178
248
  // console.log('MongoDB connection failed!');
179
249
  // console.log(`Reconnecting after ${reconnectInterval} ms.`);
180
- await new Promise(r => setTimeout(() => r(), reconnectInterval));
250
+ await waitAsync(reconnectInterval);
181
251
  return connectFn();
182
252
  }
183
253
  // console.log('MongoDB is connected.');
184
254
  this._connected = true;
185
255
  this._connecting = false;
256
+ reconnectOnClose();
257
+ this.emitter.emit('connected');
186
258
  };
187
259
 
188
- await connectFn();
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
+ });
189
274
 
190
- this._client.once('serverClosed', event => {
191
- if (this._connected) {
192
- this._connected = false;
193
- // console.log('MongoDB lost connection!');
194
- console.log(event);
195
- // console.log(`Reconnecting after ${reconnectInterval} ms.`);
196
- setTimeout(() => connectFn(), reconnectInterval);
197
- } else {
198
- // console.log('MongoDB connection closed.');
199
- }
200
- });
275
+ return connectFn();
201
276
  }
202
277
 
203
278
  /**
204
279
  * Disconnect.
205
280
  *
206
- * @return {Promise<*|undefined>}
281
+ * @return {Promise<undefined>}
207
282
  */
208
283
  async disconnect() {
209
- if (this._connecting) {
210
- const tryAgainAfter = 500;
211
- await new Promise(r => setTimeout(() => r(), tryAgainAfter));
212
- return this.disconnect();
213
- }
214
- if (!this._connected) return;
215
284
  this._connected = false;
216
- if (this._client) await this._client.close();
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
+ }
217
292
  }
218
293
 
219
294
  /**
@@ -1,3 +1,5 @@
1
+ import net from 'net';
2
+ import chai from 'chai';
1
3
  import {expect} from 'chai';
2
4
  import {ObjectId} from 'mongodb';
3
5
  import {MongoClient} from 'mongodb';
@@ -9,6 +11,7 @@ import {createMongodbUrl} from './utils/index.js';
9
11
  import {MongodbAdapter} from './mongodb-adapter.js';
10
12
  import {AdapterRegistry} from '@e22m4u/js-repository';
11
13
  import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '@e22m4u/js-repository';
14
+ const sandbox = chai.spy.sandbox();
12
15
 
13
16
  const CONFIG = {
14
17
  host: process.env.MONGODB_HOST || 'localhost',
@@ -32,6 +35,7 @@ describe('MongodbAdapter', function () {
32
35
  this.timeout(15000);
33
36
 
34
37
  afterEach(async function () {
38
+ sandbox.restore();
35
39
  await MDB_CLIENT.db(CONFIG.database).dropDatabase();
36
40
  });
37
41
 
@@ -42,12 +46,149 @@ describe('MongodbAdapter', function () {
42
46
  await MDB_CLIENT.close(true);
43
47
  });
44
48
 
45
- it('able to connect and disconnect', async function () {
49
+ it('updates "connected" and "connecting" properties', async function () {
46
50
  const S = new Service();
51
+ const events = [];
47
52
  const adapter = new MongodbAdapter(S.container, CONFIG);
48
- await adapter.connect();
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;
49
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');
50
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();
51
192
  });
52
193
 
53
194
  describe('create', function () {
@@ -11,7 +11,7 @@ export function createMongodbUrl(options = {}) {
11
11
  );
12
12
  if (options.protocol && typeof options.protocol !== 'string')
13
13
  throw new InvalidArgumentError(
14
- 'MongoDB option "protocol" must be a string, but %v given.',
14
+ 'MongoDB option "protocol" must be a String, but %v given.',
15
15
  options.protocol,
16
16
  );
17
17
  if (options.hostname && typeof options.hostname !== 'string')
@@ -8,11 +8,12 @@ describe('createMongodbUrl', function () {
8
8
  expect(value).to.be.eq('mongodb://127.0.0.1:27017/database');
9
9
  });
10
10
 
11
- it('throws an error when the first argument is a non-object value', function () {
11
+ it('requires the first argument to be an object', function () {
12
12
  const throwable = v => () => createMongodbUrl(v);
13
13
  const error = v =>
14
14
  format(
15
- 'The first argument of "createMongodbUrl" must be an Object, but %s given.',
15
+ 'The first argument of "createMongodbUrl" must be ' +
16
+ 'an Object, but %s given.',
16
17
  v,
17
18
  );
18
19
  expect(throwable('')).to.throw(error('""'));
@@ -26,4 +27,257 @@ describe('createMongodbUrl', function () {
26
27
  throwable(undefined)();
27
28
  throwable({})();
28
29
  });
30
+
31
+ it('requires the "protocol" option to be a string', function () {
32
+ const throwable = v => () => createMongodbUrl({protocol: v});
33
+ const error = v =>
34
+ format(
35
+ 'MongoDB option "protocol" must be ' + 'a String, but %s given.',
36
+ v,
37
+ );
38
+ expect(throwable(10)).to.throw(error('10'));
39
+ expect(throwable(true)).to.throw(error('true'));
40
+ expect(throwable([])).to.throw(error('Array'));
41
+ expect(throwable({})).to.throw(error('Object'));
42
+ throwable('mongodb')();
43
+ throwable('')();
44
+ throwable(0)();
45
+ throwable(false)();
46
+ throwable(undefined)();
47
+ throwable(null)();
48
+ });
49
+
50
+ it('requires the "hostname" option to be a string', function () {
51
+ const throwable = v => () => createMongodbUrl({hostname: v});
52
+ const error = v =>
53
+ format(
54
+ 'MongoDB option "hostname" must be ' + 'a String, but %s given.',
55
+ v,
56
+ );
57
+ expect(throwable(10)).to.throw(error('10'));
58
+ expect(throwable(true)).to.throw(error('true'));
59
+ expect(throwable([])).to.throw(error('Array'));
60
+ expect(throwable({})).to.throw(error('Object'));
61
+ throwable('127.0.0.1')();
62
+ throwable('')();
63
+ throwable(0)();
64
+ throwable(false)();
65
+ throwable(undefined)();
66
+ throwable(null)();
67
+ });
68
+
69
+ it('requires the "host" option to be a string', function () {
70
+ const throwable = v => () => createMongodbUrl({host: v});
71
+ const error = v =>
72
+ format('MongoDB option "host" must be ' + 'a String, but %s given.', v);
73
+ expect(throwable(10)).to.throw(error('10'));
74
+ expect(throwable(true)).to.throw(error('true'));
75
+ expect(throwable([])).to.throw(error('Array'));
76
+ expect(throwable({})).to.throw(error('Object'));
77
+ throwable('127.0.0.1')();
78
+ throwable('')();
79
+ throwable(0)();
80
+ throwable(false)();
81
+ throwable(undefined)();
82
+ throwable(null)();
83
+ });
84
+
85
+ it('requires the "port" option to be a number or a string', function () {
86
+ const throwable = v => () => createMongodbUrl({port: v});
87
+ const error = v =>
88
+ format(
89
+ 'MongoDB option "port" must be a Number ' +
90
+ 'or a String, but %s given.',
91
+ v,
92
+ );
93
+ expect(throwable(true)).to.throw(error('true'));
94
+ expect(throwable([])).to.throw(error('Array'));
95
+ expect(throwable({})).to.throw(error('Object'));
96
+ throwable('127.0.0.1')();
97
+ throwable('')();
98
+ throwable(10)();
99
+ throwable(0)();
100
+ throwable(false)();
101
+ throwable(undefined)();
102
+ throwable(null)();
103
+ });
104
+
105
+ it('requires the "database" option to be a string', function () {
106
+ const throwable = v => () => createMongodbUrl({database: v});
107
+ const error = v =>
108
+ format(
109
+ 'MongoDB option "database" must be ' + 'a String, but %s given.',
110
+ v,
111
+ );
112
+ expect(throwable(10)).to.throw(error('10'));
113
+ expect(throwable(true)).to.throw(error('true'));
114
+ expect(throwable([])).to.throw(error('Array'));
115
+ expect(throwable({})).to.throw(error('Object'));
116
+ throwable('database')();
117
+ throwable('')();
118
+ throwable(0)();
119
+ throwable(false)();
120
+ throwable(undefined)();
121
+ throwable(null)();
122
+ });
123
+
124
+ it('requires the "db" option to be a string', function () {
125
+ const throwable = v => () => createMongodbUrl({db: v});
126
+ const error = v =>
127
+ format('MongoDB option "db" must be ' + 'a String, but %s given.', v);
128
+ expect(throwable(10)).to.throw(error('10'));
129
+ expect(throwable(true)).to.throw(error('true'));
130
+ expect(throwable([])).to.throw(error('Array'));
131
+ expect(throwable({})).to.throw(error('Object'));
132
+ throwable('database')();
133
+ throwable('')();
134
+ throwable(0)();
135
+ throwable(false)();
136
+ throwable(undefined)();
137
+ throwable(null)();
138
+ });
139
+
140
+ it('requires the "username" option to be a string', function () {
141
+ const throwable = v => () => createMongodbUrl({username: v});
142
+ const error = v =>
143
+ format(
144
+ 'MongoDB option "username" must be ' + 'a String, but %s given.',
145
+ v,
146
+ );
147
+ expect(throwable(10)).to.throw(error('10'));
148
+ expect(throwable(true)).to.throw(error('true'));
149
+ expect(throwable([])).to.throw(error('Array'));
150
+ expect(throwable({})).to.throw(error('Object'));
151
+ throwable('username')();
152
+ throwable('')();
153
+ throwable(0)();
154
+ throwable(false)();
155
+ throwable(undefined)();
156
+ throwable(null)();
157
+ });
158
+
159
+ it('requires the "password" option to be a string or a number', function () {
160
+ const throwable = v => () => createMongodbUrl({password: v});
161
+ const error = v =>
162
+ format(
163
+ 'MongoDB option "password" must be a String ' +
164
+ 'or a Number, but %s given.',
165
+ v,
166
+ );
167
+ expect(throwable(true)).to.throw(error('true'));
168
+ expect(throwable([])).to.throw(error('Array'));
169
+ expect(throwable({})).to.throw(error('Object'));
170
+ throwable('password')();
171
+ throwable('')();
172
+ throwable(10)();
173
+ throwable(0)();
174
+ throwable(false)();
175
+ throwable(undefined)();
176
+ throwable(null)();
177
+ });
178
+
179
+ it('requires the "pass" option to be a string or a number', function () {
180
+ const throwable = v => () => createMongodbUrl({pass: v});
181
+ const error = v =>
182
+ format(
183
+ 'MongoDB option "pass" must be a String ' +
184
+ 'or a Number, but %s given.',
185
+ v,
186
+ );
187
+ expect(throwable(true)).to.throw(error('true'));
188
+ expect(throwable([])).to.throw(error('Array'));
189
+ expect(throwable({})).to.throw(error('Object'));
190
+ throwable('pass')();
191
+ throwable('')();
192
+ throwable(10)();
193
+ throwable(0)();
194
+ throwable(false)();
195
+ throwable(undefined)();
196
+ throwable(null)();
197
+ });
198
+
199
+ it('sets the given "protocol" option', function () {
200
+ const res = createMongodbUrl({protocol: 'value'});
201
+ expect(res).to.be.eq('value://127.0.0.1:27017/database');
202
+ });
203
+
204
+ it('sets the given "hostname" option', function () {
205
+ const res = createMongodbUrl({hostname: 'value'});
206
+ expect(res).to.be.eq('mongodb://value:27017/database');
207
+ });
208
+
209
+ it('sets the given "host" option', function () {
210
+ const res = createMongodbUrl({host: 'value'});
211
+ expect(res).to.be.eq('mongodb://value:27017/database');
212
+ });
213
+
214
+ it('sets the given "port" option as a number', function () {
215
+ const res = createMongodbUrl({port: 8080});
216
+ expect(res).to.be.eq('mongodb://127.0.0.1:8080/database');
217
+ });
218
+
219
+ it('sets the given "port" option as a string', function () {
220
+ const res = createMongodbUrl({port: '8080'});
221
+ expect(res).to.be.eq('mongodb://127.0.0.1:8080/database');
222
+ });
223
+
224
+ it('sets the given "database" option', function () {
225
+ const res = createMongodbUrl({database: 'value'});
226
+ expect(res).to.be.eq('mongodb://127.0.0.1:27017/value');
227
+ });
228
+
229
+ it('sets the given "db" option', function () {
230
+ const res = createMongodbUrl({db: 'value'});
231
+ expect(res).to.be.eq('mongodb://127.0.0.1:27017/value');
232
+ });
233
+
234
+ it('does not use the provided "username" option without a password', function () {
235
+ const res = createMongodbUrl({username: 'value'});
236
+ expect(res).to.be.eq('mongodb://127.0.0.1:27017/database');
237
+ });
238
+
239
+ it('does not use the provided "user" option without a password', function () {
240
+ const res = createMongodbUrl({username: 'value'});
241
+ expect(res).to.be.eq('mongodb://127.0.0.1:27017/database');
242
+ });
243
+
244
+ it('does not use the provided "password" option without a username', function () {
245
+ const res = createMongodbUrl({password: 'value'});
246
+ expect(res).to.be.eq('mongodb://127.0.0.1:27017/database');
247
+ });
248
+
249
+ it('does not use the provided "pass" option without a username', function () {
250
+ const res = createMongodbUrl({pass: 'value'});
251
+ expect(res).to.be.eq('mongodb://127.0.0.1:27017/database');
252
+ });
253
+
254
+ it('sets the given "username" and "password" option', function () {
255
+ const res = createMongodbUrl({username: 'usr', password: 'pwd'});
256
+ expect(res).to.be.eq('mongodb://usr:pwd@127.0.0.1:27017/database');
257
+ });
258
+
259
+ it('sets the given "username" and "pass" option', function () {
260
+ const res = createMongodbUrl({username: 'usr', pass: 'pwd'});
261
+ expect(res).to.be.eq('mongodb://usr:pwd@127.0.0.1:27017/database');
262
+ });
263
+
264
+ it('sets the given "user" and "password" option', function () {
265
+ const res = createMongodbUrl({user: 'usr', password: 'pwd'});
266
+ expect(res).to.be.eq('mongodb://usr:pwd@127.0.0.1:27017/database');
267
+ });
268
+
269
+ it('sets the given "user" and "pass" option', function () {
270
+ const res = createMongodbUrl({user: 'usr', pass: 'pwd'});
271
+ expect(res).to.be.eq('mongodb://usr:pwd@127.0.0.1:27017/database');
272
+ });
273
+
274
+ it('does not use the default "port" option for "mongodb+srv" protocol', function () {
275
+ const res = createMongodbUrl({protocol: 'mongodb+srv'});
276
+ expect(res).to.be.eq('mongodb+srv://127.0.0.1/database');
277
+ });
278
+
279
+ it('does not use the provided "port" option for "mongodb+srv" protocol', function () {
280
+ const res = createMongodbUrl({protocol: 'mongodb+srv', port: 8080});
281
+ expect(res).to.be.eq('mongodb+srv://127.0.0.1/database');
282
+ });
29
283
  });
@@ -1,3 +1,4 @@
1
+ export * from './wait-async.js';
1
2
  export * from './is-object-id.js';
2
3
  export * from './create-mongodb-url.js';
3
4
  export * from './transform-values-deep.js';
@@ -0,0 +1,31 @@
1
+ import {expect} from 'chai';
2
+ import {isIsoDate} from './is-iso-date.js';
3
+
4
+ describe('isIsoDate', function () {
5
+ it('returns false for an empty value', function () {
6
+ expect(isIsoDate('')).to.be.false;
7
+ expect(isIsoDate(0)).to.be.false;
8
+ expect(isIsoDate(false)).to.be.false;
9
+ expect(isIsoDate(undefined)).to.be.false;
10
+ expect(isIsoDate(null)).to.be.false;
11
+ });
12
+
13
+ it('returns false for invalid values', function () {
14
+ expect(isIsoDate(10)).to.be.false;
15
+ expect(isIsoDate([])).to.be.false;
16
+ expect(isIsoDate({})).to.be.false;
17
+ expect(isIsoDate(new Map())).to.be.false;
18
+ expect(isIsoDate(NaN)).to.be.false;
19
+ expect(isIsoDate(Infinity)).to.be.false;
20
+ });
21
+
22
+ it('returns true for the Date instance', function () {
23
+ expect(isIsoDate(new Date())).to.be.true;
24
+ });
25
+
26
+ it('validates ISO string', function () {
27
+ expect(isIsoDate('2011-10-05T14:48:00.000Z')).to.be.true;
28
+ expect(isIsoDate('2018-11-10T11:22:33+00:00')).to.be.false;
29
+ expect(isIsoDate('2011-10-05T14:99:00.000Z')).to.be.false;
30
+ });
31
+ });
@@ -1,9 +1,14 @@
1
1
  import {expect} from 'chai';
2
- import {isObjectId} from './is-object-id.js';
3
2
  import {ObjectId} from 'mongodb';
3
+ import {isObjectId} from './is-object-id.js';
4
4
 
5
5
  describe('isObjectId', function () {
6
6
  it('returns true for a valid ObjectId string or an instance', function () {
7
+ expect(isObjectId(new ObjectId())).to.be.true;
8
+ expect(isObjectId(String(new ObjectId()))).to.be.true;
9
+ });
10
+
11
+ it('returns false for invalid values', function () {
7
12
  expect(isObjectId('')).to.be.false;
8
13
  expect(isObjectId('123')).to.be.false;
9
14
  expect(isObjectId(0)).to.be.false;
@@ -17,8 +22,5 @@ describe('isObjectId', function () {
17
22
  expect(isObjectId(new Date())).to.be.false;
18
23
  expect(isObjectId(null)).to.be.false;
19
24
  expect(isObjectId(undefined)).to.be.false;
20
- //
21
- expect(isObjectId(new ObjectId())).to.be.true;
22
- expect(isObjectId(String(new ObjectId()))).to.be.true;
23
25
  });
24
26
  });
@@ -11,7 +11,7 @@ export function transformValuesDeep(value, transformer) {
11
11
  if (!transformer || typeof transformer !== 'function')
12
12
  throw new InvalidArgumentError(
13
13
  'The second argument of "transformValuesDeep" ' +
14
- 'must be a Function, but %s given.',
14
+ 'must be a Function, but %v given.',
15
15
  transformer,
16
16
  );
17
17
  if (Array.isArray(value)) {
@@ -1,8 +1,9 @@
1
1
  import {expect} from 'chai';
2
+ import {format} from '@e22m4u/js-format';
2
3
  import {transformValuesDeep} from './transform-values-deep.js';
3
4
 
4
5
  describe('transformValuesDeep', function () {
5
- it('transforms property values of an object', function () {
6
+ it('transforms property values of the given object', function () {
6
7
  const object = {
7
8
  foo: 1,
8
9
  bar: {
@@ -24,13 +25,13 @@ describe('transformValuesDeep', function () {
24
25
  });
25
26
  });
26
27
 
27
- it('transforms elements of an array', function () {
28
+ it('transforms elements of the given array', function () {
28
29
  const object = [1, 2, 3, [4, 5, 6, [7, 8, 9]]];
29
30
  const result = transformValuesDeep(object, String);
30
31
  expect(result).to.be.eql(['1', '2', '3', ['4', '5', '6', ['7', '8', '9']]]);
31
32
  });
32
33
 
33
- it('transforms non-pure objects', function () {
34
+ it('transforms the Date instance', function () {
34
35
  const date = new Date();
35
36
  const str = String(date);
36
37
  const result1 = transformValuesDeep(date, String);
@@ -44,4 +45,26 @@ describe('transformValuesDeep', function () {
44
45
  expect(result4).to.be.eql({date: str});
45
46
  expect(result5).to.be.eql({foo: {date: str}});
46
47
  });
48
+
49
+ it('requires the second argument to be a function', function () {
50
+ const throwable = v => () => transformValuesDeep('val', v);
51
+ const error = v =>
52
+ format(
53
+ 'The second argument of "transformValuesDeep" ' +
54
+ 'must be a Function, but %s given.',
55
+ v,
56
+ );
57
+ expect(throwable('str')).to.throw(error('"str"'));
58
+ expect(throwable('')).to.throw(error('""'));
59
+ expect(throwable(10)).to.throw(error('10'));
60
+ expect(throwable(0)).to.throw(error('0'));
61
+ expect(throwable(true)).to.throw(error('true'));
62
+ expect(throwable(false)).to.throw(error('false'));
63
+ expect(throwable([])).to.throw(error('Array'));
64
+ expect(throwable({})).to.throw(error('Object'));
65
+ expect(throwable(undefined)).to.throw(error('undefined'));
66
+ expect(throwable(null)).to.throw(error('null'));
67
+ throwable(() => undefined)();
68
+ throwable(function () {})();
69
+ });
47
70
  });
@@ -0,0 +1,21 @@
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
+ }
@@ -0,0 +1,37 @@
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
+ });