Skip to content

Commit 618eed8

Browse files
authored
Merge pull request #79 from unisoncomputing/cp/rework-paging
Rework paging for all paged endpoints
2 parents d498fb6 + 9449750 commit 618eed8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+293
-194
lines changed

src/Share/Notifications/API.hs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module Share.Notifications.API
99
SubscriptionRoutes (..),
1010
EmailRoutes (..),
1111
WebhookRoutes (..),
12-
GetHubEntriesResponse (..),
12+
GetHubEntriesCursor,
1313
StatusFilter (..),
1414
UpdateHubEntriesRequest (..),
1515
GetSubscriptionsResponse (..),
@@ -42,6 +42,7 @@ import Share.IDs
4242
import Share.Notifications.Types (DeliveryMethodId, HydratedEvent, NotificationDeliveryMethod, NotificationHubEntry, NotificationStatus, NotificationSubscription, NotificationTopic, NotificationTopicGroup, SubscriptionFilter)
4343
import Share.OAuth.Session (AuthenticatedUserId)
4444
import Share.Prelude
45+
import Share.Utils.API (Cursor, Paged)
4546
import Share.Utils.URI (URIParam)
4647
import Share.Web.Share.DisplayInfo.Types (UnifiedDisplayInfo)
4748

@@ -209,20 +210,14 @@ instance FromJSON StatusFilter where
209210
Nothing -> fail "Empty status filter"
210211
Just statuses -> pure $ StatusFilter $ NESet.fromList statuses
211212

213+
type GetHubEntriesCursor = (UTCTime, NotificationHubEntryId)
214+
212215
type GetHubEntriesEndpoint =
213216
AuthenticatedUserId
214217
:> QueryParam "limit" Int
215-
:> QueryParam "after" UTCTime
218+
:> QueryParam "cursor" (Cursor GetHubEntriesCursor)
216219
:> QueryParam "status" StatusFilter
217-
:> Get '[JSON] GetHubEntriesResponse
218-
219-
data GetHubEntriesResponse = GetHubEntriesResponse
220-
{ notifications :: [NotificationHubEntry UnifiedDisplayInfo HydratedEvent]
221-
}
222-
223-
instance ToJSON GetHubEntriesResponse where
224-
toJSON GetHubEntriesResponse {notifications} =
225-
object ["notifications" .= notifications]
220+
:> Get '[JSON] (Paged GetHubEntriesCursor (NotificationHubEntry UnifiedDisplayInfo HydratedEvent))
226221

227222
type UpdateHubEntriesEndpoint =
228223
AuthenticatedUserId

src/Share/Notifications/Impl.hs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
module Share.Notifications.Impl (server) where
22

3-
import Control.Lens (forOf, traversed)
4-
import Data.Time
3+
import Control.Lens
54
import Servant
65
import Servant.Server.Generic (AsServerT)
76
import Share.IDs
7+
import Share.Notifications.API (GetHubEntriesCursor)
88
import Share.Notifications.API qualified as API
99
import Share.Notifications.Ops qualified as NotifOps
1010
import Share.Notifications.Queries qualified as NotificationQ
11+
import Share.Notifications.Types
1112
import Share.Postgres qualified as PG
1213
import Share.Postgres.Ops qualified as UserQ
1314
import Share.User (User (..))
15+
import Share.Utils.API (Cursor, Paged, pagedOn)
1416
import Share.Web.App
1517
import Share.Web.Authorization qualified as AuthZ
18+
import Share.Web.Share.DisplayInfo.Types (UnifiedDisplayInfo)
1619

1720
hubRoutes :: UserHandle -> API.HubEntriesRoutes (AsServerT WebApp)
1821
hubRoutes userHandle =
@@ -77,14 +80,22 @@ server userHandle =
7780
subscriptionsRoutes = subscriptionsRoutes userHandle
7881
}
7982

