diff --git a/api4/team.go b/api4/team.go index 9f01dda811..e8a4a035f9 100644 --- a/api4/team.go +++ b/api4/team.go @@ -1000,11 +1000,6 @@ func searchTeams(c *Context, w http.ResponseWriter, r *http.Request) { return } - if len(props.Term) == 0 { - c.SetInvalidParam("term") - return - } - var teams []*model.Team var totalCount int64 var err *model.AppError diff --git a/app/team.go b/app/team.go index b07fc5457f..448bce4cbb 100644 --- a/app/team.go +++ b/app/team.go @@ -763,9 +763,9 @@ func (a *App) GetAllPublicTeamsPageWithCount(offset int, limit int) (*model.Team // SearchAllTeams returns a team list and the total count of the results func (a *App) SearchAllTeams(searchOpts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) { if searchOpts.IsPaginated() { - return a.Srv().Store.Team().SearchAllPaged(searchOpts.Term, *searchOpts.Page, *searchOpts.PerPage) + return a.Srv().Store.Team().SearchAllPaged(searchOpts.Term, searchOpts) } - results, err := a.Srv().Store.Team().SearchAll(searchOpts.Term) + results, err := a.Srv().Store.Team().SearchAll(searchOpts.Term, searchOpts) return results, int64(len(results)), err } diff --git a/model/team_search.go b/model/team_search.go index b8b1fe3055..f9de580186 100644 --- a/model/team_search.go +++ b/model/team_search.go @@ -9,9 +9,12 @@ import ( ) type TeamSearch struct { - Term string `json:"term"` - Page *int `json:"page,omitempty"` - PerPage *int `json:"per_page,omitempty"` + Term string `json:"term"` + Page *int `json:"page,omitempty"` + PerPage *int `json:"per_page,omitempty"` + AllowOpenInvite *bool `json:"allow_open_invite,omitempty"` + GroupConstrained *bool `json:"group_constrained,omitempty"` + IncludeGroupConstrained *bool `json:"include_group_constrained,omitempty"` } func (t *TeamSearch) IsPaginated() bool { diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 571e75baad..cb5c49a69f 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -7222,7 +7222,7 @@ func (s *OpenTracingLayerTeamStore) SaveMultipleMembers(members []*model.TeamMem return resultVar0, resultVar1 } -func (s *OpenTracingLayerTeamStore) SearchAll(term string) ([]*model.Team, *model.AppError) { +func (s *OpenTracingLayerTeamStore) SearchAll(term string, opts *model.TeamSearch) ([]*model.Team, *model.AppError) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchAll") s.Root.Store.SetContext(newCtx) @@ -7231,7 +7231,7 @@ func (s *OpenTracingLayerTeamStore) SearchAll(term string) ([]*model.Team, *mode }() defer span.Finish() - resultVar0, resultVar1 := s.TeamStore.SearchAll(term) + resultVar0, resultVar1 := s.TeamStore.SearchAll(term, opts) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) ext.Error.Set(span, true) @@ -7240,7 +7240,7 @@ func (s *OpenTracingLayerTeamStore) SearchAll(term string) ([]*model.Team, *mode return resultVar0, resultVar1 } -func (s *OpenTracingLayerTeamStore) SearchAllPaged(term string, page int, perPage int) ([]*model.Team, int64, *model.AppError) { +func (s *OpenTracingLayerTeamStore) SearchAllPaged(term string, opts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchAllPaged") s.Root.Store.SetContext(newCtx) @@ -7249,7 +7249,7 @@ func (s *OpenTracingLayerTeamStore) SearchAllPaged(term string, page int, perPag }() defer span.Finish() - resultVar0, resultVar1, resultVar2 := s.TeamStore.SearchAllPaged(term, page, perPage) + resultVar0, resultVar1, resultVar2 := s.TeamStore.SearchAllPaged(term, opts) if resultVar2 != nil { span.LogFields(spanlog.Error(resultVar2)) ext.Error.Set(span, true) diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go index b734f5ffd2..68ceb2aa6b 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -372,20 +372,94 @@ func (s SqlTeamStore) GetByNames(names []string) ([]*model.Team, *model.AppError return teams, nil } +func (s SqlTeamStore) teamSearchQuery(term string, opts *model.TeamSearch, countQuery bool) sq.SelectBuilder { + var selectStr string + if countQuery { + selectStr = "count(*)" + } else { + selectStr = "*" + } + + query := s.getQueryBuilder(). + Select(selectStr). + From("Teams as t") + + // Don't order or limit if getting count + if !countQuery { + query = query.OrderBy("t.DisplayName") + + if opts.IsPaginated() { + query = query.Limit(uint64(*opts.PerPage)).Offset(uint64(*opts.Page * *opts.PerPage)) + } + } + + if len(term) > 0 { + term = sanitizeSearchTerm(term, "\\") + term = wildcardSearchTerm(term) + + operatorKeyword := "ILIKE" + if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + operatorKeyword = "LIKE" + } + + query = query.Where(fmt.Sprintf("(Name %[1]s ? OR DisplayName %[1]s ?)", operatorKeyword), term, term) + } + + var teamFilters sq.Sqlizer + var openInviteFilter sq.Sqlizer + if opts.AllowOpenInvite != nil { + if *opts.AllowOpenInvite { + openInviteFilter = sq.Eq{"AllowOpenInvite": true} + } else { + openInviteFilter = sq.And{ + sq.Or{ + sq.NotEq{"AllowOpenInvite": true}, + sq.Eq{"AllowOpenInvite": nil}, + }, + sq.Or{ + sq.NotEq{"GroupConstrained": true}, + sq.Eq{"GroupConstrained": nil}, + }, + } + } + + teamFilters = openInviteFilter + } + + var groupConstrainedFilter sq.Sqlizer + if opts.GroupConstrained != nil { + if *opts.GroupConstrained { + groupConstrainedFilter = sq.Eq{"GroupConstrained": true} + } else { + groupConstrainedFilter = sq.Or{ + sq.NotEq{"GroupConstrained": true}, + sq.Eq{"GroupConstrained": nil}, + } + } + + if teamFilters == nil { + teamFilters = groupConstrainedFilter + } else { + teamFilters = sq.Or{teamFilters, groupConstrainedFilter} + } + } + + query = query.Where(teamFilters) + + return query +} + // SearchAll returns from the database a list of teams that match the Name or DisplayName // passed as the term search parameter. -func (s SqlTeamStore) SearchAll(term string) ([]*model.Team, *model.AppError) { +func (s SqlTeamStore) SearchAll(term string, opts *model.TeamSearch) ([]*model.Team, *model.AppError) { var teams []*model.Team - term = sanitizeSearchTerm(term, "\\") - term = wildcardSearchTerm(term) - - operatorKeyword := "ILIKE" - if s.DriverName() == model.DATABASE_DRIVER_MYSQL { - operatorKeyword = "LIKE" + queryString, args, err := s.teamSearchQuery(term, opts, false).ToSql() + if err != nil { + return nil, model.NewAppError("SqlTeamStore.SearchAll", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError) } - queryString := fmt.Sprintf("SELECT * FROM Teams WHERE Name %[1]s :Term OR DisplayName %[1]s :Term", operatorKeyword) - if _, err := s.GetReplica().Select(&teams, queryString, map[string]interface{}{"Term": term}); err != nil { + + if _, err = s.GetReplica().Select(&teams, queryString, args...); err != nil { return nil, model.NewAppError("SqlTeamStore.SearchAll", "store.sql_team.search_all_team.app_error", nil, "term="+term+", "+err.Error(), http.StatusInternalServerError) } @@ -393,24 +467,23 @@ func (s SqlTeamStore) SearchAll(term string) ([]*model.Team, *model.AppError) { } // SearchAllPaged returns a teams list and the total count of teams that matched the search. -func (s SqlTeamStore) SearchAllPaged(term string, page int, perPage int) ([]*model.Team, int64, *model.AppError) { +func (s SqlTeamStore) SearchAllPaged(term string, opts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) { var teams []*model.Team var totalCount int64 - offset := page * perPage - term = sanitizeSearchTerm(term, "\\") - term = wildcardSearchTerm(term) - operatorKeyword := "ILIKE" - if s.DriverName() == model.DATABASE_DRIVER_MYSQL { - operatorKeyword = "LIKE" + queryString, args, err := s.teamSearchQuery(term, opts, false).ToSql() + if err != nil { + return nil, 0, model.NewAppError("SqlTeamStore.SearchAllPage", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError) } - queryString := fmt.Sprintf("SELECT * FROM Teams WHERE Name %[1]s :Term OR DisplayName %[1]s :Term ORDER BY DisplayName, Name LIMIT :Limit OFFSET :Offset", operatorKeyword) - if _, err := s.GetReplica().Select(&teams, queryString, map[string]interface{}{"Term": term, "Limit": perPage, "Offset": offset}); err != nil { + if _, err = s.GetReplica().Select(&teams, queryString, args...); err != nil { return nil, 0, model.NewAppError("SqlTeamStore.SearchAllPage", "store.sql_team.search_all_team.app_error", nil, "term="+term+", "+err.Error(), http.StatusInternalServerError) } - queryString = fmt.Sprintf("SELECT COUNT(*) FROM Teams WHERE Name %[1]s :Term OR DisplayName %[1]s :Term", operatorKeyword) - totalCount, err := s.GetReplica().SelectInt(queryString, map[string]interface{}{"Term": term}) + queryString, args, err = s.teamSearchQuery(term, opts, true).ToSql() + if err != nil { + return nil, 0, model.NewAppError("SqlTeamStore.SearchAllPage", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError) + } + totalCount, err = s.GetReplica().SelectInt(queryString, args...) if err != nil { return nil, 0, model.NewAppError("SqlTeamStore.SearchAllPage", "store.sql_team.search_all_team.app_error", nil, "term="+term+", "+err.Error(), http.StatusInternalServerError) } diff --git a/store/store.go b/store/store.go index 558372aa7a..95b8dac024 100644 --- a/store/store.go +++ b/store/store.go @@ -78,8 +78,8 @@ type TeamStore interface { Get(id string) (*model.Team, *model.AppError) GetByName(name string) (*model.Team, *model.AppError) GetByNames(name []string) ([]*model.Team, *model.AppError) - SearchAll(term string) ([]*model.Team, *model.AppError) - SearchAllPaged(term string, page int, perPage int) ([]*model.Team, int64, *model.AppError) + SearchAll(term string, opts *model.TeamSearch) ([]*model.Team, *model.AppError) + SearchAllPaged(term string, opts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) SearchOpen(term string) ([]*model.Team, *model.AppError) SearchPrivate(term string) ([]*model.Team, *model.AppError) GetAll() ([]*model.Team, *model.AppError) diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go index dba9cbca74..aad111d9e6 100644 --- a/store/storetest/mocks/TeamStore.go +++ b/store/storetest/mocks/TeamStore.go @@ -972,13 +972,13 @@ func (_m *TeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPe return r0, r1 } -// SearchAll provides a mock function with given fields: term -func (_m *TeamStore) SearchAll(term string) ([]*model.Team, *model.AppError) { - ret := _m.Called(term) +// SearchAll provides a mock function with given fields: term, opts +func (_m *TeamStore) SearchAll(term string, opts *model.TeamSearch) ([]*model.Team, *model.AppError) { + ret := _m.Called(term, opts) var r0 []*model.Team - if rf, ok := ret.Get(0).(func(string) []*model.Team); ok { - r0 = rf(term) + if rf, ok := ret.Get(0).(func(string, *model.TeamSearch) []*model.Team); ok { + r0 = rf(term, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Team) @@ -986,8 +986,8 @@ func (_m *TeamStore) SearchAll(term string) ([]*model.Team, *model.AppError) { } var r1 *model.AppError - if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { - r1 = rf(term) + if rf, ok := ret.Get(1).(func(string, *model.TeamSearch) *model.AppError); ok { + r1 = rf(term, opts) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*model.AppError) @@ -997,13 +997,13 @@ func (_m *TeamStore) SearchAll(term string) ([]*model.Team, *model.AppError) { return r0, r1 } -// SearchAllPaged provides a mock function with given fields: term, page, perPage -func (_m *TeamStore) SearchAllPaged(term string, page int, perPage int) ([]*model.Team, int64, *model.AppError) { - ret := _m.Called(term, page, perPage) +// SearchAllPaged provides a mock function with given fields: term, opts +func (_m *TeamStore) SearchAllPaged(term string, opts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) { + ret := _m.Called(term, opts) var r0 []*model.Team - if rf, ok := ret.Get(0).(func(string, int, int) []*model.Team); ok { - r0 = rf(term, page, perPage) + if rf, ok := ret.Get(0).(func(string, *model.TeamSearch) []*model.Team); ok { + r0 = rf(term, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Team) @@ -1011,15 +1011,15 @@ func (_m *TeamStore) SearchAllPaged(term string, page int, perPage int) ([]*mode } var r1 int64 - if rf, ok := ret.Get(1).(func(string, int, int) int64); ok { - r1 = rf(term, page, perPage) + if rf, ok := ret.Get(1).(func(string, *model.TeamSearch) int64); ok { + r1 = rf(term, opts) } else { r1 = ret.Get(1).(int64) } var r2 *model.AppError - if rf, ok := ret.Get(2).(func(string, int, int) *model.AppError); ok { - r2 = rf(term, page, perPage) + if rf, ok := ret.Get(2).(func(string, *model.TeamSearch) *model.AppError); ok { + r2 = rf(term, opts) } else { if ret.Get(2) != nil { r2 = ret.Get(2).(*model.AppError) diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index 4e33b3acde..f1d38ed120 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -222,7 +222,7 @@ func testTeamStoreSearchAll(t *testing.T, ss store.Store) { require.Nil(t, err) p := model.Team{} - p.DisplayName = "ADisplayName" + model.NewId() + p.DisplayName = "BDisplayName" + model.NewId() p.Name = "zzzzzz-" + model.NewId() + "a" p.Email = MakeEmail() p.Type = model.TEAM_OPEN @@ -231,6 +231,17 @@ func testTeamStoreSearchAll(t *testing.T, ss store.Store) { _, err = ss.Team().Save(&p) require.Nil(t, err) + g := model.Team{} + g.DisplayName = "CDisplayName" + model.NewId() + g.Name = "zzzzzz-" + model.NewId() + "a" + g.Email = MakeEmail() + g.Type = model.TEAM_OPEN + g.AllowOpenInvite = false + g.GroupConstrained = model.NewBool(true) + + _, err = ss.Team().Save(&g) + require.Nil(t, err) + q := model.Team{} q.DisplayName = "CHOCOLATE" q.Name = "ilovecake" @@ -243,80 +254,130 @@ func testTeamStoreSearchAll(t *testing.T, ss store.Store) { testCases := []struct { Name string - Term string + Opts *model.TeamSearch ExpectedLenth int - ExpectedFirstId string + ExpectedTeamIds []string }{ { "Search chocolate by display name", - "ocola", + &model.TeamSearch{Term: "ocola"}, 1, - q.Id, + []string{q.Id}, }, { "Search chocolate by display name", - "choc", + &model.TeamSearch{Term: "choc"}, 1, - q.Id, + []string{q.Id}, }, { "Search chocolate by display name", - "late", + &model.TeamSearch{Term: "late"}, 1, - q.Id, + []string{q.Id}, }, { "Search chocolate by name", - "ilov", + &model.TeamSearch{Term: "ilov"}, 1, - q.Id, + []string{q.Id}, }, { "Search chocolate by name", - "ecake", + &model.TeamSearch{Term: "ecake"}, 1, - q.Id, + []string{q.Id}, }, { "Search for open team name", - o.Name, + &model.TeamSearch{Term: o.Name}, 1, - o.Id, + []string{o.Id}, }, { "Search for open team displayName", - o.DisplayName, + &model.TeamSearch{Term: o.DisplayName}, 1, - o.Id, + []string{o.Id}, }, { "Search for open team without results", - "junk", + &model.TeamSearch{Term: "junk"}, 0, - "", + []string{}, }, { "Search for private team", - p.DisplayName, + &model.TeamSearch{Term: p.DisplayName}, 1, - p.Id, + []string{p.Id}, }, { - "Search for both teams", - "zzzzzz", + "Search for all 3 z teams", + &model.TeamSearch{Term: "zzzzzz"}, + 3, + []string{o.Id, p.Id, g.Id}, + }, + { + "Search for all 3 teams filter by allow open invite", + &model.TeamSearch{Term: "zzzzzz", AllowOpenInvite: model.NewBool(true)}, + 1, + []string{o.Id}, + }, + { + "Search for all 3 teams filter by allow open invite = false", + &model.TeamSearch{Term: "zzzzzz", AllowOpenInvite: model.NewBool(false)}, + 1, + []string{p.Id}, + }, + { + "Search for all 3 teams filter by group constrained", + &model.TeamSearch{Term: "zzzzzz", GroupConstrained: model.NewBool(true)}, + 1, + []string{g.Id}, + }, + { + "Search for all 3 teams filter by group constrained = false", + &model.TeamSearch{Term: "zzzzzz", GroupConstrained: model.NewBool(false)}, 2, - "", + []string{o.Id, p.Id}, + }, + { + "Search for all 3 teams filter by allow open invite and include group constrained", + &model.TeamSearch{Term: "zzzzzz", AllowOpenInvite: model.NewBool(true), GroupConstrained: model.NewBool(true)}, + 2, + []string{o.Id, g.Id}, + }, + { + "Search for all 3 teams filter by group constrained and not open invite", + &model.TeamSearch{Term: "zzzzzz", GroupConstrained: model.NewBool(true), AllowOpenInvite: model.NewBool(false)}, + 2, + []string{g.Id, p.Id}, + }, + { + "Search for all 3 teams filter by group constrained false and open invite", + &model.TeamSearch{Term: "zzzzzz", GroupConstrained: model.NewBool(false), AllowOpenInvite: model.NewBool(true)}, + 2, + []string{o.Id, p.Id}, + }, + { + "Search for all 3 teams filter by group constrained false and open invite false", + &model.TeamSearch{Term: "zzzzzz", GroupConstrained: model.NewBool(false), AllowOpenInvite: model.NewBool(false)}, + 2, + []string{p.Id, o.Id}, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - r1, err := ss.Team().SearchAll(tc.Term) + response, err := ss.Team().SearchAll(tc.Opts.Term, tc.Opts) require.Nil(t, err) - require.Equal(t, tc.ExpectedLenth, len(r1)) - if tc.ExpectedFirstId != "" { - assert.Equal(t, tc.ExpectedFirstId, r1[0].Id) + require.Equal(t, tc.ExpectedLenth, len(response)) + responseTeamIds := []string{} + for _, team := range response { + responseTeamIds = append(responseTeamIds, team.Id) } + require.ElementsMatch(t, tc.ExpectedTeamIds, responseTeamIds) }) } } diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 6c98e53086..0358c45730 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -6522,10 +6522,10 @@ func (s *TimerLayerTeamStore) SaveMultipleMembers(members []*model.TeamMember, m return resultVar0, resultVar1 } -func (s *TimerLayerTeamStore) SearchAll(term string) ([]*model.Team, *model.AppError) { +func (s *TimerLayerTeamStore) SearchAll(term string, opts *model.TeamSearch) ([]*model.Team, *model.AppError) { start := timemodule.Now() - resultVar0, resultVar1 := s.TeamStore.SearchAll(term) + resultVar0, resultVar1 := s.TeamStore.SearchAll(term, opts) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil { @@ -6538,10 +6538,10 @@ func (s *TimerLayerTeamStore) SearchAll(term string) ([]*model.Team, *model.AppE return resultVar0, resultVar1 } -func (s *TimerLayerTeamStore) SearchAllPaged(term string, page int, perPage int) ([]*model.Team, int64, *model.AppError) { +func (s *TimerLayerTeamStore) SearchAllPaged(term string, opts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) { start := timemodule.Now() - resultVar0, resultVar1, resultVar2 := s.TeamStore.SearchAllPaged(term, page, perPage) + resultVar0, resultVar1, resultVar2 := s.TeamStore.SearchAllPaged(term, opts) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil {