mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add new group members endpoint with filtering and pagination (#23067)
Partially addresses #21813 (still need to make changes to the "add user" button to be complete) Since there are a lot of user tests already, I moved them into `coderdtest` to be shared.
This commit is contained in:
@@ -234,6 +234,34 @@ func ReducedUsersFromGroupMembers(members []database.GroupMember) []codersdk.Red
|
||||
return slice.List(members, ReducedUserFromGroupMember)
|
||||
}
|
||||
|
||||
func UserFromGroupMemberRow(member database.GetGroupMembersByGroupIDPaginatedRow) database.User {
|
||||
return database.User{
|
||||
ID: member.UserID,
|
||||
Email: member.UserEmail,
|
||||
Username: member.UserUsername,
|
||||
HashedPassword: member.UserHashedPassword,
|
||||
CreatedAt: member.UserCreatedAt,
|
||||
UpdatedAt: member.UserUpdatedAt,
|
||||
Status: member.UserStatus,
|
||||
RBACRoles: member.UserRbacRoles,
|
||||
LoginType: member.UserLoginType,
|
||||
AvatarURL: member.UserAvatarUrl,
|
||||
Deleted: member.UserDeleted,
|
||||
LastSeenAt: member.UserLastSeenAt,
|
||||
QuietHoursSchedule: member.UserQuietHoursSchedule,
|
||||
Name: member.UserName,
|
||||
GithubComUserID: member.UserGithubComUserID,
|
||||
}
|
||||
}
|
||||
|
||||
func ReducedUserFromGroupMemberRow(member database.GetGroupMembersByGroupIDPaginatedRow) codersdk.ReducedUser {
|
||||
return ReducedUser(UserFromGroupMemberRow(member))
|
||||
}
|
||||
|
||||
func ReducedUsersFromGroupMemberRows(members []database.GetGroupMembersByGroupIDPaginatedRow) []codersdk.ReducedUser {
|
||||
return slice.List(members, ReducedUserFromGroupMemberRow)
|
||||
}
|
||||
|
||||
func ReducedUsers(users []database.User) []codersdk.ReducedUser {
|
||||
return slice.List(users, ReducedUser)
|
||||
}
|
||||
|
||||
@@ -2892,6 +2892,10 @@ func (q *querier) GetGroupMembersByGroupID(ctx context.Context, arg database.Get
|
||||
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetGroupMembersByGroupID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetGroupMembersByGroupIDPaginated(ctx context.Context, arg database.GetGroupMembersByGroupIDPaginatedParams) ([]database.GetGroupMembersByGroupIDPaginatedRow, error) {
|
||||
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetGroupMembersByGroupIDPaginated)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetGroupMembersCountByGroupID(ctx context.Context, arg database.GetGroupMembersCountByGroupIDParams) (int64, error) {
|
||||
if _, err := q.GetGroupByID(ctx, arg.GroupID); err != nil { // AuthZ check
|
||||
return 0, err
|
||||
|
||||
@@ -1188,6 +1188,15 @@ func (s *MethodTestSuite) TestGroup() {
|
||||
check.Args(arg).Asserts(gm, policy.ActionRead)
|
||||
}))
|
||||
|
||||
s.Run("GetGroupMembersByGroupIDPaginated", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
g := testutil.Fake(s.T(), faker, database.Group{})
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
gm := testutil.Fake(s.T(), faker, database.GetGroupMembersByGroupIDPaginatedRow{GroupID: g.ID, UserID: u.ID})
|
||||
arg := database.GetGroupMembersByGroupIDPaginatedParams{GroupID: g.ID, IncludeSystem: false}
|
||||
dbm.EXPECT().GetGroupMembersByGroupIDPaginated(gomock.Any(), arg).Return([]database.GetGroupMembersByGroupIDPaginatedRow{gm}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(gm, policy.ActionRead)
|
||||
}))
|
||||
|
||||
s.Run("GetGroupMembersCountByGroupID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
g := testutil.Fake(s.T(), faker, database.Group{})
|
||||
arg := database.GetGroupMembersCountByGroupIDParams{GroupID: g.ID, IncludeSystem: false}
|
||||
|
||||
@@ -1472,6 +1472,14 @@ func (m queryMetricsStore) GetGroupMembersByGroupID(ctx context.Context, arg dat
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetGroupMembersByGroupIDPaginated(ctx context.Context, arg database.GetGroupMembersByGroupIDPaginatedParams) ([]database.GetGroupMembersByGroupIDPaginatedRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetGroupMembersByGroupIDPaginated(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetGroupMembersByGroupIDPaginated").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetGroupMembersByGroupIDPaginated").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetGroupMembersCountByGroupID(ctx context.Context, arg database.GetGroupMembersCountByGroupIDParams) (int64, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetGroupMembersCountByGroupID(ctx, arg)
|
||||
|
||||
@@ -2704,6 +2704,21 @@ func (mr *MockStoreMockRecorder) GetGroupMembersByGroupID(ctx, arg any) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembersByGroupID", reflect.TypeOf((*MockStore)(nil).GetGroupMembersByGroupID), ctx, arg)
|
||||
}
|
||||
|
||||
// GetGroupMembersByGroupIDPaginated mocks base method.
|
||||
func (m *MockStore) GetGroupMembersByGroupIDPaginated(ctx context.Context, arg database.GetGroupMembersByGroupIDPaginatedParams) ([]database.GetGroupMembersByGroupIDPaginatedRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetGroupMembersByGroupIDPaginated", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.GetGroupMembersByGroupIDPaginatedRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetGroupMembersByGroupIDPaginated indicates an expected call of GetGroupMembersByGroupIDPaginated.
|
||||
func (mr *MockStoreMockRecorder) GetGroupMembersByGroupIDPaginated(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembersByGroupIDPaginated", reflect.TypeOf((*MockStore)(nil).GetGroupMembersByGroupIDPaginated), ctx, arg)
|
||||
}
|
||||
|
||||
// GetGroupMembersCountByGroupID mocks base method.
|
||||
func (m *MockStore) GetGroupMembersCountByGroupID(ctx context.Context, arg database.GetGroupMembersCountByGroupIDParams) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -393,6 +393,10 @@ func (gm GroupMember) RBACObject() rbac.Object {
|
||||
return rbac.ResourceGroupMember.WithID(gm.UserID).InOrg(gm.OrganizationID).WithOwner(gm.UserID.String())
|
||||
}
|
||||
|
||||
func (gm GetGroupMembersByGroupIDPaginatedRow) RBACObject() rbac.Object {
|
||||
return rbac.ResourceGroupMember.WithID(gm.UserID).InOrg(gm.OrganizationID).WithOwner(gm.UserID.String())
|
||||
}
|
||||
|
||||
// PrebuiltWorkspaceResource defines the interface for types that can be identified as prebuilt workspaces
|
||||
// and converted to their corresponding prebuilt workspace RBAC object.
|
||||
type PrebuiltWorkspaceResource interface {
|
||||
|
||||
@@ -297,6 +297,7 @@ type sqlcQuerier interface {
|
||||
GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error)
|
||||
GetGroupMembers(ctx context.Context, includeSystem bool) ([]GroupMember, error)
|
||||
GetGroupMembersByGroupID(ctx context.Context, arg GetGroupMembersByGroupIDParams) ([]GroupMember, error)
|
||||
GetGroupMembersByGroupIDPaginated(ctx context.Context, arg GetGroupMembersByGroupIDPaginatedParams) ([]GetGroupMembersByGroupIDPaginatedRow, error)
|
||||
// Returns the total count of members in a group. Shows the total
|
||||
// count even if the caller does not have read access to ResourceGroupMember.
|
||||
// They only need ResourceGroup read access.
|
||||
|
||||
@@ -7404,6 +7404,212 @@ func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, arg GetGroupM
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getGroupMembersByGroupIDPaginated = `-- name: GetGroupMembersByGroupIDPaginated :many
|
||||
SELECT
|
||||
user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, organization_id, group_name, group_id, COUNT(*) OVER() AS count
|
||||
FROM
|
||||
group_members_expanded
|
||||
WHERE
|
||||
group_members_expanded.group_id = $1
|
||||
AND CASE
|
||||
-- This allows using the last element on a page as effectively a cursor.
|
||||
-- This is an important option for scripts that need to paginate without
|
||||
-- duplicating or missing data.
|
||||
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
|
||||
-- The pagination cursor is the last ID of the previous page.
|
||||
-- The query is ordered by the username field, so select all
|
||||
-- rows after the cursor.
|
||||
(LOWER(user_username)) > (
|
||||
SELECT
|
||||
LOWER(user_username)
|
||||
FROM
|
||||
group_members_expanded
|
||||
WHERE
|
||||
group_id = $1
|
||||
AND user_id = $2
|
||||
)
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Start filters
|
||||
-- Filter by email or username
|
||||
AND CASE
|
||||
WHEN $3 :: text != '' THEN (
|
||||
user_email ILIKE concat('%', $3, '%')
|
||||
OR user_username ILIKE concat('%', $3, '%')
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by name (display name)
|
||||
AND CASE
|
||||
WHEN $4 :: text != '' THEN
|
||||
user_name ILIKE concat('%', $4, '%')
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by status
|
||||
AND CASE
|
||||
-- @status needs to be a text because it can be empty, If it was
|
||||
-- user_status enum, it would not.
|
||||
WHEN cardinality($5 :: user_status[]) > 0 THEN
|
||||
user_status = ANY($5 :: user_status[])
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by rbac_roles
|
||||
AND CASE
|
||||
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
|
||||
-- everyone is a member.
|
||||
WHEN cardinality($6 :: text[]) > 0 AND 'member' != ANY($6 :: text[]) THEN
|
||||
user_rbac_roles && $6 :: text[]
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by last_seen
|
||||
AND CASE
|
||||
WHEN $7 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_last_seen_at <= $7
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_last_seen_at >= $8
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by created_at
|
||||
AND CASE
|
||||
WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_created_at <= $9
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_created_at >= $10
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by system type
|
||||
AND CASE
|
||||
WHEN $11::bool THEN TRUE
|
||||
ELSE user_is_system = false
|
||||
END
|
||||
AND CASE
|
||||
WHEN $12 :: bigint != 0 THEN
|
||||
user_github_com_user_id = $12
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by login_type
|
||||
AND CASE
|
||||
WHEN cardinality($13 :: login_type[]) > 0 THEN
|
||||
user_login_type = ANY($13 :: login_type[])
|
||||
ELSE true
|
||||
END
|
||||
-- End of filters
|
||||
ORDER BY
|
||||
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
|
||||
LOWER(user_username) ASC OFFSET $14
|
||||
LIMIT
|
||||
-- A null limit means "no limit", so 0 means return all
|
||||
NULLIF($15 :: int, 0)
|
||||
`
|
||||
|
||||
type GetGroupMembersByGroupIDPaginatedParams struct {
|
||||
GroupID uuid.UUID `db:"group_id" json:"group_id"`
|
||||
AfterID uuid.UUID `db:"after_id" json:"after_id"`
|
||||
Search string `db:"search" json:"search"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Status []UserStatus `db:"status" json:"status"`
|
||||
RbacRole []string `db:"rbac_role" json:"rbac_role"`
|
||||
LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"`
|
||||
LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
|
||||
CreatedBefore time.Time `db:"created_before" json:"created_before"`
|
||||
CreatedAfter time.Time `db:"created_after" json:"created_after"`
|
||||
IncludeSystem bool `db:"include_system" json:"include_system"`
|
||||
GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"`
|
||||
LoginType []LoginType `db:"login_type" json:"login_type"`
|
||||
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
|
||||
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
||||
}
|
||||
|
||||
type GetGroupMembersByGroupIDPaginatedRow struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
UserEmail string `db:"user_email" json:"user_email"`
|
||||
UserUsername string `db:"user_username" json:"user_username"`
|
||||
UserHashedPassword []byte `db:"user_hashed_password" json:"user_hashed_password"`
|
||||
UserCreatedAt time.Time `db:"user_created_at" json:"user_created_at"`
|
||||
UserUpdatedAt time.Time `db:"user_updated_at" json:"user_updated_at"`
|
||||
UserStatus UserStatus `db:"user_status" json:"user_status"`
|
||||
UserRbacRoles []string `db:"user_rbac_roles" json:"user_rbac_roles"`
|
||||
UserLoginType LoginType `db:"user_login_type" json:"user_login_type"`
|
||||
UserAvatarUrl string `db:"user_avatar_url" json:"user_avatar_url"`
|
||||
UserDeleted bool `db:"user_deleted" json:"user_deleted"`
|
||||
UserLastSeenAt time.Time `db:"user_last_seen_at" json:"user_last_seen_at"`
|
||||
UserQuietHoursSchedule string `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
|
||||
UserName string `db:"user_name" json:"user_name"`
|
||||
UserGithubComUserID sql.NullInt64 `db:"user_github_com_user_id" json:"user_github_com_user_id"`
|
||||
UserIsSystem bool `db:"user_is_system" json:"user_is_system"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
GroupName string `db:"group_name" json:"group_name"`
|
||||
GroupID uuid.UUID `db:"group_id" json:"group_id"`
|
||||
Count int64 `db:"count" json:"count"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetGroupMembersByGroupIDPaginated(ctx context.Context, arg GetGroupMembersByGroupIDPaginatedParams) ([]GetGroupMembersByGroupIDPaginatedRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getGroupMembersByGroupIDPaginated,
|
||||
arg.GroupID,
|
||||
arg.AfterID,
|
||||
arg.Search,
|
||||
arg.Name,
|
||||
pq.Array(arg.Status),
|
||||
pq.Array(arg.RbacRole),
|
||||
arg.LastSeenBefore,
|
||||
arg.LastSeenAfter,
|
||||
arg.CreatedBefore,
|
||||
arg.CreatedAfter,
|
||||
arg.IncludeSystem,
|
||||
arg.GithubComUserID,
|
||||
pq.Array(arg.LoginType),
|
||||
arg.OffsetOpt,
|
||||
arg.LimitOpt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetGroupMembersByGroupIDPaginatedRow
|
||||
for rows.Next() {
|
||||
var i GetGroupMembersByGroupIDPaginatedRow
|
||||
if err := rows.Scan(
|
||||
&i.UserID,
|
||||
&i.UserEmail,
|
||||
&i.UserUsername,
|
||||
&i.UserHashedPassword,
|
||||
&i.UserCreatedAt,
|
||||
&i.UserUpdatedAt,
|
||||
&i.UserStatus,
|
||||
pq.Array(&i.UserRbacRoles),
|
||||
&i.UserLoginType,
|
||||
&i.UserAvatarUrl,
|
||||
&i.UserDeleted,
|
||||
&i.UserLastSeenAt,
|
||||
&i.UserQuietHoursSchedule,
|
||||
&i.UserName,
|
||||
&i.UserGithubComUserID,
|
||||
&i.UserIsSystem,
|
||||
&i.OrganizationID,
|
||||
&i.GroupName,
|
||||
&i.GroupID,
|
||||
&i.Count,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getGroupMembersCountByGroupID = `-- name: GetGroupMembersCountByGroupID :one
|
||||
SELECT COUNT(*)
|
||||
FROM group_members_expanded
|
||||
|
||||
@@ -17,6 +17,110 @@ WHERE group_id = @group_id
|
||||
user_is_system = false
|
||||
END;
|
||||
|
||||
-- name: GetGroupMembersByGroupIDPaginated :many
|
||||
SELECT
|
||||
*, COUNT(*) OVER() AS count
|
||||
FROM
|
||||
group_members_expanded
|
||||
WHERE
|
||||
group_members_expanded.group_id = @group_id
|
||||
AND CASE
|
||||
-- This allows using the last element on a page as effectively a cursor.
|
||||
-- This is an important option for scripts that need to paginate without
|
||||
-- duplicating or missing data.
|
||||
WHEN @after_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
|
||||
-- The pagination cursor is the last ID of the previous page.
|
||||
-- The query is ordered by the username field, so select all
|
||||
-- rows after the cursor.
|
||||
(LOWER(user_username)) > (
|
||||
SELECT
|
||||
LOWER(user_username)
|
||||
FROM
|
||||
group_members_expanded
|
||||
WHERE
|
||||
group_id = @group_id
|
||||
AND user_id = @after_id
|
||||
)
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Start filters
|
||||
-- Filter by email or username
|
||||
AND CASE
|
||||
WHEN @search :: text != '' THEN (
|
||||
user_email ILIKE concat('%', @search, '%')
|
||||
OR user_username ILIKE concat('%', @search, '%')
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by name (display name)
|
||||
AND CASE
|
||||
WHEN @name :: text != '' THEN
|
||||
user_name ILIKE concat('%', @name, '%')
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by status
|
||||
AND CASE
|
||||
-- @status needs to be a text because it can be empty, If it was
|
||||
-- user_status enum, it would not.
|
||||
WHEN cardinality(@status :: user_status[]) > 0 THEN
|
||||
user_status = ANY(@status :: user_status[])
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by rbac_roles
|
||||
AND CASE
|
||||
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
|
||||
-- everyone is a member.
|
||||
WHEN cardinality(@rbac_role :: text[]) > 0 AND 'member' != ANY(@rbac_role :: text[]) THEN
|
||||
user_rbac_roles && @rbac_role :: text[]
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by last_seen
|
||||
AND CASE
|
||||
WHEN @last_seen_before :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_last_seen_at <= @last_seen_before
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN @last_seen_after :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_last_seen_at >= @last_seen_after
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by created_at
|
||||
AND CASE
|
||||
WHEN @created_before :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_created_at <= @created_before
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN @created_after :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
||||
user_created_at >= @created_after
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by system type
|
||||
AND CASE
|
||||
WHEN @include_system::bool THEN TRUE
|
||||
ELSE user_is_system = false
|
||||
END
|
||||
AND CASE
|
||||
WHEN @github_com_user_id :: bigint != 0 THEN
|
||||
user_github_com_user_id = @github_com_user_id
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by login_type
|
||||
AND CASE
|
||||
WHEN cardinality(@login_type :: login_type[]) > 0 THEN
|
||||
user_login_type = ANY(@login_type :: login_type[])
|
||||
ELSE true
|
||||
END
|
||||
-- End of filters
|
||||
ORDER BY
|
||||
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
|
||||
LOWER(user_username) ASC OFFSET @offset_opt
|
||||
LIMIT
|
||||
-- A null limit means "no limit", so 0 means return all
|
||||
NULLIF(@limit_opt :: int, 0);
|
||||
|
||||
-- name: GetGroupMembersCountByGroupID :one
|
||||
-- Returns the total count of members in a group. Shows the total
|
||||
-- count even if the caller does not have read access to ResourceGroupMember.
|
||||
|
||||
Reference in New Issue
Block a user