80-
getHubEntriesEndpoint :: UserHandle -> UserId -> Maybe Int -> Maybe UTCTime -> Maybe API.StatusFilter -> WebApp API.GetHubEntriesResponse
81-
getHubEntriesEndpoint userHandle callerUserId limit afterTime mayStatusFilter = do
83+
getHubEntriesEndpoint ::
84+
UserHandle ->
85+
UserId ->
86+
Maybe Int ->
87+
Maybe (Cursor GetHubEntriesCursor) ->
88+
Maybe API.StatusFilter ->
89+
WebApp (Paged GetHubEntriesCursor (NotificationHubEntry UnifiedDisplayInfo HydratedEvent))
90+
getHubEntriesEndpoint userHandle callerUserId limit mayCursor mayStatusFilter = do
8291
User {user_id = notificationUserId} <- UserQ.expectUserByHandle userHandle
8392
_authZReceipt <- AuthZ.permissionGuard $ AuthZ.checkNotificationsGet callerUserId notificationUserId
84-
notifications <- PG.runTransaction do
85-
notifs <- NotificationQ.listNotificationHubEntryPayloads notificationUserId limit afterTime (API.getStatusFilter <$> mayStatusFilter)
93+
notifications <- PG.runTransaction $ do
94+
notifs <- NotificationQ.listNotificationHubEntryPayloads notificationUserId limit mayCursor (API.getStatusFilter <$> mayStatusFilter)
8695
forOf (traversed . traversed) notifs NotifOps.hydrateEvent
87-
pure $ API.GetHubEntriesResponse {notifications}
96+
notifications
97+
& pagedOn (\(NotificationHubEntry {hubEntryId, hubEntryCreatedAt}) -> (hubEntryCreatedAt, hubEntryId))
98+
& pure
8899

89100
updateHubEntriesEndpoint :: UserHandle -> UserId -> API.UpdateHubEntriesRequest -> WebApp ()
90101
updateHubEntriesEndpoint userHandle callerUserId API.UpdateHubEntriesRequest {notificationStatus, notificationIds} = do

src/Share/Notifications/Queries.hs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@ import Control.Lens
2626
import Data.Foldable qualified as Foldable
2727
import Data.Ord (clamp)
2828
import Data.Set.NonEmpty (NESet)
29-
import Data.Time (UTCTime)
3029
import Share.Contribution
3130
import Share.IDs
31+
import Share.Notifications.API (GetHubEntriesCursor)
3232
import Share.Notifications.Types
3333
import Share.Postgres
3434
import Share.Postgres qualified as PG
3535
import Share.Postgres.Contributions.Queries qualified as ContributionQ
3636
import Share.Postgres.Users.Queries qualified as UsersQ
3737
import Share.Prelude
38+
import Share.Utils.API (Cursor (..), CursorDirection (..))
3839
import Share.Web.Share.DisplayInfo.Queries qualified as DisplayInfoQ
3940
import Share.Web.Share.DisplayInfo.Types (UnifiedDisplayInfo)
4041

@@ -55,19 +56,23 @@ expectEvent eventId = do
5556
WHERE id = #{eventId}
5657
|]
5758

