Merge branch 'mg/memberships/spaces_list_filter' of _OKE5H2PQKOUfzFFDuD4FA/default/CODE/gitness (#368)

This commit is contained in:
Marko Gacesa 2023-08-28 16:27:25 +00:00 committed by Harness
commit e8c1ac6ba3
14 changed files with 276 additions and 74 deletions

View File

@ -58,12 +58,12 @@ import (
// Injectors from wire.go:
func initSystem(ctx context.Context, config *types.Config) (*server.System, error) {
principalUID := check.ProvidePrincipalUIDCheck()
databaseConfig := server.ProvideDatabaseConfig(config)
db, err := database.ProvideDatabase(ctx, databaseConfig)
if err != nil {
return nil, err
}
principalUID := check.ProvidePrincipalUIDCheck()
pathTransformation := store.ProvidePathTransformation()
pathStore := database.ProvidePathStore(db, pathTransformation)
pathCache := cache.ProvidePathCache(pathStore, pathTransformation)
@ -76,7 +76,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
principalUIDTransformation := store.ProvidePrincipalUIDTransformation()
principalStore := database.ProvidePrincipalStore(db, principalUIDTransformation)
tokenStore := database.ProvideTokenStore(db)
controller := user.NewController(principalUID, authorizer, principalStore, tokenStore, membershipStore)
controller := user.ProvideController(db, principalUID, authorizer, principalStore, tokenStore, membershipStore)
serviceController := service.NewController(principalUID, authorizer, principalStore)
bootstrapBootstrap := bootstrap.ProvideBootstrap(config, controller, serviceController)
authenticator := authn.ProvideAuthenticator(principalStore, tokenStore)

View File

@ -19,7 +19,7 @@ import (
func (c *Controller) MembershipList(ctx context.Context,
session *auth.Session,
spaceRef string,
opts types.MembershipFilter,
filter types.MembershipUserFilter,
) ([]types.MembershipUser, int64, error) {
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
if err != nil {
@ -34,17 +34,17 @@ func (c *Controller) MembershipList(ctx context.Context,
var membershipsCount int64
err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error {
memberships, err = c.membershipStore.ListUsers(ctx, space.ID, opts)
memberships, err = c.membershipStore.ListUsers(ctx, space.ID, filter)
if err != nil {
return fmt.Errorf("failed to list memberships for space: %w", err)
}
if opts.Page == 1 && len(memberships) < opts.Size {
if filter.Page == 1 && len(memberships) < filter.Size {
membershipsCount = int64(len(memberships))
return nil
}
membershipsCount, err = c.membershipStore.CountUsers(ctx, space.ID, opts)
membershipsCount, err = c.membershipStore.CountUsers(ctx, space.ID, filter)
if err != nil {
return fmt.Errorf("failed to count memberships for space: %w", err)
}

View File

@ -13,10 +13,12 @@ import (
"github.com/harness/gitness/types/check"
"github.com/harness/gitness/types/enum"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
)
type Controller struct {
db *sqlx.DB
principalUIDCheck check.PrincipalUID
authorizer authz.Authorizer
principalStore store.PrincipalStore
@ -25,6 +27,7 @@ type Controller struct {
}
func NewController(
db *sqlx.DB,
principalUIDCheck check.PrincipalUID,
authorizer authz.Authorizer,
principalStore store.PrincipalStore,
@ -32,6 +35,7 @@ func NewController(
membershipStore store.MembershipStore,
) *Controller {
return &Controller{
db: db,
principalUIDCheck: principalUIDCheck,
authorizer: authorizer,
principalStore: principalStore,

View File

@ -10,6 +10,7 @@ import (
apiauth "github.com/harness/gitness/internal/api/auth"
"github.com/harness/gitness/internal/auth"
"github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
@ -18,21 +19,42 @@ import (
func (c *Controller) MembershipSpaces(ctx context.Context,
session *auth.Session,
userUID string,
) ([]types.MembershipSpace, error) {
filter types.MembershipSpaceFilter,
) ([]types.MembershipSpace, int64, error) {
user, err := findUserFromUID(ctx, c.principalStore, userUID)
if err != nil {
return nil, fmt.Errorf("failed to find user by UID: %w", err)
return nil, 0, fmt.Errorf("failed to find user by UID: %w", err)
}
// Ensure principal has required permissions.
if err = apiauth.CheckUser(ctx, c.authorizer, session, user, enum.PermissionUserView); err != nil {
return nil, err
return nil, 0, err
}
membershipSpaces, err := c.membershipStore.ListSpaces(ctx, user.ID)
var membershipSpaces []types.MembershipSpace
var membershipsCount int64
err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error {
membershipSpaces, err = c.membershipStore.ListSpaces(ctx, user.ID, filter)
if err != nil {
return nil, fmt.Errorf("failed to list membership spaces for user: %w", err)
return fmt.Errorf("failed to list membership spaces for user: %w", err)
}
return membershipSpaces, nil
if filter.Page == 1 && len(membershipSpaces) < filter.Size {
membershipsCount = int64(len(membershipSpaces))
return nil
}
membershipsCount, err = c.membershipStore.CountSpaces(ctx, user.ID, filter)
if err != nil {
return fmt.Errorf("failed to count memberships for user: %w", err)
}
return nil
}, dbtx.TxDefaultReadOnly)
if err != nil {
return nil, 0, err
}
return membershipSpaces, membershipsCount, nil
}

View File

@ -10,14 +10,16 @@ import (
"github.com/harness/gitness/types/check"
"github.com/google/wire"
"github.com/jmoiron/sqlx"
)
// WireSet provides a wire set for this package.
var WireSet = wire.NewSet(
NewController,
ProvideController,
)
func ProvideController(
db *sqlx.DB,
principalUIDCheck check.PrincipalUID,
authorizer authz.Authorizer,
principalStore store.PrincipalStore,
@ -25,6 +27,7 @@ func ProvideController(
membershipStore store.MembershipStore,
) *Controller {
return NewController(
db,
principalUIDCheck,
authorizer,
principalStore,

View File

@ -24,7 +24,7 @@ func HandleMembershipList(spaceCtrl *space.Controller) http.HandlerFunc {
return
}
filter := request.ParseMembershipFilter(r)
filter := request.ParseMembershipUserFilter(r)
memberships, membershipsCount, err := spaceCtrl.MembershipList(ctx, session, spaceRef, filter)
if err != nil {

View File

@ -18,12 +18,15 @@ func HandleMembershipSpaces(userCtrl *user.Controller) http.HandlerFunc {
session, _ := request.AuthSessionFrom(ctx)
userUID := session.Principal.UID
membershipSpaces, err := userCtrl.MembershipSpaces(ctx, session, userUID)
filter := request.ParseMembershipSpaceFilter(r)
membershipSpaces, membershipSpaceCount, err := userCtrl.MembershipSpaces(ctx, session, userUID, filter)
if err != nil {
render.TranslatedUserError(w, err)
return
}
render.Pagination(r, w, filter.Page, filter.Size, int(membershipSpaceCount))
render.JSON(w, http.StatusOK, membershipSpaces)
}
}

View File

@ -115,7 +115,7 @@ var queryParameterQuerySpace = openapi3.ParameterOrRef{
},
}
var queryParameterSpaceMembers = openapi3.ParameterOrRef{
var queryParameterMembershipUsers = openapi3.ParameterOrRef{
Parameter: &openapi3.Parameter{
Name: request.QueryParamQuery,
In: openapi3.ParameterInQuery,
@ -129,7 +129,7 @@ var queryParameterSpaceMembers = openapi3.ParameterOrRef{
},
}
var queryParameterSortSpaceMembers = openapi3.ParameterOrRef{
var queryParameterSortMembershipUsers = openapi3.ParameterOrRef{
Parameter: &openapi3.Parameter{
Name: request.QueryParamSort,
In: openapi3.ParameterInQuery,
@ -138,8 +138,8 @@ var queryParameterSortSpaceMembers = openapi3.ParameterOrRef{
Schema: &openapi3.SchemaOrRef{
Schema: &openapi3.Schema{
Type: ptrSchemaType(openapi3.SchemaTypeString),
Default: ptrptr(enum.MembershipSortName),
Enum: enum.MembershipSort("").Enum(),
Default: ptrptr(enum.MembershipUserSortName),
Enum: enum.MembershipUserSort("").Enum(),
},
},
},
@ -370,8 +370,8 @@ func spaceOperations(reflector *openapi3.Reflector) {
opMembershipList.WithTags("space")
opMembershipList.WithMapOfAnything(map[string]interface{}{"operationId": "membershipList"})
opMembershipList.WithParameters(
queryParameterSpaceMembers,
queryParameterOrder, queryParameterSortSpaceMembers,
queryParameterMembershipUsers,
queryParameterOrder, queryParameterSortMembershipUsers,
queryParameterPage, queryParameterLimit)
_ = reflector.SetRequest(&opMembershipList, &struct {
spaceRequest

View File

@ -8,9 +8,12 @@ import (
"net/http"
"github.com/harness/gitness/internal/api/controller/user"
"github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/api/usererror"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/gotidy/ptr"
"github.com/swaggest/openapi-go/openapi3"
)
@ -18,6 +21,36 @@ type createTokenRequest struct {
user.CreateTokenInput
}
var queryParameterMembershipSpaces = openapi3.ParameterOrRef{
Parameter: &openapi3.Parameter{
Name: request.QueryParamQuery,
In: openapi3.ParameterInQuery,
Description: ptr.String("The substring by which the spaces the users is a member of are filtered."),
Required: ptr.Bool(false),
Schema: &openapi3.SchemaOrRef{
Schema: &openapi3.Schema{
Type: ptrSchemaType(openapi3.SchemaTypeString),
},
},
},
}
var queryParameterSortMembershipSpaces = openapi3.ParameterOrRef{
Parameter: &openapi3.Parameter{
Name: request.QueryParamSort,
In: openapi3.ParameterInQuery,
Description: ptr.String("The field by which the spaces the user is a member of are sorted."),
Required: ptr.Bool(false),
Schema: &openapi3.SchemaOrRef{
Schema: &openapi3.Schema{
Type: ptrSchemaType(openapi3.SchemaTypeString),
Default: ptrptr(enum.MembershipSpaceSortUID),
Enum: enum.MembershipSpaceSort("").Enum(),
},
},
},
}
// helper function that constructs the openapi specification
// for user account resources.
func buildUser(reflector *openapi3.Reflector) {
@ -48,6 +81,10 @@ func buildUser(reflector *openapi3.Reflector) {
opMemberSpaces := openapi3.Operation{}
opMemberSpaces.WithTags("user")
opMemberSpaces.WithMapOfAnything(map[string]interface{}{"operationId": "membershipSpaces"})
opMemberSpaces.WithParameters(
queryParameterMembershipSpaces,
queryParameterOrder, queryParameterSortMembershipSpaces,
queryParameterPage, queryParameterLimit)
_ = reflector.SetRequest(&opMemberSpaces, struct{}{}, http.MethodGet)
_ = reflector.SetJSONResponse(&opMemberSpaces, new([]types.MembershipSpace), http.StatusOK)
_ = reflector.SetJSONResponse(&opMemberSpaces, new(usererror.Error), http.StatusInternalServerError)

View File

@ -11,20 +11,34 @@ import (
"github.com/harness/gitness/types/enum"
)
// ParseMembershipSort extracts the membership sort parameter from the url.
func ParseMembershipSort(r *http.Request) enum.MembershipSort {
return enum.ParseMembershipSort(
// ParseMembershipUserSort extracts the membership sort parameter from the url.
func ParseMembershipUserSort(r *http.Request) enum.MembershipUserSort {
return enum.ParseMembershipUserSort(
r.URL.Query().Get(QueryParamSort),
)
}
// ParseMembershipFilter extracts the membership filter from the url.
func ParseMembershipFilter(r *http.Request) types.MembershipFilter {
return types.MembershipFilter{
Page: ParsePage(r),
Size: ParseLimit(r),
Query: ParseQuery(r),
Sort: ParseMembershipSort(r),
// ParseMembershipUserFilter extracts the membership filter from the url.
func ParseMembershipUserFilter(r *http.Request) types.MembershipUserFilter {
return types.MembershipUserFilter{
ListQueryFilter: ParseListQueryFilterFromRequest(r),
Sort: ParseMembershipUserSort(r),
Order: ParseOrder(r),
}
}
// ParseMembershipSpaceSort extracts the membership space sort parameter from the url.
func ParseMembershipSpaceSort(r *http.Request) enum.MembershipSpaceSort {
return enum.ParseMembershipSpaceSort(
r.URL.Query().Get(QueryParamSort),
)
}
// ParseMembershipSpaceFilter extracts the membership space filter from the url.
func ParseMembershipSpaceFilter(r *http.Request) types.MembershipSpaceFilter {
return types.MembershipSpaceFilter{
ListQueryFilter: ParseListQueryFilterFromRequest(r),
Sort: ParseMembershipSpaceSort(r),
Order: ParseOrder(r),
}
}

View File

@ -230,9 +230,10 @@ type (
Create(ctx context.Context, membership *types.Membership) error
Update(ctx context.Context, membership *types.Membership) error
Delete(ctx context.Context, key types.MembershipKey) error
CountUsers(ctx context.Context, spaceID int64, filter types.MembershipFilter) (int64, error)
ListUsers(ctx context.Context, spaceID int64, filter types.MembershipFilter) ([]types.MembershipUser, error)
ListSpaces(ctx context.Context, userID int64) ([]types.MembershipSpace, error)
CountUsers(ctx context.Context, spaceID int64, filter types.MembershipUserFilter) (int64, error)
ListUsers(ctx context.Context, spaceID int64, filter types.MembershipUserFilter) ([]types.MembershipUser, error)
CountSpaces(ctx context.Context, userID int64, filter types.MembershipSpaceFilter) (int64, error)
ListSpaces(ctx context.Context, userID int64, filter types.MembershipSpaceFilter) ([]types.MembershipSpace, error)
}
// TokenStore defines the token data storage.

View File

@ -183,7 +183,7 @@ func (s *MembershipStore) Delete(ctx context.Context, key types.MembershipKey) e
// CountUsers returns a number of users memberships that matches the provided filter.
func (s *MembershipStore) CountUsers(ctx context.Context,
spaceID int64,
filter types.MembershipFilter,
filter types.MembershipUserFilter,
) (int64, error) {
stmt := database.Builder.
Select("count(*)").
@ -191,11 +191,11 @@ func (s *MembershipStore) CountUsers(ctx context.Context,
InnerJoin("principals ON membership_principal_id = principal_id").
Where("membership_space_id = ?", spaceID)
stmt = prepareMembershipListUsersStmt(stmt, filter)
stmt = applyMembershipUserFilter(stmt, filter)
sql, args, err := stmt.ToSql()
if err != nil {
return 0, fmt.Errorf("failed to convert membership count query to sql: %w", err)
return 0, fmt.Errorf("failed to convert membership users count query to sql: %w", err)
}
db := dbtx.GetAccessor(ctx, s.db)
@ -203,7 +203,7 @@ func (s *MembershipStore) CountUsers(ctx context.Context,
var count int64
err = db.QueryRowContext(ctx, sql, args...).Scan(&count)
if err != nil {
return 0, database.ProcessSQLErrorf(err, "Failed executing membership count query")
return 0, database.ProcessSQLErrorf(err, "Failed executing membership users count query")
}
return count, nil
@ -212,7 +212,7 @@ func (s *MembershipStore) CountUsers(ctx context.Context,
// ListUsers returns a list of memberships for a space or a user.
func (s *MembershipStore) ListUsers(ctx context.Context,
spaceID int64,
filter types.MembershipFilter,
filter types.MembershipUserFilter,
) ([]types.MembershipUser, error) {
const columns = membershipColumns + "," + principalInfoCommonColumns
stmt := database.Builder.
@ -221,7 +221,7 @@ func (s *MembershipStore) ListUsers(ctx context.Context,
InnerJoin("principals ON membership_principal_id = principal_id").
Where("membership_space_id = ?", spaceID)
stmt = prepareMembershipListUsersStmt(stmt, filter)
stmt = applyMembershipUserFilter(stmt, filter)
stmt = stmt.Limit(database.Limit(filter.Size))
stmt = stmt.Offset(database.Offset(filter.Page, filter.Size))
@ -231,9 +231,9 @@ func (s *MembershipStore) ListUsers(ctx context.Context,
}
switch filter.Sort {
case enum.MembershipSortName:
case enum.MembershipUserSortName:
stmt = stmt.OrderBy("principal_display_name " + order.String())
case enum.MembershipSortCreated:
case enum.MembershipUserSortCreated:
stmt = stmt.OrderBy("membership_created " + order.String())
}
@ -258,9 +258,9 @@ func (s *MembershipStore) ListUsers(ctx context.Context,
return result, nil
}
func prepareMembershipListUsersStmt(
func applyMembershipUserFilter(
stmt squirrel.SelectBuilder,
opts types.MembershipFilter,
opts types.MembershipUserFilter,
) squirrel.SelectBuilder {
if opts.Query != "" {
searchTerm := "%%" + strings.ToLower(opts.Query) + "%%"
@ -270,9 +270,38 @@ func prepareMembershipListUsersStmt(
return stmt
}
func (s *MembershipStore) CountSpaces(ctx context.Context,
userID int64,
filter types.MembershipSpaceFilter,
) (int64, error) {
stmt := database.Builder.
Select("count(*)").
From("memberships").
InnerJoin("spaces ON spaces.space_id = membership_space_id").
Where("membership_principal_id = ?", userID)
stmt = applyMembershipSpaceFilter(stmt, filter)
sql, args, err := stmt.ToSql()
if err != nil {
return 0, fmt.Errorf("failed to convert membership spaces count query to sql: %w", err)
}
db := dbtx.GetAccessor(ctx, s.db)
var count int64
err = db.QueryRowContext(ctx, sql, args...).Scan(&count)
if err != nil {
return 0, database.ProcessSQLErrorf(err, "Failed executing membership spaces count query")
}
return count, nil
}
// ListSpaces returns a list of spaces in which the provided user is a member.
func (s *MembershipStore) ListSpaces(ctx context.Context,
userID int64,
filter types.MembershipSpaceFilter,
) ([]types.MembershipSpace, error) {
const columns = membershipColumns + "," + spaceColumnsForJoin
stmt := database.Builder.
@ -280,8 +309,25 @@ func (s *MembershipStore) ListSpaces(ctx context.Context,
From("memberships").
InnerJoin("spaces ON spaces.space_id = membership_space_id").
InnerJoin(`paths ON spaces.space_id=paths.path_space_id AND paths.path_is_primary=true`).
Where("membership_principal_id = ?", userID).
OrderBy("space_path asc")
Where("membership_principal_id = ?", userID)
stmt = applyMembershipSpaceFilter(stmt, filter)
stmt = stmt.Limit(database.Limit(filter.Size))
stmt = stmt.Offset(database.Offset(filter.Page, filter.Size))
order := filter.Order
if order == enum.OrderDefault {
order = enum.OrderAsc
}
switch filter.Sort {
case enum.MembershipSpaceSortUID:
stmt = stmt.OrderBy("space_uid " + order.String())
case enum.MembershipSpaceSortPath:
stmt = stmt.OrderBy("space_path " + order.String())
case enum.MembershipSpaceSortCreated:
stmt = stmt.OrderBy("membership_created " + order.String())
}
sql, args, err := stmt.ToSql()
if err != nil {
@ -303,6 +349,18 @@ func (s *MembershipStore) ListSpaces(ctx context.Context,
return result, nil
}
func applyMembershipSpaceFilter(
stmt squirrel.SelectBuilder,
opts types.MembershipSpaceFilter,
) squirrel.SelectBuilder {
if opts.Query != "" {
searchTerm := "%%" + strings.ToLower(opts.Query) + "%%"
stmt = stmt.Where("LOWER(space_uid) LIKE ?", searchTerm)
}
return stmt
}
func mapToMembership(m *membership) types.Membership {
return types.Membership{
MembershipKey: types.MembershipKey{

View File

@ -8,45 +8,100 @@ import (
"strings"
)
// MembershipSort represents membership sort order.
type MembershipSort string
// MembershipUserSort represents membership user sort order.
type MembershipUserSort string
// Order enumeration.
// MembershipUserSort enumeration.
const (
MembershipSortName = name
MembershipSortCreated = created
MembershipUserSortName MembershipUserSort = name
MembershipUserSortCreated MembershipUserSort = created
)
var membershipSorts = sortEnum([]MembershipSort{
MembershipSortName,
MembershipSortCreated,
var membershipUserSorts = sortEnum([]MembershipUserSort{
MembershipUserSortName,
MembershipUserSortCreated,
})
func (MembershipSort) Enum() []interface{} { return toInterfaceSlice(membershipSorts) }
func (s MembershipSort) Sanitize() (MembershipSort, bool) { return Sanitize(s, GetAllMembershipSorts) }
func GetAllMembershipSorts() ([]MembershipSort, MembershipSort) {
return membershipSorts, MembershipSortName
func (MembershipUserSort) Enum() []interface{} { return toInterfaceSlice(membershipUserSorts) }
func (s MembershipUserSort) Sanitize() (MembershipUserSort, bool) {
return Sanitize(s, GetAllMembershipUserSorts)
}
func GetAllMembershipUserSorts() ([]MembershipUserSort, MembershipUserSort) {
return membershipUserSorts, MembershipUserSortName
}
// ParseMembershipSort parses the membership sort attribute string
// ParseMembershipUserSort parses the membership user sort attribute string
// and returns the equivalent enumeration.
func ParseMembershipSort(s string) MembershipSort {
func ParseMembershipUserSort(s string) MembershipUserSort {
switch strings.ToLower(s) {
case name:
return MembershipSortName
return MembershipUserSortName
case created, createdAt:
return MembershipSortCreated
return MembershipUserSortCreated
default:
return MembershipSortName
return MembershipUserSortName
}
}
// String returns the string representation of the attribute.
func (s MembershipSort) String() string {
func (s MembershipUserSort) String() string {
switch s {
case MembershipSortName:
case MembershipUserSortName:
return name
case MembershipSortCreated:
case MembershipUserSortCreated:
return created
default:
return undefined
}
}
// MembershipSpaceSort represents membership space sort order.
type MembershipSpaceSort string
// MembershipSpaceSort enumeration.
const (
MembershipSpaceSortUID MembershipSpaceSort = uid
MembershipSpaceSortPath MembershipSpaceSort = path
MembershipSpaceSortCreated MembershipSpaceSort = created
)
var membershipSpaceSorts = sortEnum([]MembershipSpaceSort{
MembershipSpaceSortUID,
MembershipSpaceSortPath,
MembershipSpaceSortCreated,
})
func (MembershipSpaceSort) Enum() []interface{} { return toInterfaceSlice(membershipSpaceSorts) }
func (s MembershipSpaceSort) Sanitize() (MembershipSpaceSort, bool) {
return Sanitize(s, GetAllMembershipSpaceSorts)
}
func GetAllMembershipSpaceSorts() ([]MembershipSpaceSort, MembershipSpaceSort) {
return membershipSpaceSorts, MembershipSpaceSortPath
}
// ParseMembershipSpaceSort parses the membership space sort attribute string
// and returns the equivalent enumeration.
func ParseMembershipSpaceSort(s string) MembershipSpaceSort {
switch strings.ToLower(s) {
case name:
return MembershipSpaceSortUID
case path:
return MembershipSpaceSortPath
case created, createdAt:
return MembershipSpaceSortCreated
default:
return MembershipSpaceSortUID
}
}
// String returns the string representation of the attribute.
func (s MembershipSpaceSort) String() string {
switch s {
case MembershipSpaceSortUID:
return uid
case MembershipSpaceSortPath:
return path
case MembershipSpaceSortCreated:
return created
default:
return undefined

View File

@ -32,6 +32,13 @@ type MembershipUser struct {
AddedBy PrincipalInfo `json:"added_by"`
}
// MembershipUserFilter holds membership user query parameters.
type MembershipUserFilter struct {
ListQueryFilter
Sort enum.MembershipUserSort `json:"sort"`
Order enum.Order `json:"order"`
}
// MembershipSpace adds space info to the Membership data.
type MembershipSpace struct {
Membership
@ -39,11 +46,9 @@ type MembershipSpace struct {
AddedBy PrincipalInfo `json:"added_by"`
}
// MembershipFilter holds membership query parameters.
type MembershipFilter struct {
Page int `json:"page"`
Size int `json:"size"`
Query string `json:"query"`
Sort enum.MembershipSort `json:"sort"`
// MembershipSpaceFilter holds membership space query parameters.
type MembershipSpaceFilter struct {
ListQueryFilter
Sort enum.MembershipSpaceSort `json:"sort"`
Order enum.Order `json:"order"`
}