feat: add filtering to org members (#23334)

Continuation of https://github.com/coder/coder/pull/23067

Add filtering to the paginated org member endpoint (pretty much the same
as what I did in the previous PR with group members, except there I also
had to add pagination since it was missing).
This commit is contained in:
Asher
2026-03-21 16:58:45 -08:00
committed by GitHub
parent 4b707515c0
commit 47daca6eea
14 changed files with 576 additions and 75 deletions
+100 -6
View File
@@ -5,7 +5,9 @@
-- - Use both to get a specific org member row
SELECT
sqlc.embed(organization_members),
users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles"
users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles",
users.last_seen_at, users.status, users.login_type,
users.created_at as user_created_at, users.updated_at as user_updated_at
FROM
organization_members
INNER JOIN
@@ -83,23 +85,115 @@ RETURNING *;
SELECT
sqlc.embed(organization_members),
users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles",
users.last_seen_at, users.status, users.login_type,
users.created_at as user_created_at, users.updated_at as user_updated_at,
COUNT(*) OVER() AS count
FROM
organization_members
INNER JOIN
INNER JOIN
users ON organization_members.user_id = users.id AND users.deleted = false
WHERE
-- Filter by organization id
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(users.username)) > (
SELECT
LOWER(users.username)
FROM
organization_members
INNER JOIN
users ON organization_members.user_id = users.id
WHERE
organization_members.user_id = @after_id
)
)
ELSE true
END
-- Start filters
-- Filter by organization id
AND CASE
WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
organization_id = @organization_id
ELSE true
END
-- Filter by system type
AND CASE WHEN @include_system::bool THEN TRUE ELSE is_system = false END
-- Filter by email or username
AND CASE
WHEN @search :: text != '' THEN (
users.email ILIKE concat('%', @search, '%')
OR users.username ILIKE concat('%', @search, '%')
)
ELSE true
END
-- Filter by name (display name)
AND CASE
WHEN @name :: text != '' THEN
users.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
users.status = ANY(@status :: user_status[])
ELSE true
END
-- Filter by global 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
users.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
users.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
users.last_seen_at >= @last_seen_after
ELSE true
END
-- Filter by created_at (user creation date, not date added to org)
AND CASE
WHEN @created_before :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
users.created_at <= @created_before
ELSE true
END
AND CASE
WHEN @created_after :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
users.created_at >= @created_after
ELSE true
END
-- Filter by system type
AND CASE
WHEN @include_system::bool THEN TRUE
ELSE users.is_system = false
END
-- Filter by github.com user ID
AND CASE
WHEN @github_com_user_id :: bigint != 0 THEN
users.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
users.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(username) ASC OFFSET @offset_opt
LOWER(users.username) ASC OFFSET @offset_opt
LIMIT
-- A null limit means "no limit", so 0 means return all
NULLIF(@limit_opt :: int, 0);