Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
3f0cb6d
Backend-independent, cursor-based pagination.
fisx Jun 6, 2025
80c94b2
Connect user groups api to wire subsystem.
fisx Jun 6, 2025
06943ee
[short detour]
fisx Jun 8, 2025
6a6c08d
Some unit tests for listing user groups.
fisx Jun 10, 2025
adb33f0
Integration test for listing user groups.
fisx Jun 10, 2025
573c63d
List groups in UserGroupStore.
fisx Jun 10, 2025
c0cd768
List groups in UserGroupSubsystem.
fisx Jun 10, 2025
c6a4558
Fix pagination, add roundtrip / golden tests.
fisx Jun 10, 2025
efcb843
wip
fisx Jun 25, 2025
7886287
wip
fisx Jun 26, 2025
202e8e6
wip
fisx Jun 29, 2025
f30d5f3
wip
fisx Jun 30, 2025
b0d1165
wip
fisx Jun 30, 2025
7ca4f6f
wip
fisx Jun 30, 2025
0222877
wip
fisx Jun 30, 2025
ed7630d
wip
fisx Jun 30, 2025
49878d9
Fix `make psql`.
fisx Jul 2, 2025
07ce808
Default instance for pagination state.
fisx Jul 2, 2025
aeedbc1
Pure function for compiling an sql query from a PaginationState.
fisx Jul 2, 2025
f7e8dd2
Complete postgres user group store interpreter.
fisx Jul 9, 2025
ff6758c
Clean up test effect constraints and stacks for user groups.
fisx Jul 9, 2025
0c35d71
Test comparing inmem and postgres interpreter for user groups. [poc]
fisx Jul 9, 2025
3077a83
Fix database schema.
fisx Jul 10, 2025
2d68073
Revert "Fix database schema."
fisx Jul 10, 2025
f990ea6
Fix hasql query for getUserGroups.
fisx Jul 10, 2025
59434a7
Fix test; write more tests for in-mem interpreter.
fisx Jul 10, 2025
660ba3c
Accomodate quickcheck (-threaded for test suite).
fisx Jul 10, 2025
ab4568e
stash
fisx Jul 10, 2025
034b496
TODO.
fisx Jul 10, 2025
5062241
Allow more postgres clients in docker-ephemeral.
fisx Jul 10, 2025
ad02822
Fix mock interpreter.
fisx Jul 10, 2025
8fc397e
Fix more tests.
fisx Jul 10, 2025
07bb9ac
Rm outdated TODO.
fisx Jul 11, 2025
a3418bc
More unit tests.
fisx Jul 11, 2025
8e985ac
Fix typo in sort function.
fisx Jul 11, 2025
f8d0b98
Comments.
fisx Jul 11, 2025
0e502e0
Give in-mem interpreter a sense of time.
fisx Jul 11, 2025
f6f1476
Cleanup.
fisx Jul 11, 2025
5d31b47
Pend POC unit test calling postgres (won't work on CI (yet)).
fisx Jul 11, 2025
4239d14
make sanitize-pr
fisx Jul 11, 2025
eb8fd7a
hlint, ormolu
fisx Jul 13, 2025
423ad78
tweak Makefile
fisx Jul 13, 2025
3ae3f0b
Fix servant route, bump version of new endpoint to 10.
fisx Jul 13, 2025
238f429
Fix integration test.
fisx Jul 13, 2025
a55083f
rm weed.
fisx Jul 13, 2025
29be8e1
More integration test coverage.
fisx Jul 14, 2025
b635dbc
Fix pagination query param parsing.
fisx Jul 14, 2025
4163475
rm stale TODO
fisx Jul 14, 2025
5cefb9c
Polish tests.
fisx Jul 14, 2025
ea52a15
Changelog.
fisx Jul 14, 2025
b93a7fb
make sanitize-pr
fisx Jul 15, 2025
5e5385c
More verbose parse error.
fisx Jul 15, 2025
f739abc
rm one impractical and one unnecessary TODO.
fisx Jul 15, 2025
8e79c00
Introduce offset as independent query param.
fisx Jul 15, 2025
7f130bf
Add unit test.
fisx Jul 16, 2025
23d5ef8
Make offset in pagination state mandatory.
fisx Jul 16, 2025
c7ac308
make sanitize-pr
fisx Jul 16, 2025
5f98abf
haddocks.
fisx Jul 21, 2025
b62c8d7
Use name, created_at, id instead of row number in pagination state.
fisx Jul 21, 2025
8112f50
rm api compatibility tests.
fisx Jul 21, 2025
e82b10e
...
fisx Jul 21, 2025
b02f6c4
nothing useful
fisx Jul 22, 2025
cb46380
Revert "nothing useful"
fisx Jul 22, 2025
d81b64b
Revert "Revert "nothing useful""
fisx Jul 22, 2025
0761cd8
...
fisx Jul 22, 2025
1126dc9
... (also ignore all query params if pagination state is given)
fisx Jul 22, 2025
267c94a
... (session)
fisx Jul 22, 2025
d027f82
... (get subsystem interpreter to compile)
fisx Jul 22, 2025
a3ff85f
... (get tests to compile) [WIP]
fisx Jul 22, 2025
385fb14
stash
fisx Jul 23, 2025
af5cebc
sql query generation
battermann Jul 23, 2025
71ef7a6
encode pagination state as base64
battermann Jul 23, 2025
909fb94
wip: fix a few tests
battermann Jul 23, 2025
eefef9e
wip: fix a few tests
fisx Jul 23, 2025
ab0652b
idea.
fisx Jul 23, 2025
bedb5a1
fix mock interpreter.
fisx Jul 24, 2025
b775386
fix test.
fisx Jul 24, 2025
5d26de2
Fix param schema instances.
fisx Jul 24, 2025
1c5c8bc
Don't make PaginationState serialization more efficient.
fisx Jul 24, 2025
91756e7
Do not fix camel vs. snake casing.
fisx Jul 24, 2025
9abc11f
Fix instances ToHttpApiData, ToSchema PaginationResult
fisx Jul 24, 2025
3523186
make sanitize-pr
fisx Jul 24, 2025
0948e28
Inline special-purpose functions.
fisx Jul 25, 2025
98bedc6
Fix integration test (pagination state encoding).
fisx Jul 25, 2025
fbe44b2
Fix merge commit.
fisx Jul 25, 2025
008bce9
Cleanup.
fisx Jul 25, 2025
6487610
Type UserGroupMeta that doesn't have a list of member ids.
fisx Jul 25, 2025
427db83
Enforce valid pagination states in ToSchema, Arbitrary.
fisx Jul 25, 2025
116fad9
Clean up postgres connection pool in unit tests.
fisx Jul 25, 2025
c56b85b
Haddocks.
fisx Jul 25, 2025
697306f
Add missing integration tests.
fisx Jul 25, 2025
075c33a
make sanitize-pr
fisx Jul 25, 2025
8c5e0b5
fix golden test
battermann Jul 29, 2025
c985d3a
refactor wire-api
battermann Jul 30, 2025
7f6f161
wip
battermann Jul 30, 2025
e169c5c
more wip
akshaymankar Jul 30, 2025
e81020a
Tests succeed
akshaymankar Jul 31, 2025
bb4ef43
Delete unused unsafe function
akshaymankar Jul 31, 2025
6d6ee17
Delete unused error
akshaymankar Jul 31, 2025
e44f71c
Move servant code to the route module
akshaymankar Jul 31, 2025
f2de6ab
Delete tests for postgresql queries
akshaymankar Jul 31, 2025
30f6beb
Delete unused unsafe function
akshaymankar Jul 31, 2025
d2d25fc
Unique name for UserGroupMeta in swagger
akshaymankar Jul 31, 2025
2ea5eff
Delete stale haddock
akshaymankar Jul 31, 2025
8b2fbc9
Rename `toText` to clearer function names
akshaymankar Jul 31, 2025
13e45ef
Docs for query params
akshaymankar Jul 31, 2025
a244ce6
clean up user group postrgres interpreter
battermann Jul 31, 2025
07c0d42
remove todo, it says above why
battermann Jul 31, 2025
8f31795
renaming
battermann Jul 31, 2025
7ea6e9e
remove todo
battermann Jul 31, 2025
a174a94
test all combinations of query parameters
battermann Jul 31, 2025
bd44919
fix errors introduced by rebasing
battermann Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ ingress-nginx-controller nginx-ingress-services reaper restund \
k8ssandra-test-cluster ldap-scim-bridge wire-server-enterprise
KIND_CLUSTER_NAME := wire-server
HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests
PSQL_DB ?= wire-server

package ?= all
EXE_SCHEMA := ./dist/$(package)-schema
Expand Down Expand Up @@ -343,7 +344,8 @@ cqlsh:
psql:
@grep -q wire-server:wire-server ~/.pgpass || \
echo "consider running 'echo localhost:5432:wire-server:wire-server:posty-the-gres > ~/.pgpass ; chmod 600 ~/.pgpass '"
psql -h localhost -p 5432 -U wire-server -w
psql -h localhost -p 5432 -d $(PSQL_DB) -U wire-server -w || \
echo 'if the database is missing, consider running "make postgres-reset", or setting $$PSQL_DB to the correct table space.'

.PHONY: db-reset-package
db-reset-package:
Expand Down
1 change: 1 addition & 0 deletions changelog.d/1-api-changes/WPB-17534-list-user-groups
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
New end-point `GET /user-groups?...` for filtering, sorting, and pagination.
1 change: 1 addition & 0 deletions deploy/dockerephemeral/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ services:
POSTGRES_PASSWORD: "posty-the-gres"
POSTGRES_USER: "wire-server"
POSTGRES_DB: "backendA"
command: postgres -c max_connections=50

cassandra:
container_name: demo_wire_cassandra
Expand Down
33 changes: 29 additions & 4 deletions integration/test/API/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1056,10 +1056,35 @@ getUserGroup user gid = do
req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid]
submit "GET" req

