@e22m4u/js-repository-mongodb-adapter 0.0.15 → 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 +1 -1
- package/src/mongodb-adapter.js +100 -25
- package/src/mongodb-adapter.spec.js +143 -2
- package/src/utils/create-mongodb-url.js +1 -1
- package/src/utils/create-mongodb-url.spec.js +256 -2
- package/src/utils/index.js +1 -0
- package/src/utils/is-iso-date.spec.js +31 -0
- package/src/utils/is-object-id.spec.js +6 -4
- package/src/utils/transform-values-deep.js +1 -1
- package/src/utils/transform-values-deep.spec.js +26 -3
- package/src/utils/wait-async.js +21 -0
- package/src/utils/wait-async.spec.js +37 -0
package/package.json
CHANGED
package/src/mongodb-adapter.js
CHANGED
|
@@ -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 || '
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
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('
|
|
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
|
|
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
|
});
|
package/src/utils/index.js
CHANGED
|
@@ -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 %
|
|
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
|
|
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
|
|
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
|
|
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
|
+
});
|