Filters Service
๐ Overviewโ
Service Path: internal/api/v1/filters/Services/filter.js
Provider Path: internal/api/v1/filters/Providers/count.js
The Filters service provides dynamic filtering capabilities for CRM entities (deals and contacts) with real-time count aggregation. Core responsibilities include:
- Filter Management: CRUD operations for custom filters
- Real-Time Counts: Aggregate counts for filtered results
- Visibility Control: User and team-level filter sharing
- Multi-Entity Support: Filters for deals, contacts, people, and businesses
- Default Filters: System-provided and user-created filters
- Permission System: Owner and admin controls for public filters
๐๏ธ Collections Usedโ
๐ Data Flowโ
Filter Creation with Countโ
sequenceDiagram
participant User
participant FilterService
participant FilterModel
participant CountProvider
participant DealsDB
participant ContactsDB
User->>FilterService: Create filter (POST)
Note over User,FilterService: { type, title, filter, isPublic }
FilterService->>FilterService: Check permissions
Note over FilterService: Only owner/admin can create public filters
FilterService->>FilterModel: Save filter
FilterModel-->>FilterService: Saved filter doc
FilterService->>FilterService: Determine entity type
alt Deal Filter
FilterService->>CountProvider: dealCount()
CountProvider->>DealsDB: Aggregate with filter conditions
DealsDB-->>CountProvider: Count result
CountProvider-->>FilterService: { filterID, total }
else Contact Filter
FilterService->>CountProvider: contactCount()
CountProvider->>ContactsDB: Aggregate with filter conditions
ContactsDB-->>CountProvider: Count result
CountProvider-->>FilterService: { filterID, total }
end
FilterService->>FilterService: Add count to filter object
FilterService-->>User: Filter with count
style FilterService fill:#e3f2fd
style CountProvider fill:#fff4e6
Filter List with Batch Countsโ
flowchart TD
A[Get Filters Request] --> B[Fetch Filters from DB]
B --> C{Filter Type?}
C -->|Deals| D[Convert filters to aggregation pipelines]
C -->|Contacts| E[Convert filters to aggregation pipelines]
D --> F[Build visibility options]
E --> G[Build visibility options]
F --> H[Single $facet aggregation]
G --> I[Single $facet aggregation]
H --> J[Extract counts for each filter]
I --> K[Extract counts for each filter]
J --> L[Merge counts with filter objects]
K --> L
L --> M{Check ownership}
M -->|User owns filter| N[Set allow_update = true]
M -->|User doesn't own| O[Set allow_update = false]
N --> P[Return filter list with counts]
O --> P
style H fill:#e8f5e9
style I fill:#e8f5e9
style J fill:#fff4e6
style K fill:#fff4e6
๐ง Business Logic & Functionsโ
FilterService.get({ accountID, userID, type, pipeline, is_owner })โ
Purpose: Retrieve all filters for a specific entity type with real-time counts
Parameters:
accountID(ObjectId) - Account IDuserID(ObjectId) - User IDtype(String) - Filter type: 'deal', 'contacts', 'people', or 'businesses'pipeline(ObjectId) - Pipeline ID (required for deal filters)is_owner(Boolean) - Whether user is account owner
Returns:
{
success: true,
message: 'SUCCESS',
data: [
{
id: String,
title: String,
type: String, // 'deal', 'contacts', 'people', 'businesses'
filter: Object, // Filter conditions
is_public: Boolean,
is_default: Boolean,
user: ObjectId,
count: Number, // Real-time count
total: Number, // Always 0 (legacy)
allow_update: Boolean // True if user owns filter
}
]
}
Business Logic Flow:
-
Fetch Filters
const filterRS = await FilterModel.list(accountID, userID, type); -
Apply Count Provider
let filterData;
if (type === 'deal') {
filterData = await countProvider.dealFilters({
filtersDocs: filterRS,
searchType: type,
pipeline,
account_id: accountID,
uid: userID,
is_owner,
});
} else if (type === 'contacts' || type === 'people' || type === 'businesses') {
filterData = await countProvider.contactFilters({
filtersDocs: filterRS,
searchType: type,
account_id: accountID,
uid: userID,
is_owner,
});
} -
Set Ownership Permissions
return {
success: true,
message: 'SUCCESS',
data: filterData?.map(f => {
if (f?.user?.toString() == userID.toString()) {
f.allow_update = true;
} else {
f.allow_update = false;
}
return f;
}),
};
Key Business Rules:
- Real-Time Counts: Counts recalculated on every request
- Ownership: Users can only update their own filters
- Visibility: Respects user permissions (owner sees all)
- Type Mapping: 'people' โ 'person', 'businesses' โ 'business'
FilterService.post({ accountID, userID, type, title, filter, isPublic, is_owner, role, pipeline })โ
Purpose: Create a new filter with initial count
Parameters:
accountID(ObjectId) - Account IDuserID(ObjectId) - User IDtype(String) - Filter typetitle(String) - Filter namefilter(Object) - Filter conditionsisPublic(Boolean) - Make filter visible to teamis_owner(Boolean) - Whether user is account ownerrole(String) - User rolepipeline(ObjectId) - Pipeline ID (for deal filters)
Returns:
{
success: true,
message: 'SUCCESS',
data: {
id: String,
title: String,
type: String,
filter: Object,
is_public: Boolean,
count: Number,
allow_update: true
}
}
Business Logic Flow:
-
Validate Permissions
if (isPublic && !is_owner && !role == 'admin') {
throw new Error('Can not add filter for team.');
} -
Save Filter
const savedData = await FilterModel.add(accountID, userID, type, filter, title, isPublic); -
Calculate Initial Count
if (type === 'deal') {
const countObj = await countProvider.dealCount({
filterObj: savedData.filter,
filterID: savedData.id,
pipeline,
account_id: accountID,
uid: userID,
is_owner,
});
savedData.count = countObj.total;
} else if (type === 'contacts' || type === 'people' || type === 'businesses') {
const countObj = await countProvider.contactCount({
filterObj: savedData.filter,
filterID: savedData.id,
searchType: type,
account_id: accountID,
uid: userID,
is_owner,
});
savedData.count = countObj.total;
} -
Return with Permissions
savedData.allow_update = true;
return { success: true, message: 'SUCCESS', data: savedData };
Key Business Rules:
- Public Filter Restriction: Only owners and admins can create public (team) filters
- Count Calculation: Immediate count provided on creation
- Ownership: Creator automatically has update permission
FilterService.put({ accountID, userID, filter, title, filterID, isPublic, pipeline, is_owner })โ
Purpose: Update existing filter or duplicate if default filter
Parameters:
accountID(ObjectId) - Account IDuserID(ObjectId) - User IDfilter(Object) - Updated filter conditionstitle(String) - Updated titlefilterID(String) - Filter ID to updateisPublic(Boolean) - Updated visibilitypipeline(ObjectId) - Pipeline ID (for deals)is_owner(Boolean) - Whether user is account owner
Returns:
{
success: true,
message: 'SUCCESS',
data: {
id: String,
title: String,
filter: Object,
count: Number,
allow_update: true
}
}
Business Logic Flow:
-
Find Existing Filter
const filterData = await FilterModel.findOne(filterID, accountID, userID);
if (!filterData) {
throw Error('Not exist!');
} -
Handle Default Filters
let savedData = {};
if (filterData.is_default) {
// Default filters can't be edited, create new one
const type = filter.type;
savedData = await FilterModel.add(accountID, userID, type, filter, title, isPublic);
} else {
// Update existing filter
savedData = await FilterModel.update(filterID, filter, title, isPublic);
} -
Recalculate Count
if (savedData.type === 'deal') {
const countObj = await countProvider.dealCount({
filterObj: savedData.filter,
filterID: savedData.id,
pipeline,
account_id: accountID,
uid: userID,
is_owner,
});
savedData.count = countObj.total;
} else if (
savedData.type === 'contacts' ||
savedData.type === 'people' ||
savedData.type === 'businesses'
) {
const countObj = await countProvider.contactCount({
filterObj: savedData.filter,
filterID: savedData.id,
searchType: savedData.type,
account_id: accountID,
uid: userID,
is_owner,
});
savedData.count = countObj.total;
}
Key Business Rules:
- Default Filter Protection: Default filters are duplicated instead of edited
- Count Recalculation: Count updated after every edit
- Ownership Preserved: User maintains ownership of duplicated default filters
FilterService.delete({ accountID, userID, filterID })โ
Purpose: Delete a user's filter
Parameters:
accountID(ObjectId) - Account IDuserID(ObjectId) - User IDfilterID(String) - Filter ID to delete
Returns:
{
success: true,
message: 'SUCCESS'
}
Business Logic Flow:
-
Validate Filter Exists
const filterData = await FilterModel.findOne(filterID, accountID, userID);
if (!filterData) {
throw Error('Not exist!');
} -
Delete Filter
await FilterModel.deleteById(filterID);
return { success: true, message: 'SUCCESS' };
Key Business Rules:
- Ownership Check: User can only delete their own filters
- Default Filters: Cannot be deleted (protected by model)
๐ Count Provider Functionsโ
countProvider.dealCount({ filterObj, filterID, pipeline, account_id, uid, is_owner })โ
Purpose: Calculate count for a single deal filter
Parameters:
filterObj(Object) - Filter conditionsfilterID(String) - Filter ID for responsepipeline(ObjectId) - Pipeline IDaccount_id(ObjectId) - Account IDuid(ObjectId) - User IDis_owner(Boolean) - Whether user is owner
Returns:
{
filterID: String,
total: Number
}
Business Logic Flow:
-
Convert Filter to MongoDB Query
const filterJsonString = JSON.stringify(filterObj || {});
const filterString = Buffer.from(filterJsonString).toString('base64');
const filterConditions = await filterUtility(filterString, uid); -
Validate Pipeline
let foundPipeline = await Pipeline.findOne({
_id: pipeline,
account_id: account_id,
});
if (!foundPipeline) {
return Promise.resolve({ filterID, total: 0 });
} -
Build Query with Visibility
let options = {
account_id: new mongoose.Types.ObjectId(account_id),
pipeline_id: new mongoose.Types.ObjectId(pipeline),
...(filterConditions ? filterConditions : {}),
};
if (!is_owner) {
options.$and = [
...(options.$and || []),
{
$or: [
{ visibility: 'all' },
{ owner: new mongoose.Types.ObjectId(uid) },
{ followers: new mongoose.Types.ObjectId(uid) },
],
},
];
} -
Count Documents
let count = await Deal.countDocuments(options);
return Promise.resolve({ filterID, total: count });
Key Business Rules:
- Visibility: Non-owners only see their own deals, followed deals, or public deals
- Pipeline Required: Returns 0 if pipeline not found
- Error Handling: Returns 0 on error (graceful degradation)
countProvider.dealFilters({ filtersDocs, searchType, pipeline, account_id, uid, is_owner })โ
Purpose: Calculate counts for multiple deal filters in single aggregation
Parameters:
filtersDocs(Array) - Array of filter documentssearchType(String) - 'deal'pipeline(ObjectId) - Pipeline IDaccount_id(ObjectId) - Account IDuid(ObjectId) - User IDis_owner(Boolean) - Whether user is owner
Returns:
[
{
id: String,
title: String,
type: String,
filter: Object,
count: Number,
total: 0,
},
];
Business Logic Flow:
-
Convert All Filters
let filters = await filtersDocs.reduce((n, c) => {
n.push(
new Promise(async (resolve, reject) => {
try {
let f = c.toJSON();
const filterString = Buffer.from(JSON.stringify(f.filter || {})).toString('base64');
resolve({
id: f.id,
filter: await filterUtility(filterString, uid),
raw: f,
});
} catch (err) {
reject(err);
}
}),
);
return n;
}, []);
filters = await Promise.all(filters); -
Build $facet Pipelines
let pipelines = {};
for (f of filters) {
pipelines[f.id] = [
{
$match: f.filter,
},
{
$group: {
_id: 'total',
total: { $sum: 1 },
},
},
];
} -
Single Aggregation for All Counts
let options = {
account_id: new mongoose.Types.ObjectId(account_id),
pipeline_id: new mongoose.Types.ObjectId(pipeline),
};
if (!is_owner) {
options.$and = [
...(options.$and || []),
{
$or: [
{ visibility: 'all' },
{ owner: new mongoose.Types.ObjectId(uid) },
{ followers: new mongoose.Types.ObjectId(uid) },
],
},
];
}
let count = await Deal.aggregate([
{
$match: options,
},
{
$facet: pipelines,
},
]); -
Merge Counts with Filter Objects
filters = Object.keys(count[0]).map(c => {
return {
...filters.find(f => f.id == c).raw,
count: count[0][c][0]?.total || 0,
total: 0,
};
});
return Promise.resolve(filters);
Key Business Rules:
- Performance: Single aggregation for all filters (not N queries)
- $facet: Each filter gets its own pipeline branch
- Visibility: Applied at aggregation level
- Error Handling: Returns 0 count on filter error
countProvider.contactCount({ filterObj, filterID, searchType, account_id, uid, is_owner })โ
Purpose: Calculate count for a single contact filter
Parameters:
filterObj(Object) - Filter conditionsfilterID(String) - Filter IDsearchType(String) - 'contacts', 'people', or 'businesses'account_id(ObjectId) - Account IDuid(ObjectId) - User IDis_owner(Boolean) - Whether user is owner
Returns:
{
filterID: String,
total: Number
}
Business Logic Flow:
-
Map Search Type to Contact Type
let type;
if (searchType === 'people') {
type = 'person';
} else if (searchType === 'businesses') {
type = 'business';
} -
Build Query Options
const filterJsonString = JSON.stringify(filterObj || {});
const filterString = Buffer.from(filterJsonString).toString('base64');
const filterConditions = await filterUtility(filterString, uid);
let options = {
parent_account: new mongoose.Types.ObjectId(account_id),
type,
...(filterConditions ? filterConditions : {}),
};
if (!is_owner) {
options.$and = [
...(options.$and || []),
{
$or: [
{ visibility: 'all' },
{ owner: new mongoose.Types.ObjectId(uid) },
{ followers: new mongoose.Types.ObjectId(uid) },
],
},
];
} -
Count with Limit
let count = await Contact.aggregate([
{
$match: options,
},
{
$limit: 1000, // Performance optimization
},
{
$group: {
_id: 'total',
total: { $sum: 1 },
},
},
]);
return Promise.resolve({ filterID, total: count[0]?.total || 0 });
Key Business Rules:
- Type Mapping: people โ person, businesses โ business
- 1000 Limit: Caps count at 1000 for performance
- Visibility: Non-owners see only accessible contacts
- Error Handling: Returns 0 on error
countProvider.contactFilters({ filtersDocs, searchType, account_id, uid, is_owner })โ
Purpose: Calculate counts for multiple contact filters in single aggregation
Parameters:
filtersDocs(Array) - Array of filter documentssearchType(String) - 'contacts', 'people', or 'businesses'account_id(ObjectId) - Account IDuid(ObjectId) - User IDis_owner(Boolean) - Whether user is owner
Returns:
[
{
id: String,
title: String,
type: String,
filter: Object,
count: Number,
total: 0,
},
];
Business Logic Flow: Similar to dealFilters but with contact-specific type mapping and no pipeline requirement.
๐ Integration Pointsโ
CRM Moduleโ
Deal Filtering:
// Get filtered deals with saved filter
const filters = await filterService.get({
accountID,
userID,
type: 'deal',
pipeline: pipelineId,
is_owner,
});
// Apply filter to deal query
const dealsQuery = {
...filters[0].filter,
pipeline_id: pipelineId,
};
Contact Filtering:
// Get people filters
const filters = await filterService.get({
accountID,
userID,
type: 'people',
is_owner,
});
Filter Utilityโ
Filter Decoding:
const filterUtility = require('../../utilities/filter');
// Convert base64 filter string to MongoDB query
const filterString = Buffer.from(JSON.stringify(filterObj)).toString('base64');
const mongoQuery = await filterUtility(filterString, uid);
๐งช Edge Cases & Special Handlingโ
Default Filter Protectionโ
Cannot Edit Default Filters:
if (filterData.is_default) {
// Create duplicate instead of editing
savedData = await FilterModel.add(accountID, userID, type, filter, title, isPublic);
}
Public Filter Permissionsโ
Only Owners/Admins Can Create Public Filters:
if (isPublic && !is_owner && !role == 'admin') {
throw new Error('Can not add filter for team.');
}
Missing Pipeline for Dealsโ
Graceful Degradation:
let foundPipeline = await Pipeline.findOne({ _id: pipeline, account_id });
if (!foundPipeline) {
return Promise.resolve({ filterID, total: 0 });
}
Empty Filter Listโ
No Pipelines Built:
if (pipelines && Object.keys(pipelines).length === 0) {
throw new Error(`Filters data not found for type ${type}`);
}
Count Limit for Performanceโ
Contact Count Capped at 1000:
let count = await Contact.aggregate([
{ $match: options },
{ $limit: 1000 }, // Prevent slow queries
{ $group: { _id: 'total', total: { $sum: 1 } } },
]);
โ ๏ธ Important Notesโ
-
Real-Time Counts: Filter counts are recalculated on every request. For high-traffic applications, consider caching.
-
Visibility Rules: Non-owners only see:
- Deals/contacts they own
- Deals/contacts they follow
- Deals/contacts with visibility='all'
-
Performance:
dealFilters()andcontactFilters()use single $facet aggregation for all filters instead of N queries. -
Filter Utility: Filters are base64-encoded JSON that's decoded and converted to MongoDB queries by the filter utility.
-
Type Mapping: Frontend uses 'people'/'businesses', backend uses 'person'/'business'.
-
Default Filters: System-provided filters cannot be edited, only duplicated.
-
Pipeline Requirement: Deal filters require a valid pipeline ID. Contact filters do not.
-
Count Limit: Contact counts capped at 1000 for performance (large contact lists).
-
Ownership Permissions: Users can only update/delete their own filters.
-
Public Filters: Only account owners and admins can create team-wide (public) filters.
-
Error Handling: Count providers return 0 on error (graceful degradation).
-
Aggregation Optimization: Uses $facet to run multiple filter counts in parallel within single query.
๐ Related Documentationโ
- CRM Deals Module - Deal filtering integration
- CRM Contacts Module - Contact filtering integration
- Pipelines - Pipeline management for deal filters