core-services-sdk 1.3.26 → 1.3.27
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": "core-services-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.27",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"http-status": "^2.1.0",
|
|
36
36
|
"mongodb": "^6.18.0",
|
|
37
37
|
"nodemailer": "^7.0.5",
|
|
38
|
+
"p-retry": "^7.0.0",
|
|
38
39
|
"pino": "^9.7.0",
|
|
39
40
|
"ulid": "^3.0.1",
|
|
40
41
|
"uuid": "^11.1.0",
|
package/src/mongodb/connect.js
CHANGED
|
@@ -1,23 +1,56 @@
|
|
|
1
|
+
import pRetry from 'p-retry'
|
|
1
2
|
import { MongoClient, ServerApiVersion } from 'mongodb'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Connects to MongoDB.
|
|
5
|
+
* Connects to MongoDB with retry and timeout support.
|
|
5
6
|
*
|
|
6
7
|
* @param {Object} options
|
|
7
8
|
* @param {string} options.uri - MongoDB connection URI.
|
|
8
9
|
* @param {object} [options.serverApi] - Optional serverApi configuration.
|
|
9
|
-
* @
|
|
10
|
+
* @param {number} [options.timeout=5000] - Timeout in ms for each attempt.
|
|
11
|
+
* @param {number} [options.retries=3] - Number of retry attempts.
|
|
12
|
+
* @returns {Promise<MongoClient>}
|
|
10
13
|
*/
|
|
14
|
+
export const mongoConnect = async ({
|
|
15
|
+
uri,
|
|
16
|
+
serverApi,
|
|
17
|
+
timeout = 5000,
|
|
18
|
+
retries = 3,
|
|
19
|
+
}) => {
|
|
20
|
+
const attemptConnect = async () => {
|
|
21
|
+
const connectPromise = MongoClient.connect(uri, {
|
|
22
|
+
serverApi: {
|
|
23
|
+
version: ServerApiVersion.v1,
|
|
24
|
+
strict: true,
|
|
25
|
+
deprecationErrors: true,
|
|
26
|
+
...serverApi,
|
|
27
|
+
},
|
|
28
|
+
connectTimeoutMS: timeout,
|
|
29
|
+
socketTimeoutMS: timeout,
|
|
30
|
+
})
|
|
11
31
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
// Race connection vs timeout
|
|
33
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
34
|
+
setTimeout(
|
|
35
|
+
() =>
|
|
36
|
+
reject(new Error(`MongoDB connection timed out after ${timeout}ms`)),
|
|
37
|
+
timeout,
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return Promise.race([connectPromise, timeoutPromise])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return pRetry(attemptConnect, {
|
|
45
|
+
retries,
|
|
46
|
+
factor: 2, // exponential backoff multiplier
|
|
47
|
+
minTimeout: 500, // wait 500ms before first retry
|
|
48
|
+
maxTimeout: timeout, // cap backoff at timeout value
|
|
49
|
+
onFailedAttempt: (error) => {
|
|
50
|
+
console.warn(
|
|
51
|
+
`MongoDB connection attempt ${error.attemptNumber} failed. ` +
|
|
52
|
+
`There are ${error.retriesLeft} retries left. Reason: ${error.message}`,
|
|
53
|
+
)
|
|
19
54
|
},
|
|
20
55
|
})
|
|
21
|
-
|
|
22
|
-
return client
|
|
23
56
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
3
|
+
import { mongoConnect } from '../../src/mongodb/connect.js'
|
|
4
|
+
import { startMongo, stopMongo } from '../resources/docker-mongo-test.js'
|
|
5
|
+
|
|
6
|
+
const MONGO_PORT = 29060
|
|
7
|
+
const CONTAINER_NAME = 'mongo-connect-integration-test'
|
|
8
|
+
|
|
9
|
+
describe('mongoConnect - Integration', () => {
|
|
10
|
+
let client
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
// Start a fresh Mongo container
|
|
14
|
+
startMongo(MONGO_PORT, CONTAINER_NAME)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterAll(async () => {
|
|
18
|
+
if (client) {
|
|
19
|
+
await client.close()
|
|
20
|
+
}
|
|
21
|
+
stopMongo(CONTAINER_NAME)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('successfully connects to a running MongoDB container', async () => {
|
|
25
|
+
client = await mongoConnect({
|
|
26
|
+
uri: `mongodb://0.0.0.0:${MONGO_PORT}`,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const db = client.db('integration-test')
|
|
30
|
+
const col = db.collection('docs')
|
|
31
|
+
|
|
32
|
+
await col.insertOne({ name: 'hello' })
|
|
33
|
+
const found = await col.findOne({ name: 'hello' })
|
|
34
|
+
|
|
35
|
+
expect(found).toMatchObject({ name: 'hello' })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('respects custom timeout', async () => {
|
|
39
|
+
// should connect quickly
|
|
40
|
+
client = await mongoConnect({
|
|
41
|
+
uri: `mongodb://0.0.0.0:${MONGO_PORT}`,
|
|
42
|
+
timeout: 2000,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(client).toBeDefined()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('fails to connect to an invalid port with retries', async () => {
|
|
49
|
+
const badPort = 29999 // unused port
|
|
50
|
+
|
|
51
|
+
await expect(
|
|
52
|
+
mongoConnect({
|
|
53
|
+
uri: `mongodb://0.0.0.0:${badPort}`,
|
|
54
|
+
timeout: 1000,
|
|
55
|
+
retries: 2,
|
|
56
|
+
}),
|
|
57
|
+
).rejects.toThrow()
|
|
58
|
+
|
|
59
|
+
// should try initial + retries
|
|
60
|
+
// we don’t assert number of attempts here because it’s handled by p-retry
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -23,7 +23,7 @@ describe('mongoConnect', () => {
|
|
|
23
23
|
MongoClient.connect.mockResolvedValue(fakeClient)
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
it('should call MongoClient.connect with default serverApi options', async () => {
|
|
26
|
+
it('should call MongoClient.connect with default serverApi options and default timeouts', async () => {
|
|
27
27
|
const uri = 'mongodb://localhost:27017'
|
|
28
28
|
await mongoConnect({ uri })
|
|
29
29
|
|
|
@@ -33,6 +33,8 @@ describe('mongoConnect', () => {
|
|
|
33
33
|
strict: true,
|
|
34
34
|
deprecationErrors: true,
|
|
35
35
|
},
|
|
36
|
+
connectTimeoutMS: 5000,
|
|
37
|
+
socketTimeoutMS: 5000,
|
|
36
38
|
})
|
|
37
39
|
})
|
|
38
40
|
|
|
@@ -48,13 +50,53 @@ describe('mongoConnect', () => {
|
|
|
48
50
|
strict: false,
|
|
49
51
|
deprecationErrors: true,
|
|
50
52
|
},
|
|
53
|
+
connectTimeoutMS: 5000,
|
|
54
|
+
socketTimeoutMS: 5000,
|
|
51
55
|
})
|
|
52
56
|
})
|
|
53
57
|
|
|
58
|
+
it('should honor custom timeout value', async () => {
|
|
59
|
+
const uri = 'mongodb://localhost:27017'
|
|
60
|
+
await mongoConnect({ uri, timeout: 2000 })
|
|
61
|
+
|
|
62
|
+
expect(MongoClient.connect).toHaveBeenCalledWith(
|
|
63
|
+
uri,
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
connectTimeoutMS: 2000,
|
|
66
|
+
socketTimeoutMS: 2000,
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
54
71
|
it('should return the connected client', async () => {
|
|
55
72
|
const uri = 'mongodb://localhost:27017'
|
|
56
73
|
const client = await mongoConnect({ uri })
|
|
57
74
|
|
|
58
75
|
expect(client).toBe(fakeClient)
|
|
59
76
|
})
|
|
77
|
+
|
|
78
|
+
it('should retry when connection fails initially', async () => {
|
|
79
|
+
const uri = 'mongodb://localhost:27017'
|
|
80
|
+
|
|
81
|
+
// fail first attempt, succeed second
|
|
82
|
+
MongoClient.connect
|
|
83
|
+
.mockRejectedValueOnce(new Error('temporary error'))
|
|
84
|
+
.mockResolvedValueOnce(fakeClient)
|
|
85
|
+
|
|
86
|
+
const client = await mongoConnect({ uri, retries: 2 })
|
|
87
|
+
|
|
88
|
+
expect(client).toBe(fakeClient)
|
|
89
|
+
expect(MongoClient.connect).toHaveBeenCalledTimes(2)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should throw after exceeding retries', async () => {
|
|
93
|
+
const uri = 'mongodb://localhost:27017'
|
|
94
|
+
MongoClient.connect.mockRejectedValue(new Error('always fails'))
|
|
95
|
+
|
|
96
|
+
await expect(
|
|
97
|
+
mongoConnect({ uri, retries: 2, timeout: 100 }),
|
|
98
|
+
).rejects.toThrow(/always fails/)
|
|
99
|
+
|
|
100
|
+
expect(MongoClient.connect).toHaveBeenCalledTimes(3) // initial + 2 retries
|
|
101
|
+
})
|
|
60
102
|
})
|