@pioneer-platform/mongo-atlas 1.0.0
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/README.md +70 -0
- package/index.js +145 -0
- package/package.json +28 -0
- package/tests/connection.test.js +134 -0
package/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# MongoDB Atlas Connection Manager
|
2
|
+
|
3
|
+
A modern, robust connection manager for MongoDB Atlas specifically designed for the Pioneer platform.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- Proper connection pooling and reuse
|
8
|
+
- Support for MongoDB Atlas connection strings
|
9
|
+
- Automatic reconnection
|
10
|
+
- Connection caching
|
11
|
+
- Monk-like API for backward compatibility
|
12
|
+
- MongoDB native driver for optimal performance
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
```javascript
|
17
|
+
const mongo = require('@pioneer-platform/mongo-atlas');
|
18
|
+
|
19
|
+
// Get a collection from a specific database
|
20
|
+
async function getTickets() {
|
21
|
+
const ticketsCollection = await mongo.get('tickets', 'support');
|
22
|
+
return ticketsCollection.find({ status: 'open' });
|
23
|
+
}
|
24
|
+
|
25
|
+
// Use the MongoDB native collection
|
26
|
+
async function advancedQuery() {
|
27
|
+
const collection = await mongo.get('users', 'admin');
|
28
|
+
return collection.collection.aggregate([
|
29
|
+
{ $match: { active: true } },
|
30
|
+
{ $group: { _id: '$role', count: { $sum: 1 } } }
|
31
|
+
]).toArray();
|
32
|
+
}
|
33
|
+
```
|
34
|
+
|
35
|
+
## Environment Variables
|
36
|
+
|
37
|
+
- `MONGO_CONNECTION`: MongoDB connection string (required)
|
38
|
+
- `MONGO_DEFAULT_DB`: Default database name (optional, defaults to 'pioneer')
|
39
|
+
|
40
|
+
## Installation
|
41
|
+
|
42
|
+
```bash
|
43
|
+
npm install @pioneer-platform/mongo-atlas
|
44
|
+
```
|
45
|
+
|
46
|
+
## Migrating from @pioneer-platform/default-mongo
|
47
|
+
|
48
|
+
### Before:
|
49
|
+
```javascript
|
50
|
+
const connection = require('@pioneer-platform/default-mongo');
|
51
|
+
const ticketsDb = connection.get('tickets');
|
52
|
+
|
53
|
+
// Using ticketsDb
|
54
|
+
const tickets = await ticketsDb.find({ status: 'open' });
|
55
|
+
```
|
56
|
+
|
57
|
+
### After:
|
58
|
+
```javascript
|
59
|
+
const mongo = require('@pioneer-platform/mongo-atlas');
|
60
|
+
|
61
|
+
// Make sure to await the get method as it's now async
|
62
|
+
const ticketsDb = await mongo.get('tickets', 'support');
|
63
|
+
|
64
|
+
// The same methods are available
|
65
|
+
const tickets = await ticketsDb.find({ status: 'open' });
|
66
|
+
```
|
67
|
+
|
68
|
+
## License
|
69
|
+
|
70
|
+
MIT
|
package/index.js
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
/*
|
2
|
+
* MongoDB Atlas Connection Manager
|
3
|
+
* Modern, robust connection handling for MongoDB Atlas
|
4
|
+
*/
|
5
|
+
|
6
|
+
const { MongoClient } = require('mongodb');
|
7
|
+
const debug = require('debug')('mongo-atlas');
|
8
|
+
|
9
|
+
// Connection cache to avoid multiple connections to the same DB
|
10
|
+
const connectionCache = {
|
11
|
+
clients: {},
|
12
|
+
databases: {}
|
13
|
+
};
|
14
|
+
|
15
|
+
// Default options for MongoDB connections
|
16
|
+
const defaultOptions = {
|
17
|
+
useUnifiedTopology: true,
|
18
|
+
useNewUrlParser: true,
|
19
|
+
connectTimeoutMS: 10000,
|
20
|
+
socketTimeoutMS: 45000,
|
21
|
+
serverSelectionTimeoutMS: 15000,
|
22
|
+
maxPoolSize: 20,
|
23
|
+
minPoolSize: 5
|
24
|
+
};
|
25
|
+
|
26
|
+
// Get or create a MongoClient for a connection string
|
27
|
+
async function getOrCreateClient(connectionString, options = {}) {
|
28
|
+
// Use original connection string as the cache key
|
29
|
+
if (!connectionCache.clients[connectionString]) {
|
30
|
+
debug(`Creating new MongoClient for ${connectionString}`);
|
31
|
+
const client = new MongoClient(connectionString, { ...defaultOptions, ...options });
|
32
|
+
await client.connect();
|
33
|
+
connectionCache.clients[connectionString] = client;
|
34
|
+
|
35
|
+
// Set up automatic reconnection
|
36
|
+
client.on('close', () => {
|
37
|
+
debug(`Connection to ${connectionString} closed, will reconnect on next use`);
|
38
|
+
delete connectionCache.clients[connectionString];
|
39
|
+
});
|
40
|
+
}
|
41
|
+
|
42
|
+
return connectionCache.clients[connectionString];
|
43
|
+
}
|
44
|
+
|
45
|
+
// Main connection object
|
46
|
+
const connection = {
|
47
|
+
/**
|
48
|
+
* Get a collection from a specific database
|
49
|
+
* @param {string} collection - Collection name
|
50
|
+
* @param {string} dbName - Database name (defaults to env variable or 'pioneer')
|
51
|
+
* @param {object} options - MongoDB connection options
|
52
|
+
* @returns {object} - MongoDB collection with added helper methods
|
53
|
+
*/
|
54
|
+
async get(collection, dbName, options = {}) {
|
55
|
+
const connectionString = process.env.MONGO_CONNECTION || 'mongodb://localhost:27017/pioneer';
|
56
|
+
const targetDb = dbName || process.env.MONGO_DEFAULT_DB || 'pioneer';
|
57
|
+
|
58
|
+
debug(`Getting collection ${collection} from database ${targetDb}`);
|
59
|
+
|
60
|
+
try {
|
61
|
+
const client = await getOrCreateClient(connectionString, options);
|
62
|
+
const db = client.db(targetDb);
|
63
|
+
const mongoCollection = db.collection(collection);
|
64
|
+
|
65
|
+
// Cache the database connection
|
66
|
+
const cacheKey = `${connectionString}:${targetDb}`;
|
67
|
+
connectionCache.databases[cacheKey] = db;
|
68
|
+
|
69
|
+
// Enhance the collection with useful methods
|
70
|
+
return enhanceCollection(mongoCollection);
|
71
|
+
} catch (error) {
|
72
|
+
debug(`Error getting collection ${collection}: ${error.message}`);
|
73
|
+
throw error;
|
74
|
+
}
|
75
|
+
},
|
76
|
+
|
77
|
+
/**
|
78
|
+
* Get a database instance
|
79
|
+
* @param {string} dbName - Database name
|
80
|
+
* @returns {object} - MongoDB database instance
|
81
|
+
*/
|
82
|
+
async getDb(dbName) {
|
83
|
+
const connectionString = process.env.MONGO_CONNECTION || 'mongodb://localhost:27017/pioneer';
|
84
|
+
const targetDb = dbName || process.env.MONGO_DEFAULT_DB || 'pioneer';
|
85
|
+
|
86
|
+
debug(`Getting database ${targetDb}`);
|
87
|
+
|
88
|
+
try {
|
89
|
+
const client = await getOrCreateClient(connectionString);
|
90
|
+
return client.db(targetDb);
|
91
|
+
} catch (error) {
|
92
|
+
debug(`Error getting database ${targetDb}: ${error.message}`);
|
93
|
+
throw error;
|
94
|
+
}
|
95
|
+
},
|
96
|
+
|
97
|
+
/**
|
98
|
+
* Close all connections
|
99
|
+
*/
|
100
|
+
async close() {
|
101
|
+
debug('Closing all connections');
|
102
|
+
|
103
|
+
const clients = Object.values(connectionCache.clients);
|
104
|
+
for (const client of clients) {
|
105
|
+
await client.close();
|
106
|
+
}
|
107
|
+
|
108
|
+
connectionCache.clients = {};
|
109
|
+
connectionCache.databases = {};
|
110
|
+
}
|
111
|
+
};
|
112
|
+
|
113
|
+
// Add Monk-like methods to a MongoDB collection
|
114
|
+
function enhanceCollection(collection) {
|
115
|
+
return {
|
116
|
+
// Original collection
|
117
|
+
collection,
|
118
|
+
|
119
|
+
// Monk-like methods for backward compatibility
|
120
|
+
find: (query = {}, options = {}) => collection.find(query, options).toArray(),
|
121
|
+
findOne: (query = {}, options = {}) => collection.findOne(query, options),
|
122
|
+
insert: (doc, options = {}) => collection.insertOne(doc, options),
|
123
|
+
update: (query, update, options = {}) => collection.updateOne(query, update, options),
|
124
|
+
findOneAndUpdate: (query, update, options = {}) => collection.findOneAndUpdate(query, update, { returnDocument: 'after', ...options }),
|
125
|
+
remove: (query, options = {}) => collection.deleteOne(query, options),
|
126
|
+
removeMany: (query, options = {}) => collection.deleteMany(query, options),
|
127
|
+
count: (query = {}) => collection.countDocuments(query),
|
128
|
+
distinct: (field, query = {}) => collection.distinct(field, query)
|
129
|
+
};
|
130
|
+
}
|
131
|
+
|
132
|
+
// Handle process termination
|
133
|
+
process.on('SIGINT', async () => {
|
134
|
+
debug('Received SIGINT, closing connections');
|
135
|
+
await connection.close();
|
136
|
+
process.exit(0);
|
137
|
+
});
|
138
|
+
|
139
|
+
process.on('SIGTERM', async () => {
|
140
|
+
debug('Received SIGTERM, closing connections');
|
141
|
+
await connection.close();
|
142
|
+
process.exit(0);
|
143
|
+
});
|
144
|
+
|
145
|
+
module.exports = connection;
|
package/package.json
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
{
|
2
|
+
"name": "@pioneer-platform/mongo-atlas",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "Modern MongoDB Atlas connection manager for Pioneer platform",
|
5
|
+
"main": "index.js",
|
6
|
+
"scripts": {
|
7
|
+
"test": "jest"
|
8
|
+
},
|
9
|
+
"keywords": [
|
10
|
+
"mongodb",
|
11
|
+
"atlas",
|
12
|
+
"database",
|
13
|
+
"connection",
|
14
|
+
"pioneer"
|
15
|
+
],
|
16
|
+
"author": "Pioneer Team",
|
17
|
+
"license": "MIT",
|
18
|
+
"dependencies": {
|
19
|
+
"mongodb": "^4.12.0",
|
20
|
+
"debug": "^4.3.4"
|
21
|
+
},
|
22
|
+
"devDependencies": {
|
23
|
+
"jest": "^29.2.2"
|
24
|
+
},
|
25
|
+
"directories": {
|
26
|
+
"test": "tests"
|
27
|
+
}
|
28
|
+
}
|
@@ -0,0 +1,134 @@
|
|
1
|
+
const mongodb = require('mongodb');
|
2
|
+
const connection = require('../index');
|
3
|
+
|
4
|
+
// Mock MongoDB client
|
5
|
+
jest.mock('mongodb', () => {
|
6
|
+
const mockCollection = {
|
7
|
+
find: jest.fn().mockReturnThis(),
|
8
|
+
toArray: jest.fn().mockResolvedValue([]),
|
9
|
+
findOne: jest.fn().mockResolvedValue({}),
|
10
|
+
insertOne: jest.fn().mockResolvedValue({ insertedId: 'mock-id' }),
|
11
|
+
updateOne: jest.fn().mockResolvedValue({ modifiedCount: 1 }),
|
12
|
+
findOneAndUpdate: jest.fn().mockResolvedValue({ value: {} }),
|
13
|
+
deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }),
|
14
|
+
deleteMany: jest.fn().mockResolvedValue({ deletedCount: 5 }),
|
15
|
+
countDocuments: jest.fn().mockResolvedValue(10),
|
16
|
+
distinct: jest.fn().mockResolvedValue(['value1', 'value2']),
|
17
|
+
aggregate: jest.fn().mockReturnThis(),
|
18
|
+
};
|
19
|
+
|
20
|
+
const mockDb = {
|
21
|
+
collection: jest.fn().mockReturnValue(mockCollection),
|
22
|
+
};
|
23
|
+
|
24
|
+
const mockClient = {
|
25
|
+
connect: jest.fn().mockResolvedValue(this),
|
26
|
+
db: jest.fn().mockReturnValue(mockDb),
|
27
|
+
close: jest.fn().mockResolvedValue(true),
|
28
|
+
on: jest.fn(),
|
29
|
+
};
|
30
|
+
|
31
|
+
return {
|
32
|
+
MongoClient: jest.fn().mockImplementation(() => mockClient),
|
33
|
+
};
|
34
|
+
});
|
35
|
+
|
36
|
+
describe('MongoDB Atlas Connection Manager', () => {
|
37
|
+
beforeEach(() => {
|
38
|
+
// Reset mocks
|
39
|
+
jest.clearAllMocks();
|
40
|
+
|
41
|
+
// Set environment variables for testing
|
42
|
+
process.env.MONGO_CONNECTION = 'mongodb://localhost:27017/test';
|
43
|
+
});
|
44
|
+
|
45
|
+
afterEach(() => {
|
46
|
+
// Clean up
|
47
|
+
delete process.env.MONGO_CONNECTION;
|
48
|
+
delete process.env.MONGO_DEFAULT_DB;
|
49
|
+
});
|
50
|
+
|
51
|
+
describe('get method', () => {
|
52
|
+
it('should get a collection from the specified database', async () => {
|
53
|
+
const collection = await connection.get('tickets', 'support');
|
54
|
+
|
55
|
+
// Verify MongoClient was created with correct connection string
|
56
|
+
expect(mongodb.MongoClient).toHaveBeenCalledWith(
|
57
|
+
'mongodb://localhost:27017/test',
|
58
|
+
expect.objectContaining({
|
59
|
+
connectTimeoutMS: 10000,
|
60
|
+
socketTimeoutMS: 45000,
|
61
|
+
})
|
62
|
+
);
|
63
|
+
|
64
|
+
// Verify db was called with correct name
|
65
|
+
const mockClientInstance = mongodb.MongoClient.mock.results[0].value;
|
66
|
+
expect(mockClientInstance.db).toHaveBeenCalledWith('support');
|
67
|
+
|
68
|
+
// Verify collection was requested
|
69
|
+
const mockDbInstance = mockClientInstance.db.mock.results[0].value;
|
70
|
+
expect(mockDbInstance.collection).toHaveBeenCalledWith('tickets');
|
71
|
+
|
72
|
+
// Verify enhanced methods exist
|
73
|
+
expect(collection.findOne).toBeDefined();
|
74
|
+
expect(collection.find).toBeDefined();
|
75
|
+
expect(collection.insert).toBeDefined();
|
76
|
+
expect(collection.update).toBeDefined();
|
77
|
+
expect(collection.findOneAndUpdate).toBeDefined();
|
78
|
+
expect(collection.remove).toBeDefined();
|
79
|
+
expect(collection.removeMany).toBeDefined();
|
80
|
+
expect(collection.count).toBeDefined();
|
81
|
+
expect(collection.distinct).toBeDefined();
|
82
|
+
});
|
83
|
+
|
84
|
+
it('should use default database when dbName is not specified', async () => {
|
85
|
+
process.env.MONGO_DEFAULT_DB = 'default-db';
|
86
|
+
|
87
|
+
await connection.get('users');
|
88
|
+
|
89
|
+
// Verify db was called with default DB name
|
90
|
+
const mockClientInstance = mongodb.MongoClient.mock.results[0].value;
|
91
|
+
expect(mockClientInstance.db).toHaveBeenCalledWith('default-db');
|
92
|
+
});
|
93
|
+
|
94
|
+
it('should reuse existing client connection', async () => {
|
95
|
+
// Get two collections
|
96
|
+
await connection.get('tickets', 'support');
|
97
|
+
await connection.get('users', 'admin');
|
98
|
+
|
99
|
+
// MongoDB client should only be created once
|
100
|
+
expect(mongodb.MongoClient).toHaveBeenCalledTimes(1);
|
101
|
+
|
102
|
+
// But db should be called twice with different names
|
103
|
+
const mockClientInstance = mongodb.MongoClient.mock.results[0].value;
|
104
|
+
expect(mockClientInstance.db).toHaveBeenCalledTimes(2);
|
105
|
+
expect(mockClientInstance.db).toHaveBeenNthCalledWith(1, 'support');
|
106
|
+
expect(mockClientInstance.db).toHaveBeenNthCalledWith(2, 'admin');
|
107
|
+
});
|
108
|
+
});
|
109
|
+
|
110
|
+
describe('getDb method', () => {
|
111
|
+
it('should get a database instance', async () => {
|
112
|
+
const db = await connection.getDb('custom-db');
|
113
|
+
|
114
|
+
// Verify db was called with correct name
|
115
|
+
const mockClientInstance = mongodb.MongoClient.mock.results[0].value;
|
116
|
+
expect(mockClientInstance.db).toHaveBeenCalledWith('custom-db');
|
117
|
+
expect(db).toBeDefined();
|
118
|
+
});
|
119
|
+
});
|
120
|
+
|
121
|
+
describe('close method', () => {
|
122
|
+
it('should close all connections', async () => {
|
123
|
+
// Create a connection first
|
124
|
+
await connection.get('tickets', 'support');
|
125
|
+
|
126
|
+
// Close connections
|
127
|
+
await connection.close();
|
128
|
+
|
129
|
+
// Verify close was called
|
130
|
+
const mockClientInstance = mongodb.MongoClient.mock.results[0].value;
|
131
|
+
expect(mockClientInstance.close).toHaveBeenCalled();
|
132
|
+
});
|
133
|
+
});
|
134
|
+
});
|