feat(search): gate advanced filters
This commit is contained in:
@@ -16,30 +16,31 @@ import (
|
||||
// This is a repository-level mirror of the search.SearchFilter struct to avoid
|
||||
// an import cycle between internal/search and internal/repository.
|
||||
type RepoSearchFilter struct {
|
||||
SearchMode search.SearchMode // ilike or trigram
|
||||
Status []string
|
||||
Priority []string
|
||||
AssigneeID *uint
|
||||
TeamID *uint
|
||||
InboxID *uint
|
||||
Labels []string
|
||||
ContactSource string
|
||||
ContactResolvedScope bool
|
||||
ContactCRMV2 bool
|
||||
MessageType string
|
||||
SenderType string
|
||||
SenderID *uint
|
||||
ContentType string
|
||||
Private *bool
|
||||
EnforceInboxAccess bool
|
||||
AccessibleInboxIDs []uint
|
||||
MessageCreatedAfter *time.Time
|
||||
DateFrom *time.Time
|
||||
DateTo *time.Time
|
||||
SortBy string
|
||||
SortOrder string
|
||||
Page int
|
||||
PerPage int
|
||||
SearchMode search.SearchMode // ilike or trigram
|
||||
Status []string
|
||||
Priority []string
|
||||
AssigneeID *uint
|
||||
TeamID *uint
|
||||
InboxID *uint
|
||||
Labels []string
|
||||
ContactSource string
|
||||
ContactResolvedScope bool
|
||||
ContactCRMV2 bool
|
||||
AdvancedSearchEnabled bool
|
||||
MessageType string
|
||||
SenderType string
|
||||
SenderID *uint
|
||||
ContentType string
|
||||
Private *bool
|
||||
EnforceInboxAccess bool
|
||||
AccessibleInboxIDs []uint
|
||||
MessageCreatedAfter *time.Time
|
||||
DateFrom *time.Time
|
||||
DateTo *time.Time
|
||||
SortBy string
|
||||
SortOrder string
|
||||
Page int
|
||||
PerPage int
|
||||
|
||||
// Article-specific filters (Knowledge Base)
|
||||
PortalID *uint
|
||||
@@ -91,33 +92,34 @@ func searchFilterToRepo(f *search.SearchFilter) *RepoSearchFilter {
|
||||
mode = search.SearchModeILike
|
||||
}
|
||||
return &RepoSearchFilter{
|
||||
SearchMode: mode,
|
||||
Status: f.Status,
|
||||
Priority: f.Priority,
|
||||
AssigneeID: f.AssigneeID,
|
||||
TeamID: f.TeamID,
|
||||
InboxID: f.InboxID,
|
||||
Labels: f.Labels,
|
||||
ContactSource: f.ContactSource,
|
||||
ContactResolvedScope: f.ContactResolvedScope,
|
||||
ContactCRMV2: f.ContactCRMV2,
|
||||
MessageType: f.MessageType,
|
||||
SenderType: f.SenderType,
|
||||
SenderID: f.SenderID,
|
||||
ContentType: f.ContentType,
|
||||
Private: f.Private,
|
||||
EnforceInboxAccess: f.EnforceInboxAccess,
|
||||
AccessibleInboxIDs: append([]uint(nil), f.AccessibleInboxIDs...),
|
||||
MessageCreatedAfter: f.MessageCreatedAfter,
|
||||
DateFrom: f.DateFrom,
|
||||
DateTo: f.DateTo,
|
||||
SortBy: f.SortBy,
|
||||
SortOrder: f.SortOrder,
|
||||
Page: f.Page,
|
||||
PerPage: f.PerPage,
|
||||
PortalID: f.PortalID,
|
||||
ArticleStatus: f.ArticleStatus,
|
||||
ArticleLocale: f.ArticleLocale,
|
||||
SearchMode: mode,
|
||||
Status: f.Status,
|
||||
Priority: f.Priority,
|
||||
AssigneeID: f.AssigneeID,
|
||||
TeamID: f.TeamID,
|
||||
InboxID: f.InboxID,
|
||||
Labels: f.Labels,
|
||||
ContactSource: f.ContactSource,
|
||||
ContactResolvedScope: f.ContactResolvedScope,
|
||||
ContactCRMV2: f.ContactCRMV2,
|
||||
AdvancedSearchEnabled: f.AdvancedSearchEnabled,
|
||||
MessageType: f.MessageType,
|
||||
SenderType: f.SenderType,
|
||||
SenderID: f.SenderID,
|
||||
ContentType: f.ContentType,
|
||||
Private: f.Private,
|
||||
EnforceInboxAccess: f.EnforceInboxAccess,
|
||||
AccessibleInboxIDs: append([]uint(nil), f.AccessibleInboxIDs...),
|
||||
MessageCreatedAfter: f.MessageCreatedAfter,
|
||||
DateFrom: f.DateFrom,
|
||||
DateTo: f.DateTo,
|
||||
SortBy: f.SortBy,
|
||||
SortOrder: f.SortOrder,
|
||||
Page: f.Page,
|
||||
PerPage: f.PerPage,
|
||||
PortalID: f.PortalID,
|
||||
ArticleStatus: f.ArticleStatus,
|
||||
ArticleLocale: f.ArticleLocale,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,12 +484,18 @@ func applyMessageFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
||||
q = q.Where("message_type = ?", filter.MessageType)
|
||||
}
|
||||
|
||||
// Sender type filter
|
||||
if filter.SenderType != "" {
|
||||
q = q.Where("sender_type = ?", filter.SenderType)
|
||||
}
|
||||
if filter.SenderID != nil {
|
||||
q = q.Where("sender_id = ?", *filter.SenderID)
|
||||
if filter.AdvancedSearchEnabled {
|
||||
// Sender and explicit inbox filters are Chatwoot advanced-search filters.
|
||||
if filter.SenderType != "" {
|
||||
q = q.Where("sender_type = ?", filter.SenderType)
|
||||
}
|
||||
if filter.SenderID != nil {
|
||||
q = q.Where("sender_id = ?", *filter.SenderID)
|
||||
}
|
||||
|
||||
if filter.InboxID != nil && searchInboxIDAllowed(filter, *filter.InboxID) {
|
||||
q = q.Where("inbox_id = ?", *filter.InboxID)
|
||||
}
|
||||
}
|
||||
|
||||
// Content type filter
|
||||
@@ -500,11 +508,6 @@ func applyMessageFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
||||
q = q.Where("private = ?", *filter.Private)
|
||||
}
|
||||
|
||||
// Inbox ID filter (messages also belong to an inbox)
|
||||
if filter.InboxID != nil && searchInboxIDAllowed(filter, *filter.InboxID) {
|
||||
q = q.Where("inbox_id = ?", *filter.InboxID)
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
@@ -582,7 +585,7 @@ func applyArticleFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
||||
|
||||
// applyDateRangeFilter applies a date range filter on created_at.
|
||||
func applyDateRangeFilter(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
||||
if filter == nil {
|
||||
if filter == nil || !filter.AdvancedSearchEnabled {
|
||||
return q
|
||||
}
|
||||
|
||||
|
||||
@@ -267,6 +267,33 @@ func TestSearchRepo_SearchMessages_MessageBaseExcludesOldMessages(t *testing.T)
|
||||
assert.Equal(t, recentMsg.ID, results[0].ID)
|
||||
}
|
||||
|
||||
func TestSearchRepo_SearchMessages_SenderFilterRequiresAdvancedSearch(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewSearchRepo(db)
|
||||
|
||||
account := createTestAccountForSearch(t, db)
|
||||
conv := createTestConversationForSearch(t, db, account.ID, "open", "")
|
||||
contactID := uint(42)
|
||||
userID := uint(77)
|
||||
contactMsg := createTestMessageForSearch(t, db, account.ID, conv.ID, "contact message", "incoming")
|
||||
require.NoError(t, db.Model(contactMsg).Updates(map[string]any{"sender_type": "contact", "sender_id": contactID}).Error)
|
||||
userMsg := createTestMessageForSearch(t, db, account.ID, conv.ID, "user message", "outgoing")
|
||||
require.NoError(t, db.Model(userMsg).Updates(map[string]any{"sender_type": "User", "sender_id": userID}).Error)
|
||||
|
||||
baseFilter := &search.SearchFilter{Page: 1, PerPage: 25, SenderType: "contact", SenderID: &contactID}
|
||||
results, total, err := repo.SearchMessages(context.Background(), account.ID, "", baseFilter)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
assert.Len(t, results, 2)
|
||||
|
||||
advancedFilter := &search.SearchFilter{Page: 1, PerPage: 25, AdvancedSearchEnabled: true, SenderType: "contact", SenderID: &contactID}
|
||||
results, total, err = repo.SearchMessages(context.Background(), account.ID, "", advancedFilter)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
require.Len(t, results, 1)
|
||||
assert.Equal(t, contactMsg.ID, results[0].ID)
|
||||
}
|
||||
|
||||
// ========== SearchConversations DB tests (PG only for ILIKE) ==========
|
||||
|
||||
func TestSearchRepo_SearchConversations_EmptyQuery(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user