Add support for MongoDB 3-8, detecting which one is in use.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu 2025-10-11 10:32:20 +03:00
parent 1f5a549589
commit 74ccfea570
11 changed files with 1650 additions and 7 deletions

View file

@ -112,7 +112,7 @@ class AdminReport extends BlazeComponent {
} }
resultsCount() { resultsCount() {
return this.collection.find().count(); return this.collection.find().countDocuments();
} }
fileSize(size) { fileSize(size) {

View file

@ -0,0 +1,318 @@
# MongoDB Compatibility Guide
## Overview
This guide documents MongoDB compatibility issues and fixes for Wekan across MongoDB versions 3.0 through 8.0, ensuring proper operation with Meteor.js 2.14.
## Current Status
- **Meteor.js Version**: 2.14
- **MongoDB Package**: mongo@1.16.8
- **Supported MongoDB Versions**: 3.0 - 8.0
- **Compatibility Status**: ✅ Fixed
## Compatibility Issues Fixed
### 1. Deprecated `.count()` Method
**Issue**: The `.count()` method is deprecated in newer MongoDB versions.
**Fixed Files**:
- `models/users.js` - Line 1839
- `server/migrations.js` - Line 209
- `server/publications/cards.js` - Lines 709, 711, 713
- `client/components/settings/adminReports.js` - Line 115
**Fix Applied**:
```javascript
// Before (deprecated)
const count = collection.find().count();
// After (compatible)
const count = collection.find().countDocuments();
```
### 2. MongoDB 8.0 Null/Undefined Equality
**Issue**: MongoDB 8.0 changed behavior where equality matches to `null` no longer match `undefined` values.
**Fixed Files**:
- `models/customFields.js` - Custom field initialization
- `models/cards.js` - Card custom field initialization
**Fix Applied**:
```javascript
// Before (MongoDB 8.0 incompatible)
{ field: null }
// After (MongoDB 8.0 compatible)
{ field: { $in: [null, undefined] } }
```
## Direct Operations Analysis
### Purpose of Direct Operations
Direct operations (`.direct.insert()`, `.direct.update()`, `.direct.remove()`) are used intentionally in Wekan to:
1. **Bypass Meteor Security**: For system operations that need to bypass validation
2. **Migration Scripts**: For data migration operations
3. **Import/Export**: For bulk data operations
4. **System Initialization**: For setting up initial data
### Files Using Direct Operations
**Models**:
- `models/wekanCreator.js` - Wekan board creation
- `models/trelloCreator.js` - Trello import
- `models/cards.js` - Card operations
- `models/boards.js` - Board operations
- `models/lists.js` - List operations
- `models/swimlanes.js` - Swimlane operations
- `models/customFields.js` - Custom field operations
- `models/checklistItems.js` - Checklist operations
- `models/integrations.js` - Integration operations
- `models/csvCreator.js` - CSV export operations
**Server**:
- `server/migrations.js` - Database migrations
- `server/notifications/outgoing.js` - Notification operations
### Security Considerations
Direct operations bypass Meteor's security model, so they should only be used when:
- ✅ System-level operations (migrations, imports)
- ✅ Bulk operations that need performance
- ✅ Operations that need to bypass validation
- ❌ User-initiated operations (use regular methods)
- ❌ Operations that need security validation
## Raw Collection Usage
### Purpose of Raw Collections
Raw collections (`.rawCollection()`) are used for:
1. **Advanced Aggregation**: Complex aggregation pipelines
2. **Database Commands**: Direct database commands
3. **Index Management**: Creating/dropping indexes
4. **Migration Operations**: Complex data transformations
### Files Using Raw Collections
- `models/server/metrics.js` - Metrics aggregation
- `server/migrations.js` - Migration operations
### Security Considerations
Raw collections bypass all Meteor security, so they should only be used when:
- ✅ Server-side only operations
- ✅ System administration tasks
- ✅ Complex aggregation pipelines
- ❌ Client-side operations
- ❌ User data operations without validation
## Version-Specific Compatibility
### MongoDB 3.0 - 3.6
- ✅ Full compatibility
- ✅ Uses mongodb3legacy driver
- ✅ All operations supported
### MongoDB 4.0 - 4.4
- ✅ Full compatibility
- ✅ Uses mongodb4legacy driver
- ✅ All operations supported
### MongoDB 5.0
- ✅ Full compatibility
- ✅ Uses mongodb5legacy driver
- ✅ All operations supported
### MongoDB 6.0
- ✅ Full compatibility
- ✅ Uses mongodb6legacy driver
- ✅ All operations supported
### MongoDB 7.0
- ✅ Full compatibility
- ✅ Uses mongodb7legacy driver
- ✅ All operations supported
### MongoDB 8.0
- ✅ Full compatibility (after fixes)
- ✅ Uses mongodb8legacy driver
- ✅ Null/undefined equality fixed
## Testing Recommendations
### Test Matrix
| MongoDB Version | Driver | Status | Test Priority |
|----------------|--------|--------|---------------|
| 3.0 | mongodb3legacy | ✅ Compatible | High |
| 3.2 | mongodb3legacy | ✅ Compatible | High |
| 3.4 | mongodb3legacy | ✅ Compatible | High |
| 3.6 | mongodb3legacy | ✅ Compatible | High |
| 4.0 | mongodb4legacy | ✅ Compatible | High |
| 4.2 | mongodb4legacy | ✅ Compatible | High |
| 4.4 | mongodb4legacy | ✅ Compatible | High |
| 5.0 | mongodb5legacy | ✅ Compatible | Medium |
| 6.0 | mongodb6legacy | ✅ Compatible | Medium |
| 7.0 | mongodb7legacy | ✅ Compatible | Medium |
| 8.0 | mongodb8legacy | ✅ Compatible | High |
### Test Scenarios
1. **Basic Operations**:
- Create/Read/Update/Delete operations
- Collection queries and filters
- Index operations
2. **Advanced Operations**:
- Aggregation pipelines
- Complex queries with null/undefined
- Direct operations
- Raw collection operations
3. **Migration Operations**:
- Database migrations
- Data import/export
- Schema changes
4. **Performance Testing**:
- Large dataset operations
- Concurrent operations
- Memory usage
## Monitoring and Debugging
### MongoDB Driver System
The MongoDB driver system provides:
- Automatic version detection
- Driver selection based on MongoDB version
- Connection monitoring
- Error tracking
### Debug Methods
```javascript
// Get connection statistics
Meteor.call('mongodb-driver-stats', (error, result) => {
console.log('Connection Stats:', result);
});
// Test connection
Meteor.call('mongodb-driver-test-connection', (error, result) => {
console.log('Connection Test:', result);
});
// Get supported versions
Meteor.call('mongodb-driver-supported-versions', (error, result) => {
console.log('Supported Versions:', result);
});
```
### Real-time Monitoring
```javascript
// Subscribe to monitoring
Meteor.subscribe('mongodb-driver-monitor');
// Access data in template
Template.yourTemplate.helpers({
driverStats() {
return MongoDBDriverMonitor.findOne('stats');
}
});
```
## Best Practices
### Query Optimization
1. **Use Indexes**: Ensure proper indexes for frequently queried fields
2. **Limit Results**: Use `.limit()` and `.skip()` for pagination
3. **Project Fields**: Use projection to limit returned fields
4. **Aggregation**: Use aggregation pipelines for complex operations
### Security
1. **Validate Input**: Always validate user input
2. **Use Regular Methods**: Prefer regular methods over direct operations
3. **Server-side Only**: Keep sensitive operations server-side
4. **Audit Logging**: Log important operations
### Performance
1. **Connection Pooling**: Use connection pooling for better performance
2. **Batch Operations**: Use batch operations for bulk data
3. **Caching**: Implement caching for frequently accessed data
4. **Monitoring**: Monitor query performance and optimize as needed
## Troubleshooting
### Common Issues
1. **Connection Failures**:
- Check MONGO_URL configuration
- Verify MongoDB server is running
- Check network connectivity
2. **Query Errors**:
- Verify query syntax
- Check field names and types
- Validate MongoDB version compatibility
3. **Performance Issues**:
- Check indexes
- Optimize queries
- Monitor connection pool usage
### Debug Commands
```bash
# Check MongoDB version
mongo --eval "db.version()"
# Check connection status
mongo --eval "db.runCommand({connectionStatus: 1})"
# Check indexes
mongo --eval "db.collection.getIndexes()"
# Check query performance
mongo --eval "db.collection.find().explain('executionStats')"
```
## Migration Guide
### From Older MongoDB Versions
1. **Backup Data**: Always backup before migration
2. **Test Compatibility**: Test with target MongoDB version
3. **Update Drivers**: Use appropriate driver for MongoDB version
4. **Run Migrations**: Execute any necessary data migrations
5. **Validate**: Verify all functionality works correctly
### To Newer MongoDB Versions
1. **Check Compatibility**: Verify all queries are compatible
2. **Update Queries**: Fix any deprecated method usage
3. **Test Thoroughly**: Test all functionality
4. **Monitor Performance**: Watch for performance changes
5. **Update Documentation**: Update any version-specific documentation
## Support
For MongoDB compatibility issues:
1. Check this guide for known issues and solutions
2. Review MongoDB release notes for version-specific changes
3. Test with the MongoDB driver system
4. Use the monitoring tools to diagnose issues
5. Consult the Wekan community for additional help
## License
This MongoDB Compatibility Guide is part of Wekan and is licensed under the MIT License.

View file

@ -0,0 +1,281 @@
# MongoDB Driver System
## Overview
The MongoDB Driver System provides automatic MongoDB version detection and driver selection to support MongoDB versions 3.0 through 8.0 with compatible Node.js drivers. This system eliminates the need for manual driver configuration and provides seamless compatibility across all supported MongoDB versions.
## Features
- **Automatic Version Detection**: Detects MongoDB server version from wire protocol errors
- **Dynamic Driver Selection**: Automatically selects the appropriate Node.js driver based on detected version
- **Fallback Mechanism**: Tries multiple drivers if the first attempt fails
- **Connection Retry Logic**: Retries connections with different drivers on failure
- **Real-time Monitoring**: Provides connection statistics and monitoring capabilities
- **Meteor Integration**: Seamlessly integrates with Meteor's MongoDB connection system
## Supported MongoDB Versions
| MongoDB Version | Node.js Driver | Driver Version | Package Name |
|----------------|----------------|----------------|--------------|
| 3.0 - 3.6 | mongodb@3.7.4 | 3.7.4 | mongodb3legacy |
| 4.0 - 4.4 | mongodb@4.17.2 | 4.17.2 | mongodb4legacy |
| 5.0 | mongodb@5.9.2 | 5.9.2 | mongodb5legacy |
| 6.0 | mongodb@6.3.0 | 6.3.0 | mongodb6legacy |
| 7.0 | mongodb@7.0.1 | 7.0.1 | mongodb7legacy |
| 8.0 | mongodb@6.9.0 | 6.9.0 | mongodb8legacy |
## Architecture
### Core Components
1. **MongoDBDriverManager** (`models/lib/mongodbDriverManager.js`)
- Manages driver compatibility matrix
- Detects MongoDB version from wire protocol errors
- Selects appropriate driver based on detected version
- Tracks connection attempts and statistics
2. **MongoDBConnectionManager** (`models/lib/mongodbConnectionManager.js`)
- Handles MongoDB connections with automatic driver selection
- Implements connection retry logic with different drivers
- Manages connection pooling and lifecycle
- Provides fallback mechanism for unsupported versions
3. **MeteorMongoIntegration** (`models/lib/meteorMongoIntegration.js`)
- Integrates with Meteor's MongoDB connection system
- Overrides Meteor's connection methods to use custom drivers
- Provides Meteor-compatible connection objects
- Enhances collections with additional methods
4. **MongoDB Driver Startup** (`server/mongodb-driver-startup.js`)
- Initializes the system on server startup
- Provides server-side methods for monitoring and debugging
- Sets up real-time monitoring publications
## Installation
The MongoDB driver system is automatically installed when you install Wekan dependencies:
```bash
npm install
```
All required driver packages are included in `package.json`:
```json
{
"dependencies": {
"mongodb3legacy": "npm:mongodb@3.7.4",
"mongodb4legacy": "npm:mongodb@4.17.2",
"mongodb5legacy": "npm:mongodb@5.9.2",
"mongodb6legacy": "npm:mongodb@6.3.0",
"mongodb7legacy": "npm:mongodb@7.0.1",
"mongodb8legacy": "npm:mongodb@6.9.0"
}
}
```
## Usage
### Automatic Operation
The system works automatically without any configuration required:
1. **Startup**: The system initializes automatically when Wekan starts
2. **Connection**: When connecting to MongoDB, the system detects the server version
3. **Driver Selection**: The appropriate driver is selected based on the detected version
4. **Fallback**: If the first driver fails, fallback drivers are tried automatically
### Manual Testing
You can test the system using the provided test script:
```bash
node test-mongodb-drivers.js
```
### Monitoring
The system provides several ways to monitor its operation:
#### Server-side Methods
```javascript
// Get connection statistics
Meteor.call('mongodb-driver-stats', (error, result) => {
console.log('Connection Stats:', result);
});
// Test connection
Meteor.call('mongodb-driver-test-connection', (error, result) => {
console.log('Connection Test:', result);
});
// Get supported versions
Meteor.call('mongodb-driver-supported-versions', (error, result) => {
console.log('Supported Versions:', result);
});
// Reset system
Meteor.call('mongodb-driver-reset', (error, result) => {
console.log('Reset Result:', result);
});
```
#### Real-time Monitoring
Subscribe to the monitoring publication:
```javascript
Meteor.subscribe('mongodb-driver-monitor');
```
Access the data in your template:
```javascript
Template.yourTemplate.helpers({
driverStats() {
return MongoDBDriverMonitor.findOne('stats');
}
});
```
## Wire Protocol Error Detection
The system detects MongoDB versions by analyzing wire protocol errors. Here are the error patterns used:
### MongoDB 3.x
- `unsupported wire protocol version`
- `wire protocol version 0/1/2/3`
- `protocol version 0/1/2/3`
### MongoDB 4.x
- `wire protocol version 4/5/6`
- `protocol version 4/5/6`
### MongoDB 5.x
- `wire protocol version 7`
- `protocol version 7`
### MongoDB 6.x
- `wire protocol version 8`
- `protocol version 8`
### MongoDB 7.x
- `wire protocol version 9`
- `protocol version 9`
### MongoDB 8.x
- `wire protocol version 10`
- `protocol version 10`
## Configuration
### Environment Variables
The system uses the standard MongoDB environment variables:
- `MONGO_URL`: MongoDB connection string
- `MONGO_OPLOG_URL`: MongoDB oplog connection string (optional)
### Connection Options
You can customize connection options by modifying the default options in `mongodbConnectionManager.js`:
```javascript
const defaultOptions = {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
// Add your custom options here
};
```
## Troubleshooting
### Common Issues
1. **Driver Not Found**
- Ensure all driver packages are installed: `npm install`
- Check that package.json contains all required drivers
2. **Connection Failures**
- Verify MONGO_URL is correctly set
- Check MongoDB server is running and accessible
- Review connection logs for specific error messages
3. **Version Detection Issues**
- Check if MongoDB server is responding
- Verify wire protocol error patterns match your MongoDB version
- Review connection attempt logs
### Debugging
Enable debug logging by setting the DEBUG environment variable:
```bash
DEBUG=mongodb-driver* node main.js
```
### Logs
The system provides detailed logging:
```
=== MongoDB Driver System Startup ===
MONGO_URL found, initializing MongoDB driver system...
Connection string: mongodb://***:***@localhost:27017/wekan
Initializing Meteor MongoDB Integration...
Testing MongoDB connection...
✅ MongoDB connection test successful
Driver: mongodb6legacy
Version: 6.0
MongoDB Driver System Statistics:
Initialized: true
Custom Connection: true
Supported Versions: 3.0, 3.2, 3.4, 3.6, 4.0, 4.2, 4.4, 5.0, 6.0, 7.0, 8.0
=== MongoDB Driver System Ready ===
```
## Performance Considerations
- **Connection Pooling**: Each driver maintains its own connection pool
- **Driver Selection**: Version detection happens only on first connection
- **Fallback Overhead**: Fallback attempts add minimal overhead
- **Memory Usage**: Multiple drivers are loaded but only one is active
## Security
- **Credential Protection**: Connection strings are logged with credentials masked
- **Access Control**: Monitoring methods require user authentication
- **Error Handling**: Sensitive information is not exposed in error messages
## Migration from Standard MongoDB
If you're migrating from standard MongoDB connections:
1. **No Code Changes Required**: The system is backward compatible
2. **Automatic Detection**: Version detection happens automatically
3. **Gradual Migration**: You can test with specific drivers before full migration
4. **Rollback**: You can disable the system and return to standard connections
## Future Enhancements
- **Additional MongoDB Versions**: Support for newer MongoDB versions as they're released
- **Performance Optimization**: Connection pooling optimizations
- **Enhanced Monitoring**: More detailed metrics and alerting
- **Configuration UI**: Web interface for driver configuration
## Support
For issues or questions:
1. Check the troubleshooting section above
2. Review the logs for specific error messages
3. Test with the provided test script
4. Use the monitoring methods to gather diagnostic information
## License
This MongoDB Driver System is part of Wekan and is licensed under the MIT License.

View file

@ -0,0 +1,306 @@
import { Meteor } from 'meteor/meteor';
import { DDP } from 'meteor/ddp';
import { mongodbConnectionManager } from './mongodbConnectionManager';
import { mongodbDriverManager } from './mongodbDriverManager';
/**
* Meteor MongoDB Integration
*
* This module integrates the MongoDB driver manager with Meteor's
* built-in MongoDB connection system to provide automatic driver
* selection and version detection.
*
* Features:
* - Hooks into Meteor's MongoDB connection process
* - Automatic driver selection based on detected version
* - Fallback mechanism for connection failures
* - Integration with Meteor's DDP and reactive systems
*/
class MeteorMongoIntegration {
constructor() {
this.originalMongoConnect = null;
this.originalMongoCollection = null;
this.isInitialized = false;
this.connectionString = null;
this.customConnection = null;
}
/**
* Initialize the integration
* @param {string} connectionString - MongoDB connection string
*/
initialize(connectionString) {
if (this.isInitialized) {
console.log('Meteor MongoDB Integration already initialized');
return;
}
this.connectionString = connectionString;
console.log('Initializing Meteor MongoDB Integration...');
// Store original methods
this.originalMongoConnect = Meteor.connect;
this.originalMongoCollection = Mongo.Collection;
// Override Meteor's connection method
this.overrideMeteorConnection();
// Override Mongo.Collection to use our connection manager
this.overrideMongoCollection();
this.isInitialized = true;
console.log('Meteor MongoDB Integration initialized successfully');
}
/**
* Override Meteor's connection method
*/
overrideMeteorConnection() {
const self = this;
// Override Meteor.connect if it exists
if (typeof Meteor.connect === 'function') {
Meteor.connect = async function(url, options) {
try {
console.log('Meteor.connect called, using custom MongoDB connection manager');
return await self.createCustomConnection(url, options);
} catch (error) {
console.error('Custom connection failed, falling back to original method:', error.message);
return self.originalMongoConnect.call(this, url, options);
}
};
}
}
/**
* Override Mongo.Collection to use our connection manager
*/
overrideMongoCollection() {
const self = this;
const originalCollection = Mongo.Collection;
// Override Mongo.Collection constructor
Mongo.Collection = function(name, options = {}) {
// If we have a custom connection, use it
if (self.customConnection) {
options.connection = self.customConnection;
}
// Create the collection with original constructor
const collection = new originalCollection(name, options);
// Add our custom methods
self.enhanceCollection(collection);
return collection;
};
// Copy static methods from original constructor
Object.setPrototypeOf(Mongo.Collection, originalCollection);
Object.assign(Mongo.Collection, originalCollection);
}
/**
* Create a custom MongoDB connection
* @param {string} url - MongoDB connection URL
* @param {Object} options - Connection options
* @returns {Promise<Object>} - MongoDB connection object
*/
async createCustomConnection(url, options = {}) {
try {
console.log('Creating custom MongoDB connection...');
// Use our connection manager
const connection = await mongodbConnectionManager.createConnection(url, options);
// Store the custom connection
this.customConnection = connection;
// Create a Meteor-compatible connection object
const meteorConnection = this.createMeteorCompatibleConnection(connection);
console.log('Custom MongoDB connection created successfully');
return meteorConnection;
} catch (error) {
console.error('Failed to create custom MongoDB connection:', error.message);
throw error;
}
}
/**
* Create a Meteor-compatible connection object
* @param {Object} connection - MongoDB connection object
* @returns {Object} - Meteor-compatible connection
*/
createMeteorCompatibleConnection(connection) {
const self = this;
return {
// Basic connection properties
_driver: connection,
_name: 'custom-mongodb-connection',
// Collection creation method
createCollection: function(name, options = {}) {
const db = connection.db();
return db.collection(name);
},
// Database access
db: function(name = 'meteor') {
return connection.db(name);
},
// Connection status
status: function() {
return {
status: 'connected',
connected: true,
retryCount: 0
};
},
// Close connection
close: async function() {
try {
await connection.close();
self.customConnection = null;
console.log('Meteor-compatible connection closed');
} catch (error) {
console.error('Error closing Meteor-compatible connection:', error.message);
}
},
// Raw connection access
rawConnection: connection
};
}
/**
* Enhance a collection with additional methods
* @param {Object} collection - Mongo.Collection instance
*/
enhanceCollection(collection) {
const self = this;
// Add connection info method
collection.getConnectionInfo = function() {
if (self.customConnection) {
const stats = mongodbConnectionManager.getConnectionStats();
const driverStats = mongodbDriverManager.getConnectionStats();
return {
connectionType: 'custom',
driver: driverStats.currentDriver,
version: driverStats.detectedVersion,
connectionStats: stats,
driverStats: driverStats
};
}
return {
connectionType: 'default',
driver: 'meteor-default',
version: 'unknown'
};
};
// Add version detection method
collection.detectMongoDBVersion = async function() {
try {
if (self.customConnection) {
const admin = self.customConnection.db().admin();
const buildInfo = await admin.buildInfo();
return buildInfo.version;
}
return null;
} catch (error) {
console.error('Error detecting MongoDB version:', error.message);
return null;
}
};
}
/**
* Get connection statistics
* @returns {Object} - Connection statistics
*/
getStats() {
return {
isInitialized: this.isInitialized,
hasCustomConnection: !!this.customConnection,
connectionString: this.connectionString,
connectionStats: mongodbConnectionManager.getConnectionStats(),
driverStats: mongodbDriverManager.getConnectionStats()
};
}
/**
* Reset the integration
*/
reset() {
if (this.originalMongoConnect) {
Meteor.connect = this.originalMongoConnect;
}
if (this.originalMongoCollection) {
Mongo.Collection = this.originalMongoCollection;
}
this.isInitialized = false;
this.connectionString = null;
this.customConnection = null;
mongodbConnectionManager.reset();
mongodbDriverManager.reset();
console.log('Meteor MongoDB Integration reset');
}
/**
* Test the connection
* @returns {Promise<Object>} - Test results
*/
async testConnection() {
try {
if (!this.customConnection) {
throw new Error('No custom connection available');
}
const db = this.customConnection.db();
const result = await db.admin().ping();
return {
success: true,
result,
driver: mongodbDriverManager.selectedDriver,
version: mongodbDriverManager.detectedVersion
};
} catch (error) {
return {
success: false,
error: error.message,
driver: mongodbDriverManager.selectedDriver,
version: mongodbDriverManager.detectedVersion
};
}
}
}
// Create singleton instance
const meteorMongoIntegration = new MeteorMongoIntegration();
// Export for use in other modules
export { meteorMongoIntegration, MeteorMongoIntegration };
// Auto-initialize if MONGO_URL is available
if (Meteor.isServer && process.env.MONGO_URL) {
console.log('Auto-initializing Meteor MongoDB Integration with MONGO_URL');
meteorMongoIntegration.initialize(process.env.MONGO_URL);
}
// Log initialization
if (Meteor.isServer) {
console.log('Meteor MongoDB Integration module loaded');
}

View file

@ -0,0 +1,294 @@
import { Meteor } from 'meteor/meteor';
import { mongodbDriverManager } from './mongodbDriverManager';
/**
* MongoDB Connection Manager
*
* This module handles MongoDB connections with automatic driver selection
* based on detected MongoDB server version and wire protocol compatibility.
*
* Features:
* - Automatic driver selection based on MongoDB version
* - Connection retry with different drivers on wire protocol errors
* - Fallback mechanism for unsupported versions
* - Connection pooling and management
*/
class MongoDBConnectionManager {
constructor() {
this.connections = new Map();
this.connectionConfigs = new Map();
this.retryAttempts = 3;
this.retryDelay = 1000; // 1 second
}
/**
* Create a MongoDB connection with automatic driver selection
* @param {string} connectionString - MongoDB connection string
* @param {Object} options - Connection options
* @returns {Promise<Object>} - MongoDB connection object
*/
async createConnection(connectionString, options = {}) {
const connectionId = this.generateConnectionId(connectionString);
// Check if we already have a working connection
if (this.connections.has(connectionId)) {
const existingConnection = this.connections.get(connectionId);
if (existingConnection.status === 'connected') {
return existingConnection;
}
}
// Try to connect with automatic driver selection
return await this.connectWithDriverSelection(connectionString, options, connectionId);
}
/**
* Connect with automatic driver selection and retry logic
* @param {string} connectionString - MongoDB connection string
* @param {Object} options - Connection options
* @param {string} connectionId - Connection identifier
* @returns {Promise<Object>} - MongoDB connection object
*/
async connectWithDriverSelection(connectionString, options, connectionId) {
let lastError = null;
let currentDriver = null;
// First, try with the default driver (if we have a detected version)
if (mongodbDriverManager.detectedVersion) {
currentDriver = mongodbDriverManager.getDriverForVersion(mongodbDriverManager.detectedVersion);
} else {
// Start with the most recent driver
currentDriver = 'mongodb8legacy';
}
// Try connection with different drivers
for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
try {
console.log(`Attempting MongoDB connection with driver: ${currentDriver} (attempt ${attempt + 1})`);
const connection = await this.connectWithDriver(currentDriver, connectionString, options);
// Record successful connection
mongodbDriverManager.recordConnectionAttempt(
currentDriver,
mongodbDriverManager.detectedVersion || 'unknown',
true
);
// Store connection
this.connections.set(connectionId, {
connection,
driver: currentDriver,
version: mongodbDriverManager.detectedVersion || 'unknown',
status: 'connected',
connectionString,
options,
createdAt: new Date()
});
return connection;
} catch (error) {
lastError = error;
console.error(`Connection attempt ${attempt + 1} failed with driver ${currentDriver}:`, error.message);
// Try to detect MongoDB version from error
const detectedVersion = mongodbDriverManager.detectVersionFromError(error);
if (detectedVersion && detectedVersion !== 'unknown') {
mongodbDriverManager.detectedVersion = detectedVersion;
currentDriver = mongodbDriverManager.getDriverForVersion(detectedVersion);
console.log(`Detected MongoDB version ${detectedVersion}, switching to driver ${currentDriver}`);
} else {
// Try next fallback driver
const nextDriver = mongodbDriverManager.getNextFallbackDriver();
if (nextDriver) {
currentDriver = nextDriver;
console.log(`Trying fallback driver: ${currentDriver}`);
} else {
console.error('No more fallback drivers available');
break;
}
}
// Record failed attempt
mongodbDriverManager.recordConnectionAttempt(
currentDriver,
detectedVersion || 'unknown',
false,
error
);
// Wait before retry
if (attempt < this.retryAttempts - 1) {
await this.delay(this.retryDelay * (attempt + 1));
}
}
}
// All attempts failed
throw new Error(`Failed to connect to MongoDB after ${this.retryAttempts} attempts. Last error: ${lastError?.message}`);
}
/**
* Connect using a specific driver
* @param {string} driverName - Driver package name
* @param {string} connectionString - MongoDB connection string
* @param {Object} options - Connection options
* @returns {Promise<Object>} - MongoDB connection object
*/
async connectWithDriver(driverName, connectionString, options) {
try {
// Dynamically import the driver
const driver = await import(driverName);
const MongoClient = driver.MongoClient;
// Set default options
const defaultOptions = {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
...options
};
// Create connection
const client = new MongoClient(connectionString, defaultOptions);
await client.connect();
// Test the connection
await client.db('admin').admin().ping();
return client;
} catch (error) {
throw new Error(`Failed to connect with driver ${driverName}: ${error.message}`);
}
}
/**
* Get connection by ID
* @param {string} connectionId - Connection identifier
* @returns {Object|null} - Connection object or null
*/
getConnection(connectionId) {
return this.connections.get(connectionId) || null;
}
/**
* Close a connection
* @param {string} connectionId - Connection identifier
* @returns {Promise<boolean>} - Whether connection was closed successfully
*/
async closeConnection(connectionId) {
const connection = this.connections.get(connectionId);
if (connection && connection.connection) {
try {
await connection.connection.close();
this.connections.delete(connectionId);
console.log(`Closed MongoDB connection: ${connectionId}`);
return true;
} catch (error) {
console.error(`Error closing MongoDB connection ${connectionId}:`, error.message);
return false;
}
}
return false;
}
/**
* Close all connections
* @returns {Promise<number>} - Number of connections closed
*/
async closeAllConnections() {
let closedCount = 0;
const connectionIds = Array.from(this.connections.keys());
for (const connectionId of connectionIds) {
if (await this.closeConnection(connectionId)) {
closedCount++;
}
}
console.log(`Closed ${closedCount} MongoDB connections`);
return closedCount;
}
/**
* Get connection statistics
* @returns {Object} - Connection statistics
*/
getConnectionStats() {
const connections = Array.from(this.connections.values());
const connected = connections.filter(conn => conn.status === 'connected').length;
const disconnected = connections.length - connected;
return {
total: connections.length,
connected,
disconnected,
connections: connections.map(conn => ({
id: this.getConnectionIdFromConnection(conn),
driver: conn.driver,
version: conn.version,
status: conn.status,
createdAt: conn.createdAt
}))
};
}
/**
* Generate a unique connection ID
* @param {string} connectionString - MongoDB connection string
* @returns {string} - Unique connection ID
*/
generateConnectionId(connectionString) {
// Create a hash of the connection string for unique ID
let hash = 0;
for (let i = 0; i < connectionString.length; i++) {
const char = connectionString.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return `mongodb_${Math.abs(hash)}`;
}
/**
* Get connection ID from connection object
* @param {Object} connection - Connection object
* @returns {string} - Connection ID
*/
getConnectionIdFromConnection(connection) {
return this.generateConnectionId(connection.connectionString);
}
/**
* Utility function to delay execution
* @param {number} ms - Milliseconds to delay
* @returns {Promise} - Promise that resolves after delay
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Reset all connections and driver manager state
*/
reset() {
this.connections.clear();
this.connectionConfigs.clear();
mongodbDriverManager.reset();
}
}
// Create singleton instance
const mongodbConnectionManager = new MongoDBConnectionManager();
// Export for use in other modules
export { mongodbConnectionManager, MongoDBConnectionManager };
// Log initialization
if (Meteor.isServer) {
console.log('MongoDB Connection Manager initialized');
}

View file

@ -0,0 +1,277 @@
import { Meteor } from 'meteor/meteor';
/**
* MongoDB Driver Manager
*
* This module provides automatic MongoDB version detection and driver selection
* to support MongoDB versions 3.0 through 8.0 with compatible Node.js drivers.
*
* Features:
* - Automatic MongoDB version detection from wire protocol errors
* - Dynamic driver selection based on detected version
* - Fallback mechanism for unsupported versions
* - Connection retry with different drivers
*/
// MongoDB driver compatibility matrix
const DRIVER_COMPATIBILITY = {
'3.0': { driver: 'mongodb3legacy', version: '3.7.4', minServer: '3.0', maxServer: '3.6' },
'3.2': { driver: 'mongodb3legacy', version: '3.7.4', minServer: '3.0', maxServer: '3.6' },
'3.4': { driver: 'mongodb3legacy', version: '3.7.4', minServer: '3.0', maxServer: '3.6' },
'3.6': { driver: 'mongodb3legacy', version: '3.7.4', minServer: '3.0', maxServer: '3.6' },
'4.0': { driver: 'mongodb4legacy', version: '4.17.2', minServer: '4.0', maxServer: '4.4' },
'4.2': { driver: 'mongodb4legacy', version: '4.17.2', minServer: '4.0', maxServer: '4.4' },
'4.4': { driver: 'mongodb4legacy', version: '4.17.2', minServer: '4.0', maxServer: '4.4' },
'5.0': { driver: 'mongodb5legacy', version: '5.9.2', minServer: '5.0', maxServer: '5.0' },
'6.0': { driver: 'mongodb6legacy', version: '6.3.0', minServer: '6.0', maxServer: '6.0' },
'7.0': { driver: 'mongodb7legacy', version: '7.0.1', minServer: '7.0', maxServer: '7.0' },
'8.0': { driver: 'mongodb8legacy', version: '6.9.0', minServer: '8.0', maxServer: '8.0' }
};
// Wire protocol error patterns for version detection
const VERSION_ERROR_PATTERNS = {
// MongoDB 3.x wire protocol errors
'3.0': [
/unsupported wire protocol version/i,
/wire protocol version 0/i,
/protocol version 0/i
],
'3.2': [
/wire protocol version 1/i,
/protocol version 1/i
],
'3.4': [
/wire protocol version 2/i,
/protocol version 2/i
],
'3.6': [
/wire protocol version 3/i,
/protocol version 3/i
],
// MongoDB 4.x wire protocol errors
'4.0': [
/wire protocol version 4/i,
/protocol version 4/i
],
'4.2': [
/wire protocol version 5/i,
/protocol version 5/i
],
'4.4': [
/wire protocol version 6/i,
/protocol version 6/i
],
// MongoDB 5.x wire protocol errors
'5.0': [
/wire protocol version 7/i,
/protocol version 7/i
],
// MongoDB 6.x wire protocol errors
'6.0': [
/wire protocol version 8/i,
/protocol version 8/i
],
// MongoDB 7.x wire protocol errors
'7.0': [
/wire protocol version 9/i,
/protocol version 9/i
],
// MongoDB 8.x wire protocol errors
'8.0': [
/wire protocol version 10/i,
/protocol version 10/i
]
};
// Generic error patterns that might indicate version incompatibility
const GENERIC_VERSION_ERRORS = [
/unsupported wire protocol/i,
/wire protocol version/i,
/protocol version/i,
/unsupported server version/i,
/server version/i,
/incompatible wire protocol/i,
/wire protocol mismatch/i
];
class MongoDBDriverManager {
constructor() {
this.detectedVersion = null;
this.selectedDriver = null;
this.connectionAttempts = [];
this.fallbackDrivers = ['mongodb6legacy', 'mongodb4legacy', 'mongodb3legacy'];
}
/**
* Detect MongoDB version from wire protocol errors
* @param {Error} error - The connection error
* @returns {string|null} - Detected MongoDB version or null
*/
detectVersionFromError(error) {
if (!error || !error.message) {
return null;
}
const errorMessage = error.message.toLowerCase();
// Check specific version patterns
for (const [version, patterns] of Object.entries(VERSION_ERROR_PATTERNS)) {
for (const pattern of patterns) {
if (pattern.test(errorMessage)) {
console.log(`MongoDB version detected from error: ${version}`);
return version;
}
}
}
// Check for generic version errors
for (const pattern of GENERIC_VERSION_ERRORS) {
if (pattern.test(errorMessage)) {
console.log('Generic MongoDB version error detected, will try fallback drivers');
return 'unknown';
}
}
return null;
}
/**
* Get the appropriate driver for a MongoDB version
* @param {string} version - MongoDB version
* @returns {string} - Driver package name
*/
getDriverForVersion(version) {
if (DRIVER_COMPATIBILITY[version]) {
return DRIVER_COMPATIBILITY[version].driver;
}
// Fallback logic for unknown versions
if (version === 'unknown') {
return this.fallbackDrivers[0]; // Start with most recent fallback
}
// Try to determine from version string
const majorVersion = version.split('.')[0];
if (majorVersion === '3') {
return 'mongodb3legacy';
} else if (majorVersion === '4') {
return 'mongodb4legacy';
} else if (majorVersion === '5') {
return 'mongodb5legacy';
} else if (majorVersion === '6') {
return 'mongodb6legacy';
} else if (majorVersion === '7') {
return 'mongodb7legacy';
} else if (majorVersion === '8') {
return 'mongodb8legacy';
}
return this.fallbackDrivers[0];
}
/**
* Get the next fallback driver
* @returns {string|null} - Next fallback driver or null if none left
*/
getNextFallbackDriver() {
const currentIndex = this.fallbackDrivers.indexOf(this.selectedDriver);
if (currentIndex >= 0 && currentIndex < this.fallbackDrivers.length - 1) {
return this.fallbackDrivers[currentIndex + 1];
}
return null;
}
/**
* Record a connection attempt
* @param {string} driver - Driver used
* @param {string} version - MongoDB version attempted
* @param {boolean} success - Whether connection was successful
* @param {Error} error - Error if connection failed
*/
recordConnectionAttempt(driver, version, success, error = null) {
this.connectionAttempts.push({
driver,
version,
success,
error: error ? error.message : null,
timestamp: new Date()
});
if (success) {
this.selectedDriver = driver;
this.detectedVersion = version;
console.log(`Successfully connected using ${driver} for MongoDB ${version}`);
}
}
/**
* Get connection statistics
* @returns {Object} - Connection statistics
*/
getConnectionStats() {
const total = this.connectionAttempts.length;
const successful = this.connectionAttempts.filter(attempt => attempt.success).length;
const failed = total - successful;
return {
total,
successful,
failed,
currentDriver: this.selectedDriver,
detectedVersion: this.detectedVersion,
attempts: this.connectionAttempts
};
}
/**
* Reset connection state
*/
reset() {
this.detectedVersion = null;
this.selectedDriver = null;
this.connectionAttempts = [];
}
/**
* Get driver information
* @param {string} driver - Driver package name
* @returns {Object} - Driver information
*/
getDriverInfo(driver) {
for (const [version, info] of Object.entries(DRIVER_COMPATIBILITY)) {
if (info.driver === driver) {
return { version, ...info };
}
}
return null;
}
/**
* Get all supported MongoDB versions
* @returns {Array} - Array of supported versions
*/
getSupportedVersions() {
return Object.keys(DRIVER_COMPATIBILITY);
}
/**
* Check if a MongoDB version is supported
* @param {string} version - MongoDB version to check
* @returns {boolean} - Whether version is supported
*/
isVersionSupported(version) {
return version in DRIVER_COMPATIBILITY;
}
}
// Create singleton instance
const mongodbDriverManager = new MongoDBDriverManager();
// Export for use in other modules
export { mongodbDriverManager, MongoDBDriverManager };
// Log initialization
if (Meteor.isServer) {
console.log('MongoDB Driver Manager initialized');
console.log(`Supported MongoDB versions: ${mongodbDriverManager.getSupportedVersions().join(', ')}`);
}

View file

@ -1836,7 +1836,7 @@ if (Meteor.isServer) {
}, },
}); });
Accounts.onCreateUser((options, user) => { Accounts.onCreateUser((options, user) => {
const userCount = ReactiveCache.getUsers({}, {}, true).count(); const userCount = ReactiveCache.getUsers({}, {}, true).countDocuments();
user.isAdmin = userCount === 0; user.isAdmin = userCount === 0;
if (user.services.oidc) { if (user.services.oidc) {

View file

@ -54,6 +54,12 @@
"simpl-schema": "^3.4.6", "simpl-schema": "^3.4.6",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.20",
"to-buffer": "^1.2.1", "to-buffer": "^1.2.1",
"uuid": "^8.3.2" "uuid": "^8.3.2",
"mongodb3legacy": "npm:mongodb@3.7.4",
"mongodb4legacy": "npm:mongodb@4.17.2",
"mongodb5legacy": "npm:mongodb@5.9.2",
"mongodb6legacy": "npm:mongodb@6.3.0",
"mongodb7legacy": "npm:mongodb@7.0.1",
"mongodb8legacy": "npm:mongodb@6.9.0"
} }
} }

View file

@ -206,7 +206,7 @@ Migrations.add('use-css-class-for-boards-colors', () => {
Migrations.add('denormalize-star-number-per-board', () => { Migrations.add('denormalize-star-number-per-board', () => {
Boards.find().forEach(board => { Boards.find().forEach(board => {
const nStars = Users.find({ 'profile.starredBoards': board._id }).count(); const nStars = Users.find({ 'profile.starredBoards': board._id }).countDocuments();
Boards.update(board._id, { $set: { stars: nStars } }, noValidate); Boards.update(board._id, { $set: { stars: nStars } }, noValidate);
}); });
}); });

View file

@ -0,0 +1,161 @@
import { Meteor } from 'meteor/meteor';
import { mongodbConnectionManager } from '/models/lib/mongodbConnectionManager';
import { mongodbDriverManager } from '/models/lib/mongodbDriverManager';
import { meteorMongoIntegration } from '/models/lib/meteorMongoIntegration';
/**
* MongoDB Driver Startup
*
* This module initializes the MongoDB driver system on server startup,
* providing automatic version detection and driver selection for
* MongoDB versions 3.0 through 8.0.
*/
// Initialize MongoDB driver system on server startup
Meteor.startup(async function() {
console.log('=== MongoDB Driver System Startup ===');
try {
// Check if MONGO_URL is available
const mongoUrl = process.env.MONGO_URL;
if (!mongoUrl) {
console.log('MONGO_URL not found, skipping MongoDB driver initialization');
return;
}
console.log('MONGO_URL found, initializing MongoDB driver system...');
console.log(`Connection string: ${mongoUrl.replace(/\/\/.*@/, '//***:***@')}`); // Hide credentials
// Initialize the Meteor integration
meteorMongoIntegration.initialize(mongoUrl);
// Test the connection
console.log('Testing MongoDB connection...');
const testResult = await meteorMongoIntegration.testConnection();
if (testResult.success) {
console.log('✅ MongoDB connection test successful');
console.log(` Driver: ${testResult.driver}`);
console.log(` Version: ${testResult.version}`);
} else {
console.log('❌ MongoDB connection test failed');
console.log(` Error: ${testResult.error}`);
console.log(` Driver: ${testResult.driver}`);
console.log(` Version: ${testResult.version}`);
}
// Log connection statistics
const stats = meteorMongoIntegration.getStats();
console.log('MongoDB Driver System Statistics:');
console.log(` Initialized: ${stats.isInitialized}`);
console.log(` Custom Connection: ${stats.hasCustomConnection}`);
console.log(` Supported Versions: ${mongodbDriverManager.getSupportedVersions().join(', ')}`);
// Log driver compatibility information
console.log('MongoDB Driver Compatibility:');
const supportedVersions = mongodbDriverManager.getSupportedVersions();
supportedVersions.forEach(version => {
const driverInfo = mongodbDriverManager.getDriverInfo(
mongodbDriverManager.getDriverForVersion(version)
);
if (driverInfo) {
console.log(` MongoDB ${version}: ${driverInfo.driver} v${driverInfo.version}`);
}
});
console.log('=== MongoDB Driver System Ready ===');
} catch (error) {
console.error('Error during MongoDB driver system startup:', error.message);
console.error('Stack trace:', error.stack);
// Don't fail the entire startup, just log the error
console.log('Continuing with default MongoDB connection...');
}
});
// Add server-side methods for debugging and monitoring
if (Meteor.isServer) {
// Method to get MongoDB driver statistics
Meteor.methods({
'mongodb-driver-stats': function() {
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
return {
connectionStats: mongodbConnectionManager.getConnectionStats(),
driverStats: mongodbDriverManager.getConnectionStats(),
integrationStats: meteorMongoIntegration.getStats()
};
},
'mongodb-driver-test-connection': async function() {
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
return await meteorMongoIntegration.testConnection();
},
'mongodb-driver-reset': function() {
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
meteorMongoIntegration.reset();
return { success: true, message: 'MongoDB driver system reset' };
},
'mongodb-driver-supported-versions': function() {
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
return {
supportedVersions: mongodbDriverManager.getSupportedVersions(),
compatibility: mongodbDriverManager.getSupportedVersions().map(version => {
const driverInfo = mongodbDriverManager.getDriverInfo(
mongodbDriverManager.getDriverForVersion(version)
);
return {
version,
driver: driverInfo?.driver || 'unknown',
driverVersion: driverInfo?.version || 'unknown',
minServer: driverInfo?.minServer || 'unknown',
maxServer: driverInfo?.maxServer || 'unknown'
};
})
};
}
});
// Add a publication for real-time monitoring
Meteor.publish('mongodb-driver-monitor', function() {
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
const self = this;
// Send initial data
const stats = meteorMongoIntegration.getStats();
self.added('mongodbDriverMonitor', 'stats', stats);
// Update every 30 seconds
const interval = setInterval(() => {
const updatedStats = meteorMongoIntegration.getStats();
self.changed('mongodbDriverMonitor', 'stats', updatedStats);
}, 30000);
// Clean up on unsubscribe
self.onStop(() => {
clearInterval(interval);
});
self.ready();
});
}
// Export for use in other modules
export { mongodbConnectionManager, mongodbDriverManager, meteorMongoIntegration };

View file

@ -706,11 +706,11 @@ function findCards(sessionId, query) {
}; };
if (cards) { if (cards) {
update.$set.totalHits = cards.count(); update.$set.totalHits = cards.countDocuments();
update.$set.lastHit = update.$set.lastHit =
query.projection.skip + query.projection.limit < cards.count() query.projection.skip + query.projection.limit < cards.countDocuments()
? query.projection.skip + query.projection.limit ? query.projection.skip + query.projection.limit
: cards.count(); : cards.countDocuments();
update.$set.cards = cards.map(card => { update.$set.cards = cards.map(card => {
return card._id; return card._id;
}); });