memos/server/router/api/v1/common.go
boojack 4bc3928029 fix(user): implement ListUsers pagination
Rework ListUsers to match the ListMemos pagination contract: opaque
PageToken, normalizePageSize, DB-level limit+1 look-ahead, and a
next_page_token. Adds Offset to store.FindUser with an OFFSET clause in
all three dialects, and an `id DESC` ORDER BY tiebreaker so offset pages
stay stable when created_ts ties.

Also align pagination across list endpoints:
- Bump DefaultPageSize 10 -> 50 to match the documented default; use
  normalizePageSize in ListAttachments.
- Remove the never-implemented total_size field from all six list
  responses (ListUsers, ListAttachments, ListMemoComments,
  ListMemoReactions, ListUserSettings, ListPersonalAccessTokens) and
  regenerate.
- useListUsers now pages through next_page_token so the admin members
  view still loads every user past the default page size.
2026-07-12 20:51:04 +08:00

83 lines
1.9 KiB
Go

package v1
import (
"encoding/base64"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/store"
)
const (
// DefaultPageSize is the default page size for requests.
DefaultPageSize = 50
// MaxPageSize is the maximum page size for requests.
MaxPageSize = 1000
)
func convertStateFromStore(rowStatus store.RowStatus) v1pb.State {
switch rowStatus {
case store.Normal:
return v1pb.State_NORMAL
case store.Archived:
return v1pb.State_ARCHIVED
default:
return v1pb.State_STATE_UNSPECIFIED
}
}
func convertStateToStore(state v1pb.State) store.RowStatus {
switch state {
case v1pb.State_ARCHIVED:
return store.Archived
default:
return store.Normal
}
}
func getPageToken(limit int, offset int) (string, error) {
return marshalPageToken(&v1pb.PageToken{
Limit: int32(limit),
Offset: int32(offset),
})
}
func normalizePageSize(pageSize int32) int {
limit := int(pageSize)
if limit <= 0 {
return DefaultPageSize
}
if limit > MaxPageSize {
return MaxPageSize
}
return limit
}
func marshalPageToken(pageToken *v1pb.PageToken) (string, error) {
b, err := proto.Marshal(pageToken)
if err != nil {
return "", errors.Wrapf(err, "failed to marshal page token")
}
return base64.StdEncoding.EncodeToString(b), nil
}
func unmarshalPageToken(s string, pageToken *v1pb.PageToken) error {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return errors.Wrapf(err, "failed to decode page token")
}
if err := proto.Unmarshal(b, pageToken); err != nil {
return errors.Wrapf(err, "failed to unmarshal page token")
}
return nil
}
func isSuperUser(user *store.User) bool {
return user.Role == store.RoleAdmin
}
func canModifyMemo(user *store.User, memo *store.Memo) bool {
return user != nil && memo != nil && (memo.CreatorID == user.ID || isSuperUser(user))
}