MM-15422: Adds new parameters for retrieving pages of channels. (#10903)

* MM-15422: Adds new parameters for retrieving pages of channels and searching channels.

* MM-15422: Appends excluded channel names with defaults. Adds separate struct for data-tier option parameter.
Этот коммит содержится в:
Martin Kraft
2019-05-24 11:28:42 -04:00
коммит произвёл GitHub
родитель 124b371312
Коммит e8af4872c6
11 изменённых файлов: 256 добавлений и 91 удалений

Просмотреть файл

@@ -949,17 +949,37 @@ func (s SqlChannelStore) GetChannels(teamId string, userId string, includeDelete
})
}
func (s SqlChannelStore) GetAllChannels(offset int, limit int, includeDeleted bool) store.StoreChannel {
func (s SqlChannelStore) GetAllChannels(offset int, limit int, opts store.ChannelSearchOpts) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
deleteFilter := "AND c.DeleteAt = 0"
if includeDeleted {
deleteFilter = ""
query := s.getQueryBuilder().
Select("c.*, Teams.DisplayName AS TeamDisplayName, Teams.Name AS TeamName, Teams.UpdateAt AS TeamUpdateAt").
From("Channels AS c").
Join("Teams ON Teams.Id = c.TeamId").
Where(sq.Eq{"c.Type": []string{model.CHANNEL_PRIVATE, model.CHANNEL_OPEN}}).
OrderBy("c.DisplayName, Teams.DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset))
if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": int(0)})
}
query := "SELECT c.*, Teams.DisplayName AS TeamDisplayName, Teams.Name AS TeamName, Teams.UpdateAt as TeamUpdateAt FROM Channels AS c JOIN Teams ON Teams.Id = c.TeamId WHERE (c.Type = 'P' OR c.Type = 'O') " + deleteFilter + " ORDER BY c.DisplayName, Teams.DisplayName LIMIT :Limit OFFSET :Offset"
if len(opts.NotAssociatedToGroup) > 0 {
query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup)
}
if len(opts.ExcludeChannelNames) > 0 {
query = query.Where(fmt.Sprintf("c.Name NOT IN ('%s')", strings.Join(opts.ExcludeChannelNames, "', '")))
}
queryString, args, err := query.ToSql()
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.GetAllChannels", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
data := &model.ChannelListWithTeamData{}
_, err := s.GetReplica().Select(data, query, map[string]interface{}{"Limit": limit, "Offset": offset})
_, err = s.GetReplica().Select(data, queryString, args...)
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.GetAllChannels", "store.sql_channel.get_all_channels.get.app_error", nil, err.Error(), http.StatusInternalServerError)
@@ -2120,29 +2140,45 @@ func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted
})
}
func (s SqlChannelStore) SearchAllChannels(term string, includeDeleted bool) store.StoreChannel {
func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
parameters := map[string]interface{}{}
deleteFilter := "AND c.DeleteAt = 0"
if includeDeleted {
deleteFilter = ""
query := s.getQueryBuilder().
Select("c.*, t.DisplayName AS TeamDisplayName, t.Name AS TeamName, t.UpdateAt as TeamUpdateAt").
From("Channels AS c").
Join("Teams AS t ON t.Id = c.TeamId").
Where(sq.Eq{"c.Type": []string{model.CHANNEL_PRIVATE, model.CHANNEL_OPEN}}).
OrderBy("c.DisplayName, t.DisplayName").
Limit(uint64(100))
if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": int(0)})
}
searchQuery := `SELECT c.*, t.DisplayName AS TeamDisplayName, t.Name AS TeamName, t.UpdateAt as TeamUpdateAt FROM Channels AS c JOIN Teams AS t ON t.Id = c.TeamId WHERE (c.Type = 'P' OR c.Type = 'O') ` + deleteFilter + ` SEARCH_CLAUSE ORDER BY c.DisplayName, t.DisplayName LIMIT 100`
likeClause, likeTerm := s.buildLIKEClause(term, "c.Name, c.DisplayName, c.Purpose")
if likeTerm == "" {
// If the likeTerm is empty after preparing, then don't bother searching.
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1)
} else {
parameters["LikeTerm"] = likeTerm
if len(likeTerm) > 0 {
likeClause = strings.ReplaceAll(likeClause, ":LikeTerm", "'"+likeTerm+"'")
fulltextClause, fulltextTerm := s.buildFulltextClause(term, "c.Name, c.DisplayName, c.Purpose")
parameters["FulltextTerm"] = fulltextTerm
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "AND ("+likeClause+" OR "+fulltextClause+")", 1)
fulltextClause = strings.ReplaceAll(fulltextClause, ":FulltextTerm", "'"+fulltextTerm+"'")
query = query.Where("(" + likeClause + " OR " + fulltextClause + ")")
}
if len(opts.ExcludeChannelNames) > 0 {
query = query.Where(fmt.Sprintf("c.Name NOT IN ('%s')", strings.Join(opts.ExcludeChannelNames, "', '")))
}
if len(opts.NotAssociatedToGroup) > 0 {
query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup)
}
queryString, args, err := query.ToSql()
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.SearchAllChannels", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
var channels model.ChannelListWithTeamData
if _, err := s.GetReplica().Select(&channels, searchQuery, parameters); err != nil {
if _, err := s.GetReplica().Select(&channels, queryString, args...); err != nil {
result.Err = model.NewAppError("SqlChannelStore.Search", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
@@ -2501,7 +2537,7 @@ func (s SqlChannelStore) GetAllChannelsForExportAfter(limit int, afterId string)
Id
LIMIT :Limit`,
map[string]interface{}{"AfterId": afterId, "Limit": limit}); err != nil {
result.Err = model.NewAppError("SqlTeamStore.GetAllChannelsForExportAfter", "store.sql_channel.get_all.app_error", nil, err.Error(), http.StatusInternalServerError)
result.Err = model.NewAppError("SqlChannelStore.GetAllChannelsForExportAfter", "store.sql_channel.get_all.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
@@ -2561,12 +2597,12 @@ func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId s
queryString, args, err := query.ToSql()
if err != nil {
result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
result.Err = model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if _, err = s.GetReplica().Select(&directChannelsForExport, queryString, args...); err != nil {
result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
result.Err = model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
@@ -2585,13 +2621,13 @@ func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId s
queryString, args, err = query.ToSql()
if err != nil {
result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
result.Err = model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
var channelMembers []*model.ChannelMemberForExport
if _, err := s.GetReplica().Select(&channelMembers, queryString, args...); err != nil {
result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
result.Err = model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
}
// Populate each channel with its members

Просмотреть файл

@@ -148,7 +148,7 @@ type ChannelStore interface {
GetDeletedByName(team_id string, name string) StoreChannel
GetDeleted(team_id string, offset int, limit int) StoreChannel
GetChannels(teamId string, userId string, includeDeleted bool) StoreChannel
GetAllChannels(page, perPage int, includeDeleted bool) StoreChannel
GetAllChannels(page, perPage int, opts ChannelSearchOpts) StoreChannel
GetMoreChannels(teamId string, userId string, offset int, limit int) StoreChannel
GetPublicChannelsForTeam(teamId string, offset int, limit int) StoreChannel
GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) StoreChannel
@@ -182,7 +182,7 @@ type ChannelStore interface {
GetMembersForUserWithPagination(teamId, userId string, page, perPage int) StoreChannel
AutocompleteInTeam(teamId string, term string, includeDeleted bool) StoreChannel
AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) StoreChannel
SearchAllChannels(term string, includeDeleted bool) StoreChannel
SearchAllChannels(term string, opts ChannelSearchOpts) StoreChannel
SearchInTeam(teamId string, term string, includeDeleted bool) StoreChannel
SearchMore(userId string, teamId string, term string) StoreChannel
GetMembersByIds(channelId string, userIds []string) StoreChannel
@@ -607,3 +607,15 @@ type LinkMetadataStore interface {
Save(linkMetadata *model.LinkMetadata) StoreChannel
Get(url string, timestamp int64) StoreChannel
}
// ChannelSearchOpts contains options for searching channels.
//
// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records.
// IncludeDeleted will include channel records where DeleteAt != 0.
// ExcludeChannelNames will exclude channels from the results by name.
//
type ChannelSearchOpts struct {
NotAssociatedToGroup string
IncludeDeleted bool
ExcludeChannelNames []string
}

Просмотреть файл

@@ -23,9 +23,9 @@ type SqlSupplier interface {
}
func cleanupChannels(t *testing.T, ss store.Store) {
result := <-ss.Channel().GetAllChannels(0, 100000, true)
result := <-ss.Channel().GetAllChannels(0, 100000, store.ChannelSearchOpts{IncludeDeleted: true})
if result.Err != nil {
t.Fatal("error cleaning all channels")
t.Fatalf("error cleaning all channels: %v", result.Err)
}
list := result.Data.(*model.ChannelListWithTeamData)
for _, channel := range *list {
@@ -1073,6 +1073,15 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlSupplier)
c1.Type = model.CHANNEL_OPEN
store.Must(ss.Channel().Save(&c1, -1))
group := &model.Group{
Name: model.NewId(),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewId(),
}
store.Must(ss.Group().Create(group))
store.Must(ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, c1.Id, true)))
c2 := model.Channel{}
c2.TeamId = t1.Id
c2.DisplayName = "Channel2" + model.NewId()
@@ -1103,7 +1112,7 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlSupplier)
c5.Type = model.CHANNEL_GROUP
store.Must(ss.Channel().Save(&c5, -1))
cresult := <-ss.Channel().GetAllChannels(0, 10, false)
cresult := <-ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{})
list := cresult.Data.(*model.ChannelListWithTeamData)
assert.Len(t, *list, 2)
assert.Equal(t, (*list)[0].Id, c1.Id)
@@ -1111,7 +1120,7 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlSupplier)
assert.Equal(t, (*list)[1].Id, c3.Id)
assert.Equal(t, (*list)[1].TeamDisplayName, "Name2")
cresult = <-ss.Channel().GetAllChannels(0, 10, true)
cresult = <-ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{IncludeDeleted: true})
list = cresult.Data.(*model.ChannelListWithTeamData)
assert.Len(t, *list, 3)
assert.Equal(t, (*list)[0].Id, c1.Id)
@@ -1119,12 +1128,22 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlSupplier)
assert.Equal(t, (*list)[1].Id, c2.Id)
assert.Equal(t, (*list)[2].Id, c3.Id)
cresult = <-ss.Channel().GetAllChannels(0, 1, true)
cresult = <-ss.Channel().GetAllChannels(0, 1, store.ChannelSearchOpts{IncludeDeleted: true})
list = cresult.Data.(*model.ChannelListWithTeamData)
assert.Len(t, *list, 1)
assert.Equal(t, (*list)[0].Id, c1.Id)
assert.Equal(t, (*list)[0].TeamDisplayName, "Name")
// Not associated to group
cresult = <-ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{NotAssociatedToGroup: group.Id})
list = cresult.Data.(*model.ChannelListWithTeamData)
assert.Len(t, *list, 1)
// Exclude channel names
cresult = <-ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{ExcludeChannelNames: []string{c1.Name}})
list = cresult.Data.(*model.ChannelListWithTeamData)
assert.Len(t, *list, 1)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMaster().Exec("TRUNCATE Channels")
}
@@ -2325,6 +2344,15 @@ func testChannelStoreSearchAllChannels(t *testing.T, ss store.Store) {
}
store.Must(ss.Channel().Save(&o7, -1))
group := &model.Group{
Name: model.NewId(),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewId(),
}
store.Must(ss.Group().Create(group))
store.Must(ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, o7.Id, true)))
o8 := model.Channel{
TeamId: t1.Id,
DisplayName: "Off-Limit",
@@ -2381,28 +2409,31 @@ func testChannelStoreSearchAllChannels(t *testing.T, ss store.Store) {
testCases := []struct {
Description string
Term string
IncludeDeleted bool
Opts store.ChannelSearchOpts
ExpectedResults *model.ChannelList
}{
{"ChannelA", "ChannelA", false, &model.ChannelList{&o1, &o2, &o3}},
{"ChannelA, include deleted", "ChannelA", true, &model.ChannelList{&o1, &o2, &o3, &o13}},
{"empty string", "", false, &model.ChannelList{&o1, &o2, &o3, &o4, &o5, &o12, &o11, &o8, &o7, &o6, &o10, &o9}},
{"no matches", "blargh", false, &model.ChannelList{}},
{"prefix", "off-", false, &model.ChannelList{&o8, &o7, &o6}},
{"full match with dash", "off-topic", false, &model.ChannelList{&o6}},
{"town square", "town square", false, &model.ChannelList{&o9}},
{"the in name", "the", false, &model.ChannelList{&o10}},
{"Mobile", "Mobile", false, &model.ChannelList{&o11}},
{"search purpose", "now searchable", false, &model.ChannelList{&o12}},
{"pipe ignored", "town square |", false, &model.ChannelList{&o9}},
{"ChannelA", "ChannelA", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o1, &o2, &o3}},
{"ChannelA, include deleted", "ChannelA", store.ChannelSearchOpts{IncludeDeleted: true}, &model.ChannelList{&o1, &o2, &o3, &o13}},
{"empty string", "", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o1, &o2, &o3, &o4, &o5, &o12, &o11, &o8, &o7, &o6, &o10, &o9}},
{"no matches", "blargh", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{}},
{"prefix", "off-", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o8, &o7, &o6}},
{"full match with dash", "off-topic", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o6}},
{"town square", "town square", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o9}},
{"the in name", "the", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o10}},
{"Mobile", "Mobile", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o11}},
{"search purpose", "now searchable", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o12}},
{"pipe ignored", "town square |", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o9}},
{"exclude defaults search 'off'", "off-", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeChannelNames: []string{"off-topic"}}, &model.ChannelList{&o8, &o7}},
{"exclude defaults search 'town'", "town", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeChannelNames: []string{"town-square"}}, &model.ChannelList{}},
{"exclude by group association", "off", store.ChannelSearchOpts{IncludeDeleted: false, NotAssociatedToGroup: group.Id}, &model.ChannelList{&o8, &o6}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
result := <-ss.Channel().SearchAllChannels(testCase.Term, testCase.IncludeDeleted)
result := <-ss.Channel().SearchAllChannels(testCase.Term, testCase.Opts)
require.Nil(t, result.Err)
channels := result.Data.(*model.ChannelListWithTeamData)
require.Equal(t, len(*channels), len(*testCase.ExpectedResults))
require.Equal(t, len(*testCase.ExpectedResults), len(*channels))
for i, expected := range *testCase.ExpectedResults {
require.Equal(t, (*channels)[i].Id, expected.Id)
}

Просмотреть файл

@@ -212,13 +212,13 @@ func (_m *ChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId stri
return r0
}
// GetAllChannels provides a mock function with given fields: page, perPage, includeDeleted
func (_m *ChannelStore) GetAllChannels(page int, perPage int, includeDeleted bool) store.StoreChannel {
ret := _m.Called(page, perPage, includeDeleted)
// GetAllChannels provides a mock function with given fields: page, perPage, opts
func (_m *ChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) store.StoreChannel {
ret := _m.Called(page, perPage, opts)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(int, int, bool) store.StoreChannel); ok {
r0 = rf(page, perPage, includeDeleted)
if rf, ok := ret.Get(0).(func(int, int, store.ChannelSearchOpts) store.StoreChannel); ok {
r0 = rf(page, perPage, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)
@@ -1003,13 +1003,13 @@ func (_m *ChannelStore) SaveMember(member *model.ChannelMember) store.StoreChann
return r0
}
// SearchAllChannels provides a mock function with given fields: term, includeDeleted
func (_m *ChannelStore) SearchAllChannels(term string, includeDeleted bool) store.StoreChannel {
ret := _m.Called(term, includeDeleted)
// SearchAllChannels provides a mock function with given fields: term, opts
func (_m *ChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) store.StoreChannel {
ret := _m.Called(term, opts)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string, bool) store.StoreChannel); ok {
r0 = rf(term, includeDeleted)
if rf, ok := ret.Get(0).(func(string, store.ChannelSearchOpts) store.StoreChannel); ok {
r0 = rf(term, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)