Users Trigger - User Account Synchronization
Overview
File: queue-manager/triggers/users.js
Function: syncUserWithAccount()
Purpose: Real-time synchronization of user profile changes to accounts and orders
When user information changes, this trigger ensures that account owner details, contact information, and order metadata are immediately updated to maintain consistency across the platform.
Trigger Configuration
Watched Collection
- Collection:
Users - Events: Insert, Replace, Update
- Watched Fields:
nameemailphoneimage
Match Conditions
const matchConditions = {
$or: [
{ operationType: { $in: ['insert', 'replace'] } },
{
$and: [
{ operationType: 'update' },
{
$or: [
{ 'updateDescription.updatedFields.name': { $exists: true } },
{ 'updateDescription.updatedFields.email': { $exists: true } },
{ 'updateDescription.updatedFields.phone': { $exists: true } },
{ 'updateDescription.updatedFields.image': { $exists: true } },
],
},
],
},
],
};
Data Flow
sequenceDiagram
participant API as Internal API
participant USER as Users Collection
participant TRIGGER as users.js Trigger
participant ACCOUNT as Accounts Collection
participant CONTACT as Contacts Collection
participant ORDER as Orders Collection
participant TASK as Projects Tasks
API->>USER: Update user profile
USER->>TRIGGER: Change stream event
alt User is owner
TRIGGER->>ACCOUNT: Update owner object
TRIGGER->>CONTACT: Update business email
end
alt User is manager
TRIGGER->>ORDER: Update manager metadata
end
alt User is specialist
TRIGGER->>ORDER: Update specialist metadata
end
alt User is in assigned_user_ids
TRIGGER->>ORDER: Update responsible array
TRIGGER->>TASK: Update order_metadata
end
Processing Logic
Step 1: Owner User Updates
If the user is an account owner (is_owner: true), update the account:
if (fullDocument.is_owner) {
const updatePayload = {
owner: {
id: fullDocument._id,
name: fullDocument.name,
email: fullDocument.email,
phone: fullDocument.phone || '',
avatar: fullDocument.image,
},
};
await Account.updateOne({ _id: fullDocument.account }, { $set: updatePayload });
}
Step 2: Business Contact Email Sync
For owner users, sync email to business contact:
const business = await Contacts.findOne({
account: fullDocument.account,
type: 'business',
});
if (business && business.email !== fullDocument.email) {
await Contacts.updateOne({ _id: business._id }, { $set: { email: fullDocument.email } });
}
Step 3: Order Metadata Updates (Update Operations Only)
When a user is updated (not inserted), update order metadata:
if (op == 'update') {
const userMetadata = {
id: fullDocument._id,
name: fullDocument.name,
email: fullDocument.email,
phone: fullDocument.phone || '',
image: fullDocument.image,
};
// Update manager metadata
await Order.updateMany(
{ manager: fullDocument._id },
{ $set: { 'metadata.dashclicks_team.manager': userMetadata } },
);
// Update specialist metadata
await Order.updateMany(
{ specialist: fullDocument._id },
{ $set: { 'metadata.dashclicks_team.specialist': userMetadata } },
);
}
Step 4: Responsible Array Updates
For users in assigned_user_ids, rebuild responsible arrays:
const ordersWithUser = await Order.find(
{ assigned_user_ids: fullDocument._id },
{ _id: 1, assigned_user_ids: 1, 'metadata.responsible': 1 },
).lean();
for (const order of ordersWithUser) {
let responsible = Array.isArray(order.metadata?.responsible) ? order.metadata.responsible : [];
const userIndex = responsible.findIndex(
user => user.id && user.id.toString() === fullDocument._id.toString(),
);
if (userIndex >= 0) {
// Update existing user
responsible[userIndex] = userMetadata;
} else {
// Rebuild entire array from assigned_user_ids
responsible = await User.find(
{ _id: { $in: order.assigned_user_ids || [] } },
{
id: '$_id',
_id: 0,
name: 1,
email: 1,
phone: 1,
image: 1,
},
).lean();
}
// Update order and tasks
await Order.updateOne({ _id: order._id }, { $set: { 'metadata.responsible': responsible } });
await ProjectsTask.updateMany(
{ order_id: order._id },
{ $set: { 'order_metadata.responsible': responsible } },
);
}
Collections Updated
Owner User Updates
- Accounts -
ownerobject (id, name, email, phone, avatar) - Contacts - Business contact
emailfield
Non-Owner User Updates (on update only)
- Orders -
metadata.dashclicks_team.manager(if user is manager) - Orders -
metadata.dashclicks_team.specialist(if user is specialist) - Orders -
metadata.responsiblearray (if user in assigned_user_ids) - Projects Tasks -
order_metadata.responsible(cascaded from orders)
Use Cases
Use Case 1: Owner Profile Update
Scenario: Account owner changes their name and profile picture
Flow:
- User updates name from "John Smith" to "John R. Smith"
- User uploads new profile picture
- Trigger fires immediately
- Account
ownerobject updated with new name and avatar - Owner information reflects across dashboard immediately
Use Case 2: Team Member Email Change
Scenario: Specialist changes their email address
Flow:
- User updates email from "specialist@old.com" to "specialist@new.com"
- Trigger fires (update operation)
- All orders where user is specialist get updated metadata
- Order metadata now shows new email
- Communications use new email address
Use Case 3: Manager Reassignment
Scenario: User assigned as manager to multiple orders updates their phone
Flow:
- User updates phone number
- Trigger fires for all orders where user is manager
metadata.dashclicks_team.manager.phoneupdated on all orders- Tasks inherit updated order metadata
- Manager phone visible in all order contexts
Error Handling
Account Update Errors
try {
await Account.updateOne({ _id: fullDocument.account }, { $set: updatePayload });
} catch (err) {
console.error(err);
}
Order Metadata Update Errors
try {
await Order.updateMany(/* ... */);
// ... other updates ...
} catch (err) {
console.error('Error updating order metadata:', err);
}
Performance Considerations
Optimizations
- Conditional Owner Logic: Only process owner updates for
is_owner: trueusers - Insert vs Update: Only update order metadata on
updateoperations (not inserts) - Lean Queries: All queries use
.lean()for performance - Selective Field Updates: Only update specific metadata paths
- Parallel Updates: Uses
Promise.allSettled()for batch updates
Potential Bottlenecks
- High-Volume Orders: Users assigned to many orders trigger multiple updates
- Responsible Array Rebuilds: If user not found in array, entire array rebuilt from DB
- Tasks Updates: Each order triggers additional task updates
Update Volume Analysis
For a user assigned to N orders:
- Manager/specialist role: N order updates
- Assigned user role: N order updates + N task batch updates
- Owner role: 1 account update + 1 contact update
Owner vs Non-Owner Logic
Owner User Flow
if (fullDocument.is_owner) {
// 1. Update account owner object
await Account.updateOne(/* ... */);
// 2. Sync email to business contact
const business = await Contacts.findOne(/* ... */);
if (business.email !== fullDocument.email) {
await Contacts.updateOne(/* ... */);
}
}
Non-Owner User Flow (Update Only)
if (op == 'update') {
// 1. Update manager metadata
await Order.updateMany({ manager: userId } /* ... */);
// 2. Update specialist metadata
await Order.updateMany({ specialist: userId } /* ... */);
// 3. Update responsible arrays
const orders = await Order.find({ assigned_user_ids: userId } /* ... */);
// ... rebuild responsible arrays ...
}
Responsible Array Rebuilding
Efficient Update (User Found in Array)
const userIndex = responsible.findIndex(
user => user.id && user.id.toString() === fullDocument._id.toString(),
);
if (userIndex >= 0) {
responsible[userIndex] = userMetadata;
}
Full Rebuild (User Not Found)
else {
responsible = await User.find(
{ _id: { $in: order.assigned_user_ids || [] } },
{ id: '$_id', _id: 0, name: 1, email: 1, phone: 1, image: 1 }
).lean();
}
Why rebuild? The user should be in the responsible array (they're in assigned_user_ids) but isn't. This indicates data inconsistency, so the entire array is rebuilt from source.
Related Triggers
Downstream Triggers
- order.js: Order metadata updates may trigger additional order processing
- projects.js: Task order_metadata updates propagate through project triggers
Complementary Triggers
- contacts.js: Handles contact-side updates (business contact changes)
- order.js: Handles order assignment changes that affect users
Monitoring
Key Metrics
- Trigger Frequency: How often user profiles are updated
- Order Volume: Number of orders affected per user update
- Rebuild Rate: How often responsible arrays need full rebuilds
- Error Rate: Failed updates to accounts/orders/tasks
Logging
console.error('Error updating order metadata:', err);
console.error(err); // Account update errors
Note: Currently uses console.error() - should migrate to logger.error() for consistency.
Best Practices
1. Owner-Only Processing
Only execute owner-specific logic when needed:
if (fullDocument.is_owner) {
// Owner updates
}
2. Operation-Specific Logic
Only process order metadata on updates, not inserts:
if (op == 'update') {
// Order metadata updates
}
3. Email Comparison Before Update
Avoid unnecessary updates:
if (business.email !== fullDocument.email) {
await Contacts.updateOne(/* ... */);
}
4. Graceful Handling of Missing Data
phone: fullDocument.phone || '',
Improvement Opportunities
1. Consistent Logging
Replace console.error() with logger.error():
logger.error({
initiator: 'queue-manager/triggers/users',
error: err,
message: 'Error updating order metadata',
});
2. Batch Processing
Use bulkWrite() for multiple order updates:
const bulkOps = ordersWithUser.map(order => ({
updateOne: {
filter: { _id: order._id },
update: { $set: { 'metadata.responsible': responsible } },
},
}));
await Order.bulkWrite(bulkOps);
3. Incremental Array Updates
Instead of rebuilding entire responsible array, update only the changed user when possible.
Summary
The users trigger maintains synchronization between user profiles, account owner information, and order metadata. It handles both owner-specific updates (account owner object, business contact email) and team member updates (manager, specialist, responsible arrays), ensuring user information stays consistent across all contexts.
Key Characteristics:
- ⚡ Immediate user profile synchronization
- 👥 Owner vs non-owner logic separation
- 📊 Cascades to orders and tasks
- 🔄 Intelligent array rebuilding
- 🎯 Operation-specific processing (insert vs update)