getUserGroups :: (MakesValue user) => user -> Maybe Int -> Maybe String -> App Response
getUserGroups user mbLimit mbLastKey = do
req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups"]
submit "GET" $ req & addQueryParams (catMaybes [(("limit",) . show) <$> mbLimit, ("last_key",) <$> mbLastKey])
data GetUserGroupsArgs = GetUserGroupsArgs
{ q :: Maybe String,
sortByKeys :: Maybe String,
sortOrder :: Maybe String,
pSize :: Maybe Int,
lastName :: Maybe String,
lastCreatedAt :: Maybe String,
lastId :: Maybe String
}

instance Default GetUserGroupsArgs where
def = GetUserGroupsArgs Nothing Nothing Nothing Nothing Nothing Nothing Nothing

getUserGroups :: (MakesValue user) => user -> GetUserGroupsArgs -> App Response
getUserGroups user GetUserGroupsArgs {..} = do
req <- baseRequest user Brig Versioned "user-groups"
submit "GET" $
req
& addQueryParams
( catMaybes
[ ("q",) <$> q,
("sort_by",) <$> sortByKeys,
("sort_order",) <$> sortOrder,
(("page_size",) . show) <$> pSize,
("last_seen_name",) <$> lastName,
("last_seen_created_at",) <$> lastCreatedAt,
("last_seen_id",) <$> lastId
]
)

