Synchronizes data between collections in mongo with mongo replica with a simple configuration file
211
Lightweight service for propagating changes across MongoDB collections using Change Streams with support for nested fields and flexible field mapping.
project/
├── mongo-sync-service/
│ ├── MongoSyncManager.js # Core framework
│ ├── index.js # Entry point
│ ├── package.json
│ ├── Dockerfile
│ └── .dockerignore
├── sync-config.js # Your configuration (external)
└── docker-compose.yml
mkdir mongo-sync-service
cd mongo-sync-service
Create the following files:
MongoSyncManager.js (core framework)index.js (entry point)package.jsonDockerfile.dockerignoreCreate sync-config.js in your project root (not inside mongo-sync-service):
module.exports = function(syncManager) {
// Simple example: propagate user changes to jobs
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: {
'_id': 'USER.USER_ID',
'NAME': 'USER.USER_NAME',
'EMAIL': 'USER.USER_EMAIL'
}
},
target: {
collection: 'JOBS_ENRICHED',
matchField: 'USER.USER_ID'
}
});
};
docker build -t mongo-sync-service ./mongo-sync-service
services:
mongo-sync-service:
build: ./mongo-sync-service
container_name: mongo-sync-service
depends_on:
- mongo
environment:
- MONGO_URI=mongodb://mongo:27017/?replicaSet=rs0
- MONGO_DATABASE=pixpro_readmodel
- CONFIG_FILE=/config/sync-config.js
volumes:
- ./sync-config.js:/config/sync-config.js:ro
restart: unless-stopped
ports:
- "8080:8080"
docker-compose up -d mongo-sync-service
docker-compose logs -f mongo-sync-service
module.exports = function(syncManager) {
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: ['name', 'email'] // Same names in both collections
},
target: {
collection: 'JOBS_ENRICHED',
matchField: 'user_id'
}
});
};
module.exports = function(syncManager) {
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: {
name: 'user_name', // USERS.name -> JOBS.user_name
email: 'user_email' // USERS.email -> JOBS.user_email
}
},
target: {
collection: 'JOBS_ENRICHED',
matchField: 'user_id'
}
});
};
module.exports = function(syncManager) {
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: {
'_id': 'USER.USER_ID', // Flat to nested
'NAME': 'USER.USER_NAME', // Flat to nested
'EMAIL': 'USER.USER_EMAIL' // Flat to nested
}
},
target: {
collection: 'JOBS_ENRICHED',
matchField: 'USER.USER_ID' // Match on nested field
}
});
};
Result in JOBS_ENRICHED:
{
"_id": 2,
"STATUS": "PENDING",
"USER": {
"USER_ID": 2,
"USER_NAME": "John Doe",
"USER_EMAIL": "[email protected]"
},
"_synced_at": "2025-10-24T12:00:00Z"
}
module.exports = function(syncManager) {
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: {
'profile.name': 'user_name', // Nested to flat
'profile.avatar': 'user_avatar', // Nested to flat
'contact.email': 'user_email' // Nested to flat
}
},
target: {
collection: 'JOBS_ENRICHED',
matchField: 'user_id'
}
});
};
module.exports = function(syncManager) {
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: {
'profile.firstName': 'USER.PROFILE.FIRST_NAME',
'profile.lastName': 'USER.PROFILE.LAST_NAME',
'contact.email': 'USER.CONTACT.EMAIL'
}
},
target: {
collection: 'ORDERS_ENRICHED',
matchField: 'USER.ID'
}
});
};
module.exports = function(syncManager) {
syncManager.addRule({
source: {
collection: 'PRODUCTS_TABLE',
matchField: '_id',
fields: {
'_id': 'PRODUCT.ID',
'name': 'PRODUCT.INFO.NAME',
'description': 'PRODUCT.INFO.DESCRIPTION',
'pricing.current': 'PRODUCT.PRICING.CURRENT',
'pricing.original': 'PRODUCT.PRICING.ORIGINAL',
'images.main': 'PRODUCT.IMAGES.MAIN',
'images.thumbnail': 'PRODUCT.IMAGES.THUMB',
'inventory.stock': 'PRODUCT.STOCK.AVAILABLE'
}
},
target: {
collection: 'ORDER_ITEMS_ENRICHED',
matchField: 'PRODUCT.ID'
}
});
};
Result:
{
"PRODUCT": {
"ID": 123,
"INFO": { "NAME": "...", "DESCRIPTION": "..." },
"PRICING": { "CURRENT": 99.99, "ORIGINAL": 149.99 },
"IMAGES": { "MAIN": "...", "THUMB": "..." },
"STOCK": { "AVAILABLE": 50 }
}
}
module.exports = function(syncManager) {
// Users to Jobs
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: { name: 'user_name', email: 'user_email' }
},
target: {
collection: 'JOBS_ENRICHED',
matchField: 'user_id'
}
});
// Users to Orders (different field names)
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: { name: 'customer_name', email: 'customer_email' }
},
target: {
collection: 'ORDERS_ENRICHED',
matchField: 'customer_id'
}
});
};
module.exports = function(syncManager) {
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: { name: 'user_name', email: 'user_email' }
},
target: {
collection: 'JOBS_ENRICHED',
matchField: 'user_id'
},
filter: (change, document) => {
// Only sync verified and active users
return document.verified === true && document.status === 'active';
}
});
};
module.exports = function(syncManager) {
syncManager.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: {
'profile.firstName': 'first_name',
'profile.lastName': 'last_name',
'email': 'email'
}
},
target: {
collection: 'JOBS_ENRICHED',
matchField: 'user_id'
},
transform: (updateDoc, fullDocument) => {
return {
...updateDoc,
// Add computed fields
full_name: `${updateDoc.first_name} ${updateDoc.last_name}`,
email: updateDoc.email?.toLowerCase(),
// Access full document
is_premium: fullDocument.subscription?.tier === 'premium',
display_name: updateDoc.first_name?.toUpperCase()
};
}
});
};
module.exports = function(syncManager) {
syncManager
.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: ['name', 'email']
},
target: { collection: 'JOBS_ENRICHED', matchField: 'user_id' }
})
.addRule({
source: {
collection: 'USERS_TABLE',
matchField: '_id',
fields: ['name']
},
target: { collection: 'ORDERS_ENRICHED', matchField: 'user_id' }
})
.addRule({
source: {
collection: 'PRODUCTS_TABLE',
matchField: '_id',
fields: { name: 'product_name', 'pricing.current': 'price' }
},
target: { collection: 'ORDER_ITEMS', matchField: 'product_id' }
});
};
Main method to add a synchronization rule.
Parameters:
config.source (Object) - Source configuration
collection (string) - Source collection namematchField (string) - Field to match from (supports dot notation, default: '_id')fields (Array|Object) - Fields to extract
['name', 'email'] - same names in target{ name: 'user_name', email: 'user_email' } - field mappingconfig.target (Object) - Target configuration
collection (string) - Target collection namematchField (string) - Field to match to (supports dot notation)config.filter (Function) - Optional filter function (change, document) => booleanconfig.transform (Function) - Optional transform function (updateDoc, fullDocument) => objectBackward compatible simplified method.
Parameters:
sourceCollection (string) - Source collection nametargetCollection (string) - Target collection namesourceMatchField (string) - Field in source to match fromtargetMatchField (string) - Field in target to match tofields (Array) - Fields to propagateHelper method to propagate user changes to multiple collections.
Parameters:
userCollection (string) - User collection nametargets (Array) - Array of target configurations
collection (string) - Target collection namesourceMatchField (string) - Source match field (default: '_id')targetMatchField (string) - Target match field (default: 'user_id')fields (Array|Object) - Fields to propagatecurl http://localhost:8080/health
Response:
{
"status": "healthy",
"totalChangesDetected": 156,
"totalDocumentsUpdated": 342,
"errors": 0,
"uptimeSeconds": 3600
}
curl http://localhost:8080/metrics
curl http://localhost:8080/rules
Response:
{
"rules": [
{
"source": "USERS_TABLE",
"target": "JOBS_ENRICHED",
"matchField": "USER.USER_ID",
"fields": ["_id->USER.USER_ID", "NAME->USER.USER_NAME"]
}
],
"total": 1
}
| Variable | Default | Description |
|---|---|---|
MONGO_URI | mongodb://mongo:27017/?replicaSet=rs0 | MongoDB connection URI (must be replica set) |
MONGO_DATABASE | pixpro_readmodel | Database name |
CONFIG_FILE | /config/sync-config.js | Path to config file |
PORT | 8080 | Health check port |
Simply edit sync-config.js and add a new rule:
module.exports = function(syncManager) {
// Existing rule
syncManager.addRule({
source: { collection: 'USERS_TABLE', matchField: '_id', fields: ['name', 'email'] },
target: { collection: 'JOBS_ENRICHED', matchField: 'user_id' }
});
// New rule
syncManager.addRule({
source: { collection: 'USERS_TABLE', matchField: '_id', fields: ['name', 'email'] },
target: { collection: 'ORDERS_ENRICHED', matchField: 'user_id' }
});
};
Then restart the service:
docker-compose restart mongo-sync-service
Check logs:
docker-compose logs mongo-sync-service
Common issues:
Verify the volume mount:
docker exec mongo-sync-service ls -la /config
Test configuration syntax:
node -c sync-config.js
MongoDB replica set: Change streams require MongoDB to run as a replica set
mongo:
command: --replSet rs0
Verify collection names: Collection names are case-sensitive
docker exec mongo mongosh --eval "db.getMongo().getDBNames()"
Check field names: Use the exact field names from your documents
docker exec mongo mongosh pixpro_readmodel --eval "db.USERS_TABLE.findOne()"
Review logs: Check for warnings about missing fields
docker-compose logs -f mongo-sync-service | grep WARN
Ensure you're using the correct dot notation:
'profile.name' - reads from nested field'USER.NAME' - writes to nested fieldTest the field path:
// In sync-config.js, add logging in transform
transform: (updateDoc, fullDocument) => {
console.log('Update doc:', JSON.stringify(updateDoc));
console.log('Full document:', JSON.stringify(fullDocument));
return updateDoc;
}
filter: (change) => {
const hourAgo = Date.now() - (60 * 60 * 1000);
return change.clusterTime.getHighBits() * 1000 > hourAgo;
}
cd mongo-sync-service
npm install
MONGO_URI=mongodb://localhost:27017/?replicaSet=rs0 \
MONGO_DATABASE=pixpro_readmodel \
CONFIG_FILE=../sync-config.js \
npm run dev
Test your configuration without running the full service:
// test-config.js
const MongoSyncManager = require('./MongoSyncManager');
const config = require('./sync-config.js');
const manager = new MongoSyncManager('mongodb://localhost:27017/?replicaSet=rs0', 'test_db');
config(manager);
console.log('Loaded rules:', manager.rules.size);
manager.rules.forEach((rules, collection) => {
console.log(`\n${collection}:`);
rules.forEach(rule => {
console.log(` -> ${rule.targetCollection}`);
console.log(` Fields:`, rule.fieldMapping);
});
});
Content type
Image
Digest
sha256:0f56e65fa…
Size
47.2 MB
Last updated
11 months ago
docker pull serjunken1990/mongo-sync-service:1.0.0