feat: add paginated API endpoint for groups (#27603)

backend-only changes from #27271; see that PR for summary of changes +
implementation details
This commit is contained in:
Andrew Aquino
2026-08-10 13:23:14 -07:00
committed by GitHub
parent ddb2799009
commit 6e07e2610f
19 changed files with 1189 additions and 0 deletions
+24
View File
@@ -174,6 +174,30 @@ func Users(query string) (database.GetUsersParams, []codersdk.ValidationError) {
return filter, parser.Errors
}
// Groups parses a group search query using the standard filter syntax shared
// with the rest of the dashboard. Bare terms (including multi-word terms)
// become a free-text search over group name and display name. A value that
// contains a colon must be quoted or supplied via the explicit search key,
// e.g. search:"team: frontend", because an unquoted colon is otherwise treated
// as a key:value delimiter. Unknown keys are rejected, which keeps room for
// real key:value filters in the future.
func Groups(query string) (string, []codersdk.ValidationError) {
// Always lowercase for all searches.
query = strings.ToLower(query)
values, errors := searchTerms(query, func(term string, values url.Values) error {
values.Add("search", term)
return nil
})
if len(errors) > 0 {
return "", errors
}
parser := httpapi.NewQueryParamParser()
search := parser.String(values, "", "search")
parser.ErrorExcessParams(values)
return search, parser.Errors
}
func Members(query string, organizationID uuid.UUID) (database.OrganizationMembersParams, []codersdk.ValidationError) {
query = strings.TrimSpace(query)
if query == "" {
+91
View File
@@ -1753,3 +1753,94 @@ func TestSearchChats(t *testing.T) {
})
}
}
func TestSearchGroups(t *testing.T) {
t.Parallel()
testCases := []struct {
Name string
Query string
Expected string
ExpectedErrorContains string
}{
{
Name: "Empty",
Query: "",
Expected: "",
},
{
Name: "SingleWord",
Query: "alpha",
Expected: "alpha",
},
{
// Groups support free-text search, so an unquoted multi-word query
// is joined into a single search value instead of being rejected as
// a duplicate param.
Name: "MultiWord",
Query: "front end",
Expected: "front end",
},
{
Name: "CaseInsensitive",
Query: "AlPhA",
Expected: "alpha",
},
{
Name: "MultiWordCaseInsensitive",
Query: "Front End",
Expected: "front end",
},
{
Name: "TrimsSurroundingSpaces",
Query: " alpha ",
Expected: "alpha",
},
{
// Structured key:value queries are not supported for groups; the
// unrecognized key surfaces as an invalid query param. Rejecting
// unknown keys leaves room for real key:value filters later.
Name: "StructuredKeyValueRejected",
Query: "name:alpha",
ExpectedErrorContains: "is not a valid query param",
},
{
// The explicit search key is supported.
Name: "SearchKey",
Query: "search:alpha",
Expected: "alpha",
},
{
// A colon-containing name is searchable when quoted via the search
// key, since group display names may legally contain colons.
Name: "QuotedColonValue",
Query: `search:"team: frontend"`,
Expected: "team: frontend",
},
{
// An unquoted colon is treated as a key:value delimiter, so a bare
// colon term is rejected. Users must quote it (see QuotedColonValue).
Name: "BareColonRejected",
Query: "team: frontend",
ExpectedErrorContains: "cannot start or end with ':'",
},
}
for _, c := range testCases {
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
search, errs := searchquery.Groups(c.Query)
if c.ExpectedErrorContains != "" {
require.True(t, len(errs) > 0, "expect some errors")
var s strings.Builder
for _, err := range errs {
_, _ = s.WriteString(fmt.Sprintf("%s: %s\n", err.Field, err.Detail))
}
require.Contains(t, s.String(), c.ExpectedErrorContains)
} else {
require.Len(t, errs, 0, "expected no error")
require.Equal(t, c.Expected, search, "expected search value")
}
})
}
}