updateUserGroup :: (MakesValue user, MakesValue userGroupUpdate) => user -> String -> userGroupUpdate -> App Response
updateUserGroup user gid userGroupUpdate = do
Expand Down
183 changes: 183 additions & 0 deletions integration/test/Test/UserGroup.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Test.UserGroup where

import API.Brig
import API.Galley
import Control.Error (lastMay)
import Notifications (isUserGroupCreatedNotif)
import SetupHelpers
import Testlib.Prelude
Expand Down Expand Up @@ -60,6 +61,10 @@ testUserGroupSmoke = do
resp.json %. "name" `shouldMatch` "also good"
resp.json %. "members" `shouldMatch` [mem2id, mem3id]

bindResponse (getUserGroups owner def) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. "page.0.name" `shouldMatch` "also good"

bindResponse (deleteUserGroup owner badGid) $ \resp -> do
resp.status `shouldMatchInt` 404

Expand All @@ -74,3 +79,181 @@ testUserGroupSmoke = do

bindResponse (removeUserFromGroup owner gid mem1id) $ \resp -> do
resp.status `shouldMatchInt` 404

testUserGroupGetGroups :: (HasCallStack) => App ()
testUserGroupGetGroups = do
(owner, _team, []) <- createTeam OwnDomain 1

let groupNames = ["First group", "CC", "CCC"] <> ((: []) <$> ['A' .. 'G'])
forM_ groupNames $ \gname -> do
let newGroup = object ["name" .= gname, "members" .= ([] :: [()])]
bindResponse (createUserGroup owner newGroup) $ \resp -> do
resp.status `shouldMatchInt` 200
resp.json %. "name" `shouldMatch` gname
resp.json %. "members" `shouldMatch` ([] :: [()])

-- Default sort by is createdAt, and sortOrder is DESC
_ <- runSearch owner def {q = Just "C"} ["C", "CCC", "CC"]

-- Default sortOrder is DESC, regardless of sortBy
_ <- runSearch owner def {q = Just "CC", sortByKeys = Just "name"} ["CCC", "CC"]

-- Test combinations of sortBy and sortOrder:
_ <-
runSearch
owner
def {sortByKeys = Just "name", sortOrder = Just "asc"}
[ "A",
"B",
"C",
"CC",
"CCC",
"D",
"E",
"F",
"First group",
"G"
]
_ <-
runSearch
owner
def {sortByKeys = Just "name", sortOrder = Just "desc"}
( reverse
[ "A",
"B",
"C",
"CC",
"CCC",
"D",
"E",
"F",
"First group",
"G"
]
)
_ <-
runSearch
owner
def {sortByKeys = Just "created_at", sortOrder = Just "asc"}
[ "First group",
"CC",
"CCC",
"A",
"B",
"C",
"D",
"E",
"F",
"G"
]
_ <-
runSearch
owner
def {sortByKeys = Just "created_at", sortOrder = Just "desc"}
( reverse
[ "First group",
"CC",
"CCC",
"A",
"B",
"C",
"D",
"E",
"F",
"G"
]
)

-- Test sorting and filtering works across pages
let firstPageParams = def {sortByKeys = Just "name", sortOrder = Just "desc", pSize = Just 3}
Just (name1, createdAt1, id1) <-
runSearch
owner
firstPageParams
[ "G",
"First group",
"F"
]
Just (name2, createdAt2, id2) <-
runSearch
owner
firstPageParams {lastName = Just name1, lastCreatedAt = Just createdAt1, lastId = Just id1}
[ "E",
"D",
"CCC"
]
Just (name3, createdAt3, id3) <-
runSearch
owner
firstPageParams {lastName = Just name2, lastCreatedAt = Just createdAt2, lastId = Just id2}
[ "CC",
"C",
"B"
]

void
$ runSearch
owner
firstPageParams {lastName = Just name3, lastCreatedAt = Just createdAt3, lastId = Just id3}
["A"]

runSearch :: (HasCallStack, MakesValue owner) => owner -> GetUserGroupsArgs -> [String] -> App (Maybe (String, String, String))
runSearch owner args expected =
bindResponse (getUserGroups owner args) $ \resp -> do
resp.status `shouldMatchInt` 200
found <- ((%. "name") `mapM`) =<< asList =<< resp.json %. "page"
found `shouldMatch` expected
results <- asList $ resp.json %. "page"
for (lastMay results) $ \lastGroup ->
(,,)
<$> asString (lastGroup %. "name")
<*> asString (lastGroup %. "createdAt")
<*> asString (lastGroup %. "id")

testUserGroupGetGroupsAllInputs :: (HasCallStack) => App ()
testUserGroupGetGroupsAllInputs = do
(owner, _team, []) <- createTeam OwnDomain 1
for_ ((: []) <$> ['A' .. 'Z']) $ \gname -> do
let newGroup = object ["name" .= gname, "members" .= ([] :: [()])]
createUserGroup owner newGroup >>= assertSuccess

Just (ln, ltz, lid) <- runSearch owner def {pSize = Just 3} ["Z", "Y", "X"]
let getUserGroupArgs = getUserGroupArgsCombinations ln ltz lid
for_ getUserGroupArgs $ \args -> do
bindResponse (getUserGroups owner args) $ \resp -> do
-- most important check is that all combinations return 200
resp.status `shouldMatchInt` 200
-- additionally we can check a few invariants
groups <- resp.json %. "page" >>= asList
case (args.q, args.lastName, args.lastCreatedAt, args.lastId) of
(Nothing, Nothing, Nothing, Nothing) -> length groups `shouldMatchInt` (fromMaybe 15 args.pSize)
(Just _, Nothing, Nothing, Nothing) -> length groups `shouldMatchInt` 1
_ -> pure ()
where
getUserGroupArgsCombinations :: String -> String -> String -> [GetUserGroupsArgs]
getUserGroupArgsCombinations ln ltz lid =
[ GetUserGroupsArgs
{ q = q',
sortByKeys = sortBy',
sortOrder = sortOrder',
pSize = pSize',
lastName = lastName',
lastCreatedAt = lastCreatedAt',
lastId = lastId'
}
| q' <- qs,
sortBy' <- sortByKeysList,
sortOrder' <- sortOrders,
pSize' <- pSizes,
lastName' <- lastNames,
lastCreatedAt' <- lastCreatedAts,
lastId' <- lastIds
]
where
qs = [Nothing, Just "A"]
sortByKeysList = [Nothing, Just "name", Just "created_at"]
sortOrders = [Nothing, Just "asc", Just "desc"]
pSizes = [Nothing, Just 3]
lastNames = [Nothing, Just ln]
lastCreatedAts = [Nothing, Just ltz]
lastIds = [Nothing, Just lid]
12 changes: 12 additions & 0 deletions libs/types-common/src/Data/Json/Util.hs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ newtype UTCTimeMillis = UTCTimeMillis {fromUTCTimeMillis :: UTCTime}
deriving (Eq, Ord, Generic)
deriving (FromJSON, ToJSON, S.ToSchema) via Schema UTCTimeMillis

instance S.ToParamSchema UTCTimeMillis

instance ToSchema UTCTimeMillis where
schema =
UTCTimeMillis
Expand Down Expand Up @@ -144,6 +146,16 @@ instance Show UTCTimeMillis where
instance BS.ToByteString UTCTimeMillis where
builder = BB.byteString . UTF8.fromString . show

instance ToHttpApiData UTCTimeMillis where
toUrlPiece = showUTCTimeMillis

instance FromHttpApiData UTCTimeMillis where
parseUrlPiece raw =
maybe (Left $ "Could not parse UTCTimeMillis: " <> raw) Right
. readUTCTimeMillis
. Text.unpack
$ raw

instance BS.FromByteString UTCTimeMillis where
parser = maybe (fail "UTCTimeMillis") pure . readUTCTimeMillis =<< BS.parser

Expand Down
22 changes: 22 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Public/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import Data.CommaSeparatedList (CommaSeparatedList)
import Data.Domain
import Data.Handle
import Data.Id as Id
import Data.Json.Util
import Data.Misc
import Data.Nonce (Nonce)
import Data.OpenApi hiding (Contact, Header, Schema, ToSchema)
Expand Down Expand Up @@ -82,6 +83,7 @@ import Wire.API.User.Password (CompletePasswordReset, NewPasswordReset, Password
import Wire.API.User.RichInfo (RichInfoAssocList)
import Wire.API.User.Search (Contact, PagingState, RoleFilter, SearchResult, TeamContact, TeamUserSearchSortBy, TeamUserSearchSortOrder)
import Wire.API.UserGroup
import Wire.API.UserGroup.Pagination
import Wire.API.UserMap

type BrigAPI =
Expand Down Expand Up @@ -290,6 +292,12 @@ type UserAPI =
(Respond 200 "Protocols supported by the user" (Set BaseProtocolTag))
)

type LastSeenNameDesc = Description "`name` of the last seen user group, used to get the next page when sorting by name."

type LastSeenCreatedAtDesc = Description "`created_at` field of the last seen user group, used to get the next page when sorting by created_at."

type LastSeenIdDesc = Description "`id` of the last seen group, used to get the next page. **Must** be sent to get the next page."

type UserGroupAPI =
Named
"create-user-group"
Expand All @@ -316,6 +324,20 @@ type UserGroupAPI =
]
(Maybe UserGroup)
)
:<|> Named
"get-user-groups"
( From 'V10
:> ZLocalUser
:> "user-groups"
:> QueryParam' '[Optional, Strict, Description "Search string"] "q" Text
:> QueryParam' '[Optional, Strict] "sort_by" SortBy
:> QueryParam' '[Optional, Strict] "sort_order" SortOrder
:> QueryParam' '[Optional, Strict] "page_size" PageSize
:> QueryParam' '[Optional, Strict, LastSeenNameDesc] "last_seen_name" UserGroupName
:> QueryParam' '[Optional, Strict, LastSeenCreatedAtDesc] "last_seen_created_at" UTCTimeMillis
:> QueryParam' '[Optional, Strict, LastSeenIdDesc] "last_seen_id" UserGroupId
:> Get '[JSON] UserGroupPage
)
:<|> Named
"update-user-group"
( From 'V10
Expand Down
Loading