58-
listNotificationHubEntryPayloads :: UserId -> Maybe Int -> Maybe UTCTime -> Maybe (NESet NotificationStatus) -> Transaction e [NotificationHubEntry UnifiedDisplayInfo HydratedEventPayload]
59-
listNotificationHubEntryPayloads notificationUserId mayLimit afterTime statusFilter = do
59+
listNotificationHubEntryPayloads :: UserId -> Maybe Int -> Maybe (Cursor GetHubEntriesCursor) -> Maybe (NESet NotificationStatus) -> Transaction e [NotificationHubEntry UnifiedDisplayInfo HydratedEventPayload]
60+
listNotificationHubEntryPayloads notificationUserId mayLimit mayCursor statusFilter = do
6061
let limit = clamp (0, 1000) . fromIntegral @Int @Int32 . fromMaybe 50 $ mayLimit
6162
let statusFilterList = Foldable.toList <$> statusFilter
63+
let cursorFilter = case mayCursor of
64+
Nothing -> mempty
65+
Just (Cursor (beforeTime, entryId) Previous) -> [PG.sql| AND (hub.created_at, hub.id) < (#{beforeTime}, #{entryId})|]
66+
Just (Cursor (afterTime, entryId) Next) -> [PG.sql| AND (hub.created_at, hub.id) > (#{afterTime}, #{entryId})|]
6267
dbNotifications <-
6368
queryListRows @(NotificationHubEntry UserId NotificationEventData)
6469
[sql|
65-
SELECT hub.id, hub.status, event.id, event.occurred_at, event.scope_user_id, event.actor_user_id, event.resource_id, event.topic, event.data
70+
SELECT hub.id, hub.status, hub.created_at, event.id, event.occurred_at, event.scope_user_id, event.actor_user_id, event.resource_id, event.topic, event.data
6671
FROM notification_hub_entries hub
6772
JOIN notification_events event ON hub.event_id = event.id
6873
WHERE hub.user_id = #{notificationUserId}
6974
AND (#{statusFilterList} IS NULL OR hub.status = ANY(#{statusFilterList}::notification_status[]))
70-
AND (#{afterTime} IS NULL OR event.occurred_at > #{afterTime})
75+
^{cursorFilter}
7176
ORDER BY hub.created_at DESC
7277
LIMIT #{limit}
7378
|]

src/Share/Notifications/Types.hs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,24 +390,27 @@ instance Aeson.ToJSON (NotificationSubscription NotificationSubscriptionId) wher
390390
data NotificationHubEntry userInfo eventPayload = NotificationHubEntry
391391
{ hubEntryId :: NotificationHubEntryId,
392392
hubEntryEvent :: NotificationEvent NotificationEventId userInfo UTCTime eventPayload,
393-
hubEntryStatus :: NotificationStatus
393+
hubEntryStatus :: NotificationStatus,
394+
hubEntryCreatedAt :: UTCTime
394395
}
395396
deriving stock (Functor, Foldable, Traversable)
396397

397398
instance (Aeson.ToJSON eventPayload, Aeson.ToJSON userInfo) => Aeson.ToJSON (NotificationHubEntry userInfo eventPayload) where
398-
toJSON NotificationHubEntry {hubEntryId, hubEntryEvent, hubEntryStatus} =
399+
toJSON NotificationHubEntry {hubEntryId, hubEntryEvent, hubEntryStatus, hubEntryCreatedAt} =
399400
Aeson.object
400401
[ "id" Aeson..= hubEntryId,
401402
"event" Aeson..= hubEntryEvent,
402-
"status" Aeson..= hubEntryStatus
403+
"status" Aeson..= hubEntryStatus,
404+
"createdAt" Aeson..= hubEntryCreatedAt
403405
]
404406

405407
instance Hasql.DecodeRow (NotificationHubEntry UserId NotificationEventData) where
406408
decodeRow = do
407409
hubEntryId <- PG.decodeField
408410
hubEntryStatus <- PG.decodeField
411+
hubEntryCreatedAt <- PG.decodeField
409412
hubEntryEvent <- PG.decodeRow
410-
pure $ NotificationHubEntry {hubEntryId, hubEntryEvent, hubEntryStatus}
413+
pure $ NotificationHubEntry {hubEntryId, hubEntryEvent, hubEntryStatus, hubEntryCreatedAt}
411414

412415
hubEntryUserInfo_ :: Traversal (NotificationHubEntry userInfo eventPayload) (NotificationHubEntry userInfo' eventPayload) userInfo userInfo'
413416
hubEntryUserInfo_ f (NotificationHubEntry {hubEntryEvent, ..}) = do

src/Share/Postgres/Contributions/Queries.hs

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import Data.List qualified as List
2828
import Data.Map qualified as Map
2929
import Data.Set qualified as Set
3030
import Data.Time (UTCTime)
31-
import Safe (lastMay)
31+
import Safe (headMay, lastMay)
3232
import Share.Codebase.Types (CodebaseEnv (..))
3333
import Share.Contribution (Contribution (..), ContributionStatus (..))
3434
import Share.IDs
@@ -112,7 +112,7 @@ listContributionsByProjectId ::
112112
Maybe UserId ->
113113
Maybe ContributionStatus ->
114114
Maybe ContributionKindFilter ->
115-
PG.Transaction e (Maybe (Cursor ListContributionsCursor), [ShareContribution UserId])
115+
PG.Transaction e (Paged ListContributionsCursor (ShareContribution UserId))
116116
listContributionsByProjectId projectId limit mayCursor mayUserFilter mayStatusFilter mayKindFilter = do
117117
let kindFilter = case mayKindFilter of
118118
Nothing -> "true"
@@ -122,11 +122,15 @@ listContributionsByProjectId projectId limit mayCursor mayUserFilter mayStatusFi
122122
OnlyContributorContributions -> [PG.sql| source_branch.contributor_id IS NOT NULL |]
123123
let cursorFilter = case mayCursor of
124124
Nothing -> "true"
125-
Just (Cursor (beforeTime, contributionId)) ->
125+
Just (Cursor (beforeTime, contributionId) Next) ->
126126
[PG.sql|
127127
(contribution.updated_at, contribution.id) < (#{beforeTime}, #{contributionId})
128128
|]
129-
addCursor
129+
Just (Cursor (afterTime, contributionId) Previous) ->
130+
[PG.sql|
131+
(contribution.updated_at, contribution.id) > (#{afterTime}, #{contributionId})
132+
|]
133+
paged
130134
<$> PG.queryListRows @(ShareContribution UserId)
131135
[PG.sql|
132136
SELECT
@@ -162,12 +166,15 @@ listContributionsByProjectId projectId limit mayCursor mayUserFilter mayStatusFi
162166
LIMIT #{limit}
163167
|]
164168
where
165-
addCursor :: [ShareContribution UserId] -> (Maybe (Cursor ListContributionsCursor), [ShareContribution UserId])
166-
addCursor xs =
167-
( lastMay xs <&> \(ShareContribution {updatedAt, contributionId}) ->
168-
Cursor (updatedAt, contributionId),
169-
xs
170-
)
169+
paged :: [ShareContribution UserId] -> Paged ListContributionsCursor (ShareContribution UserId)
170+
paged items =
171+
let prevCursor = headMay items <&> \(ShareContribution {updatedAt, contributionId}) -> Cursor (updatedAt, contributionId) Previous
172+
nextCursor = lastMay items <&> \(ShareContribution {updatedAt, contributionId}) -> Cursor (updatedAt, contributionId) Next
173+
in Paged
174+
{ items,
175+
prevCursor,
176+
nextCursor
177+
}
171178

172179
contributionById :: (PG.QueryA m) => ContributionId -> m Contribution
173180
contributionById contributionId = do
@@ -268,7 +275,7 @@ listContributionsByUserId ::
268275
Maybe (Cursor (UTCTime, ContributionId)) ->
269276
Maybe ContributionStatus ->
270277
Maybe ContributionKindFilter ->
271-
PG.Transaction e (Maybe (Cursor (UTCTime, ContributionId)), [ShareContribution UserId])
278+
PG.Transaction e (Paged (UTCTime, ContributionId) (ShareContribution UserId))
272279
listContributionsByUserId callerUserId userId limit mayCursor mayStatusFilter mayKindFilter = do
273280
let kindFilter = case mayKindFilter of
274281
Nothing -> "true"
@@ -278,11 +285,15 @@ listContributionsByUserId callerUserId userId limit mayCursor mayStatusFilter ma
278285
OnlyContributorContributions -> [PG.sql| source_branch.contributor_id IS NOT NULL |]
279286
let cursorFilter = case mayCursor of
280287
Nothing -> "true"
281-
Just (Cursor (beforeTime, contributionId)) ->
288+
Just (Cursor (beforeTime, contributionId) Next) ->
282289
[PG.sql|
283290
(contribution.updated_at, contribution.id) < (#{beforeTime}, #{contributionId})
284291
|]
285-
addCursor
292+
Just (Cursor (afterTime, contributionId) Previous) ->
293+
[PG.sql|
294+
(contribution.updated_at, contribution.id) > (#{afterTime}, #{contributionId})
295+
|]
296+
paged
286297
<$> PG.queryListRows @(ShareContribution UserId)
287298
[PG.sql|
288299
SELECT
@@ -313,12 +324,15 @@ listContributionsByUserId callerUserId userId limit mayCursor mayStatusFilter ma
313324
LIMIT #{limit}
314325
|]
315326
where
316-
addCursor :: [ShareContribution UserId] -> (Maybe (Cursor ListContributionsCursor), [ShareContribution UserId])
317-
addCursor xs =
318-
( lastMay xs <&> \(ShareContribution {updatedAt, contributionId}) ->
319-
Cursor (updatedAt, contributionId),
320-
xs
321-
)
327+
paged :: [ShareContribution UserId] -> (Paged ListContributionsCursor (ShareContribution UserId))
328+
paged items =
329+
let prevCursor = headMay items <&> \(ShareContribution {updatedAt, contributionId}) -> Cursor (updatedAt, contributionId) Previous
330+
nextCursor = lastMay items <&> \(ShareContribution {updatedAt, contributionId}) -> Cursor (updatedAt, contributionId) Next
331+
in Paged
332+
{ items,
333+
prevCursor,
334+
nextCursor
335+
}
322336

323337
-- | Note: Doesn't perform auth checks, the assumption is that if you already have access to
324338
-- the branchId you have access to all associated contributions.

src/Share/Postgres/Queries.hs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,8 @@ listBranchesByProject limit mayCursor mayBranchNamePrefix mayContributorQuery ki
772772
Just (Query branchNamePrefix) -> [PG.sql| AND starts_with(b.name, #{branchNamePrefix}) |]
773773
let cursorFilter = case mayCursor of
774774
Nothing -> mempty
775-
Just (Cursor (beforeTime, branchId)) -> [PG.sql| AND (b.updated_at, b.id) < (#{beforeTime}, #{branchId})|]
775+
Just (Cursor (beforeTime, branchId) Previous) -> [PG.sql| AND (b.updated_at, b.id) < (#{beforeTime}, #{branchId})|]
776+
Just (Cursor (afterTime, branchId) Next) -> [PG.sql| AND (b.updated_at, b.id) > (#{afterTime}, #{branchId})|]
776777
let sql =
777778
intercalateMap
778779
"\n"
@@ -873,7 +874,8 @@ listContributorBranchesOfUserAccessibleToCaller contributorUserId mayCallerUserI
873874
Just (Query branchNamePrefix) -> [PG.sql| AND starts_with(b.name, #{branchNamePrefix}) |]
874875
let cursorFilter = case mayCursor of
875876
Nothing -> mempty
876-
Just (Cursor (beforeTime, branchId)) -> [PG.sql| AND (b.updated_at, b.id) < (#{beforeTime}, #{branchId}) |]
877+
Just (Cursor (beforeTime, branchId) Previous) -> [PG.sql| AND (b.updated_at, b.id) < (#{beforeTime}, #{branchId}) |]
878+
Just (Cursor (afterTime, branchId) Next) -> [PG.sql| AND (b.updated_at, b.id) > (#{afterTime}, #{branchId}) |]
877879
let projectFilter = case mayProjectId of
878880
Nothing -> mempty
879881
Just projId -> [PG.sql| AND b.project_id = #{projId} |]
@@ -1128,8 +1130,10 @@ listReleasesByProject limit mayCursor mayVersionPrefix status projectId = do
11281130
in numericalFilters
11291131
let cursorFilter = case mayCursor of
11301132
Nothing -> mempty
1131-
Just (Cursor (major, minor, patch, releaseId)) ->
1133+
Just (Cursor (major, minor, patch, releaseId) Previous) ->
11321134
[PG.sql| AND (release.major_version, release.minor_version, release.patch_version, release.id) < (#{major}, #{minor}, #{patch}, #{releaseId}) |]
1135+
Just (Cursor (major, minor, patch, releaseId) Next) ->
1136+
[PG.sql| AND (release.major_version, release.minor_version, release.patch_version, release.id) >= (#{major}, #{minor}, #{patch}, #{releaseId}) |]
11331137
let (sql) =
11341138
intercalateMap
11351139
"\n"

0 commit comments

Comments
 (0)