feat(users): add user search endpoint
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
452b190e9d
commit
2460a7b09e
2 changed files with 218 additions and 0 deletions
112
users.go
112
users.go
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"iter"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"dappco.re/go/core/forge/types"
|
||||
)
|
||||
|
|
@ -18,6 +20,25 @@ type UserService struct {
|
|||
Resource[types.User, struct{}, struct{}]
|
||||
}
|
||||
|
||||
// UserSearchOptions controls filtering for user searches.
|
||||
type UserSearchOptions struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
func (o UserSearchOptions) queryParams() map[string]string {
|
||||
if o.UID == 0 {
|
||||
return nil
|
||||
}
|
||||
return map[string]string{
|
||||
"uid": strconv.FormatInt(o.UID, 10),
|
||||
}
|
||||
}
|
||||
|
||||
type userSearchResults struct {
|
||||
Data []*types.User `json:"data,omitempty"`
|
||||
OK bool `json:"ok,omitempty"`
|
||||
}
|
||||
|
||||
func newUserService(c *Client) *UserService {
|
||||
return &UserService{
|
||||
Resource: *NewResource[types.User, struct{}, struct{}](
|
||||
|
|
@ -62,6 +83,97 @@ func (s *UserService) GetQuota(ctx context.Context) (*types.QuotaInfo, error) {
|
|||
return &out, nil
|
||||
}
|
||||
|
||||
// SearchUsersPage returns a single page of users matching the search filters.
|
||||
func (s *UserService) SearchUsersPage(ctx context.Context, query string, pageOpts ListOptions, filters ...UserSearchOptions) (*PagedResult[types.User], error) {
|
||||
if pageOpts.Page < 1 {
|
||||
pageOpts.Page = 1
|
||||
}
|
||||
if pageOpts.Limit < 1 {
|
||||
pageOpts.Limit = 50
|
||||
}
|
||||
|
||||
u, err := url.Parse("/api/v1/users/search")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
q.Set("q", query)
|
||||
for _, filter := range filters {
|
||||
for key, value := range filter.queryParams() {
|
||||
q.Set(key, value)
|
||||
}
|
||||
}
|
||||
q.Set("page", strconv.Itoa(pageOpts.Page))
|
||||
q.Set("limit", strconv.Itoa(pageOpts.Limit))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
var out userSearchResults
|
||||
resp, err := s.client.doJSON(ctx, http.MethodGet, u.String(), nil, &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalCount, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
|
||||
items := make([]types.User, 0, len(out.Data))
|
||||
for _, user := range out.Data {
|
||||
if user != nil {
|
||||
items = append(items, *user)
|
||||
}
|
||||
}
|
||||
|
||||
return &PagedResult[types.User]{
|
||||
Items: items,
|
||||
TotalCount: totalCount,
|
||||
Page: pageOpts.Page,
|
||||
HasMore: (totalCount > 0 && (pageOpts.Page-1)*pageOpts.Limit+len(items) < totalCount) ||
|
||||
(totalCount == 0 && len(items) >= pageOpts.Limit),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchUsers returns all users matching the search filters.
|
||||
func (s *UserService) SearchUsers(ctx context.Context, query string, filters ...UserSearchOptions) ([]types.User, error) {
|
||||
var all []types.User
|
||||
page := 1
|
||||
|
||||
for {
|
||||
result, err := s.SearchUsersPage(ctx, query, ListOptions{Page: page, Limit: 50}, filters...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all = append(all, result.Items...)
|
||||
if !result.HasMore {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// IterSearchUsers returns an iterator over users matching the search filters.
|
||||
func (s *UserService) IterSearchUsers(ctx context.Context, query string, filters ...UserSearchOptions) iter.Seq2[types.User, error] {
|
||||
return func(yield func(types.User, error) bool) {
|
||||
page := 1
|
||||
for {
|
||||
result, err := s.SearchUsersPage(ctx, query, ListOptions{Page: page, Limit: 50}, filters...)
|
||||
if err != nil {
|
||||
yield(*new(types.User), err)
|
||||
return
|
||||
}
|
||||
for _, item := range result.Items {
|
||||
if !yield(item, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !result.HasMore {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ListQuotaArtifacts returns all artifacts affecting the authenticated user's quota.
|
||||
func (s *UserService) ListQuotaArtifacts(ctx context.Context) ([]types.QuotaUsedArtifact, error) {
|
||||
return ListAll[types.QuotaUsedArtifact](ctx, s.client, "/api/v1/user/quota/artifacts", nil)
|
||||
|
|
|
|||
106
users_test.go
106
users_test.go
|
|
@ -5,6 +5,7 @@ import (
|
|||
json "github.com/goccy/go-json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/forge/types"
|
||||
|
|
@ -163,6 +164,111 @@ func TestUserService_GetQuota_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUserService_SearchUsersPage_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/users/search" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if got := r.URL.Query().Get("q"); got != "al" {
|
||||
t.Errorf("wrong q: %s", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("uid"); got != "7" {
|
||||
t.Errorf("wrong uid: %s", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("page"); got != "1" {
|
||||
t.Errorf("wrong page: %s", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("limit"); got != "50" {
|
||||
t.Errorf("wrong limit: %s", got)
|
||||
}
|
||||
w.Header().Set("X-Total-Count", "2")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": []*types.User{
|
||||
{ID: 1, UserName: "alice"},
|
||||
{ID: 2, UserName: "alex"},
|
||||
},
|
||||
"ok": true,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
result, err := f.Users.SearchUsersPage(context.Background(), "al", ListOptions{}, UserSearchOptions{UID: 7})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.TotalCount != 2 || result.Page != 1 || result.HasMore {
|
||||
t.Fatalf("got %#v", result)
|
||||
}
|
||||
if len(result.Items) != 2 || result.Items[0].UserName != "alice" || result.Items[1].UserName != "alex" {
|
||||
t.Fatalf("got %#v", result.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserService_SearchUsers_Good(t *testing.T) {
|
||||
var requests int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/v1/users/search" {
|
||||
t.Errorf("wrong path: %s", r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if got := r.URL.Query().Get("q"); got != "al" {
|
||||
t.Errorf("wrong q: %s", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("limit"); got != strconv.Itoa(50) {
|
||||
t.Errorf("wrong limit: %s", got)
|
||||
}
|
||||
|
||||
switch r.URL.Query().Get("page") {
|
||||
case "1":
|
||||
w.Header().Set("X-Total-Count", "3")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": []*types.User{
|
||||
{ID: 1, UserName: "alice"},
|
||||
{ID: 2, UserName: "alex"},
|
||||
},
|
||||
"ok": true,
|
||||
})
|
||||
case "2":
|
||||
w.Header().Set("X-Total-Count", "3")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": []*types.User{
|
||||
{ID: 3, UserName: "ally"},
|
||||
},
|
||||
"ok": true,
|
||||
})
|
||||
default:
|
||||
t.Fatalf("unexpected page %q", r.URL.Query().Get("page"))
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
var got []string
|
||||
for user, err := range f.Users.IterSearchUsers(context.Background(), "al") {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = append(got, user.UserName)
|
||||
}
|
||||
if requests != 2 {
|
||||
t.Fatalf("expected 2 requests, got %d", requests)
|
||||
}
|
||||
if len(got) != 3 || got[0] != "alice" || got[1] != "alex" || got[2] != "ally" {
|
||||
t.Fatalf("got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserService_ListQuotaArtifacts_Good(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue