Skip to content

Commit a862c90

Browse files
authored
Merge pull request #11851 from IQSS/11804-notifs-api-ext
Notifications API fixes for In-App
2 parents 79f5cf5 + b8cca26 commit a862c90

File tree

13 files changed

+840
-141
lines changed

13 files changed

+840
-141
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## Notifications API Update
2+
3+
**Endpoint:** `notifications/all`
4+
5+
**Enhancements:**
6+
7+
* When the query parameter `inAppNotificationFormat=true` is set:
8+
9+
* Notifications of types:
10+
11+
* `REQUESTFILEACCESS`
12+
* `REQUESTEDFILEACCESS`
13+
* `GRANTFILEACCESS`
14+
* `REJECTFILEACCESS`
15+
16+
now return both the **dataset display name** and **dataset persistent identifier**.
17+
18+
* Notifications of type `DATASETMENTIONED` now return a **formatted JSON** in the `additionalInfo` field when this field contains a valid persisted JSON string, instead of a raw JSON string.
19+
20+
Related issue: #11804
21+
Related PR: #11851
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
## Notifications API Update
2+
3+
**Endpoint:** `notifications/all`
4+
5+
The user notifications endpoint has been enhanced with new optional query parameters to allow for more specific and
6+
efficient data retrieval.
7+
8+
**1. Filter by Unread Status**
9+
10+
You can now fetch only unread notifications by using the `onlyUnread` boolean parameter.
11+
12+
* **`onlyUnread`**: (Optional, boolean) When set to `true`, the API will only return notifications that the user has not
13+
yet marked as read.
14+
15+
**Example:**
16+
17+
```bash
18+
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all?onlyUnread=true"
19+
```
20+
21+
**2. Pagination Support**
22+
23+
Pagination is now supported through the limit and offset parameters, allowing you to retrieve notifications in smaller,
24+
manageable chunks.
25+
26+
- **`limit`**: (Optional, integer) Specifies the maximum number of notifications to return.
27+
28+
- **`offset`**: (Optional, integer) Specifies the number of notifications to skip before starting to return results.
29+
30+
Example (Retrieve notifications 11 through 20):
31+
32+
```bash
33+
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all?limit=10&offset=10"
34+
```
35+
36+
**3 Breaking Change: API Response Format Updated**
37+
38+
To support pagination and improve consistency across the API, the JSON response format for this endpoint has been
39+
changed. This is a breaking change and will require updates to any client code currently using this endpoint.
40+
41+
**Old Format:**
42+
43+
Previously, the response nested the notification list inside a notifications object within the data field.
44+
45+
```
46+
{
47+
"status": "OK",
48+
"data": {
49+
"notifications": [
50+
/ ... /
51+
]
52+
}
53+
}
54+
```
55+
56+
**New Format:**
57+
58+
The response now includes a top-level totalCount field (required for pagination) and places the notification list
59+
directly in the data field. This flattens the structure and makes it consistent with other paginated endpoints.
60+
61+
```
62+
{
63+
"status": "OK",
64+
"totalCount": 2,
65+
"data": [
66+
/ ... /
67+
]
68+
}
69+
```
70+
71+
Related issue: #11852
72+
Related PR: #11854

doc/sphinx-guides/source/api/changelog.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ v6.9
1313
- The POST /api/admin/makeDataCount/{id}/updateCitationsForDataset processing is now asynchronous and the response no longer includes the number of citations. The response can be OK if the request is queued or 503 if the queue is full (default queue size is 1000).
1414
- The way to set per-format size limits for tabular ingest has changed. JSON input is now used. See :ref:`:TabularIngestSizeLimit`.
1515
- In the past, the settings API would accept any key and value. This is no longer the case because validation has been added. See :ref:`settings_put_single`, for example.
16+
- For GET /api/notifications/all the JSON response has changed breaking the backward compatibility of the API.
1617

1718
v6.8
1819
----
1920

2021
- For POST /api/files/{id}/metadata passing an empty string ("description":"") or array ("categories":[]) will no longer be ignored. Empty fields will now clear out the values in the file's metadata. To ignore the fields simply do not include them in the JSON string.
2122
- For PUT /api/datasets/{id}/editMetadata the query parameter "sourceInternalVersionNumber" has been removed and replaced with "sourceLastUpdateTime" to verify that the data being edited hasn't been modified and isn't stale.
22-
- For GET /api/dataverses/$dataverse-alias/links the Json response has changed breaking the backward compatibility of the API.
23+
- For GET /api/dataverses/$dataverse-alias/links the JSON response has changed breaking the backward compatibility of the API.
2324
- For GET /api/admin/dataverse/{dataverse-alias}/storageDriver and /api/datasets/{identifier}/storageDriver the driver name is no longer returned in data.message. This value is now returned in data.name.
2425
- For PUT /api/dataverses/$dataverse-alias/inputLevels custom input levels that had been previously set will no longer be deleted. To delete input levels send an empty list (deletes all), then send the new/modified list.
2526
- For GET /api/externalTools and /api/externalTools/{id} the responses are now formatted as JSON (previously the toolParameters and allowedApiCalls were a JSON object and array (respectively) that were serialized as JSON strings) and any configured "requirements" are included.

doc/sphinx-guides/source/api/native-api.rst

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6520,44 +6520,65 @@ The expected OK (200) response looks something like this:
65206520
65216521
{
65226522
"status": "OK",
6523-
"data": {
6524-
"notifications": [
6525-
{
6526-
"id": 38,
6527-
"type": "CREATEACC",
6528-
"displayAsRead": true,
6529-
"subjectText": "Root: Your account has been created",
6530-
"messageText": "Hello, \nWelcome to...",
6531-
"sentTimestamp": "2025-07-21T19:15:37Z"
6532-
}
6523+
"totalCount": 15,
6524+
"data": [
6525+
{
6526+
"id": 38,
6527+
"type": "CREATEACC",
6528+
"displayAsRead": true,
6529+
"subjectText": "Root: Your account has been created",
6530+
"messageText": "Hello, \nWelcome to...",
6531+
"sentTimestamp": "2025-07-21T19:15:37Z"
6532+
}
65336533
...
65346534
6535-
This endpoint supports an optional query parameter ``inAppNotificationFormat`` which, if sent as ``true``, retrieves the fields needed to build the in-app notifications for the Notifications section of the Dataverse UI, omitting fields related to email notifications.
6535+
This endpoint supports several optional query parameters to filter and paginate the results.
6536+
6537+
The ``inAppNotificationFormat`` parameter, if sent as ``true``, retrieves the fields needed to build the in-app notifications for the Notifications section of the Dataverse UI, omitting fields related to email notifications.
65366538
65376539
.. code-block:: bash
65386540
65396541
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all?inAppNotificationFormat=true"
65406542
6541-
The expected OK (200) response looks something like this:
6543+
The ``onlyUnread`` parameter, if sent as ``true``, filters the results to include only notifications that have not been marked as read.
6544+
6545+
.. code-block:: bash
6546+
6547+
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all?onlyUnread=true"
6548+
6549+
The ``limit`` and ``offset`` parameters can be used for pagination. ``limit`` specifies the maximum number of notifications to return, and ``offset`` specifies the number of notifications to skip from the beginning of the list. For example, to retrieve notifications 11 through 15:
6550+
6551+
To aid in pagination the JSON response also includes the total number of rows (totalCount) available.
6552+
6553+
.. code-block:: bash
6554+
6555+
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all?limit=5&offset=10"
6556+
6557+
All parameters can be combined. For instance, to get the first page of 10 unread notifications in the in-app format:
6558+
6559+
.. code-block:: bash
6560+
6561+
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all?inAppNotificationFormat=true&onlyUnread=true&limit=1&offset=0"
6562+
6563+
The expected OK (200) response for an in-app format request looks something like this:
65426564
65436565
.. code-block:: text
65446566
65456567
{
65466568
"status": "OK",
6547-
"data": {
6548-
"notifications": [
6549-
{
6550-
"id": 79,
6551-
"type": "CREATEACC",
6552-
"displayAsRead": false,
6553-
"sentTimestamp": "2025-08-08T08:00:16Z",
6554-
"installationBrandName": "Your Installation Name",
6555-
"userGuidesBaseUrl": "https://guides.dataverse.org",
6556-
"userGuidesVersion": "6.7.1",
6557-
"userGuidesSectionPath": "user/index.html"
6558-
}
6559-
]
6560-
}
6569+
"totalCount": 15,
6570+
"data": [
6571+
{
6572+
"id": 79,
6573+
"type": "CREATEACC",
6574+
"displayAsRead": false,
6575+
"sentTimestamp": "2025-08-08T08:00:16Z",
6576+
"installationBrandName": "Your Installation Name",
6577+
"userGuidesBaseUrl": "https://guides.dataverse.org",
6578+
"userGuidesVersion": "6.7.1",
6579+
"userGuidesSectionPath": "user/index.html"
6580+
}
6581+
]
65616582
}
65626583
...
65636584

src/main/java/edu/harvard/iq/dataverse/UserNotificationServiceBean.java

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import jakarta.ejb.Stateless;
1919
import jakarta.ejb.TransactionAttribute;
2020
import jakarta.ejb.TransactionAttributeType;
21-
import jakarta.inject.Inject;
2221
import jakarta.inject.Named;
2322
import jakarta.persistence.EntityManager;
2423
import jakarta.persistence.PersistenceContext;
@@ -44,11 +43,63 @@ public class UserNotificationServiceBean {
4443
SettingsServiceBean settingsService;
4544

4645
public List<UserNotification> findByUser(Long userId) {
47-
TypedQuery<UserNotification> query = em.createQuery("select un from UserNotification un where un.user.id =:userId order by un.sendDate desc", UserNotification.class);
46+
return findByUser(userId, false, null, null);
47+
}
48+
49+
/**
50+
* Finds notifications for a user, with options for pagination and filtering by read status.
51+
*
52+
* @param userId The ID of the user.
53+
* @param onlyUnread If true, returns only unread notifications. If false, returns all.
54+
* @param limit The maximum number of notifications to return (for pagination). Can be null.
55+
* @param offset The starting position for the results (for pagination). Can be null.
56+
* @return A list of UserNotification objects, ordered by send date descending.
57+
*/
58+
public List<UserNotification> findByUser(Long userId, boolean onlyUnread, Integer limit, Integer offset) {
59+
TypedQuery<UserNotification> query = em.createQuery(
60+
"select un from UserNotification un " +
61+
"where un.user.id = :userId and (:onlyUnread = false or un.readNotification = false) " +
62+
"order by un.sendDate desc",
63+
UserNotification.class
64+
);
65+
4866
query.setParameter("userId", userId);
67+
query.setParameter("onlyUnread", onlyUnread);
68+
69+
if (offset != null) {
70+
query.setFirstResult(offset);
71+
}
72+
if (limit != null) {
73+
query.setMaxResults(limit);
74+
}
75+
4976
return query.getResultList();
5077
}
51-
78+
79+
/**
80+
* Finds the total count of notifications for a user, with an option to count only unread notifications.
81+
*
82+
* @param userId The ID of the user.
83+
* @param onlyUnread If true, counts only unread notifications. If false, counts all notifications.
84+
* @return The total count as a Long.
85+
*/
86+
public Long findTotalCountByUser(Long userId, boolean onlyUnread) {
87+
if (userId == null) {
88+
return 0L;
89+
}
90+
91+
TypedQuery<Long> query = em.createQuery(
92+
"select count(un) from UserNotification un " +
93+
"where un.user.id = :userId and (:onlyUnread = false or un.readNotification = false)",
94+
Long.class
95+
);
96+
97+
query.setParameter("userId", userId);
98+
query.setParameter("onlyUnread", onlyUnread);
99+
100+
return query.getSingleResult();
101+
}
102+
52103
public List<UserNotification> findByRequestor(Long userId) {
53104
TypedQuery<UserNotification> query = em.createQuery("select un from UserNotification un where un.requestor.id =:userId order by un.sendDate desc", UserNotification.class);
54105
query.setParameter("userId", userId);

src/main/java/edu/harvard/iq/dataverse/api/Notifications.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,16 @@ public class Notifications extends AbstractApiBean {
2727
@GET
2828
@AuthRequired
2929
@Path("/all")
30-
public Response getAllNotificationsForUser(@Context ContainerRequestContext crc, @QueryParam("inAppNotificationFormat") boolean inAppNotificationFormat) {
30+
public Response getAllNotificationsForUser(@Context ContainerRequestContext crc,
31+
@QueryParam("onlyUnread") boolean onlyUnread,
32+
@QueryParam("inAppNotificationFormat") boolean inAppNotificationFormat,
33+
@QueryParam("limit") Integer limit,
34+
@QueryParam("offset") Integer offset) {
3135
try {
3236
AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc);
33-
List<UserNotification> userNotifications = userNotificationSvc.findByUser(authenticatedUser.getId());
34-
return ok(Json.createObjectBuilder().add("notifications", json(userNotifications, authenticatedUser, inAppNotificationFormat)));
37+
List<UserNotification> userNotifications = userNotificationSvc.findByUser(authenticatedUser.getId(), onlyUnread, limit, offset);
38+
long userNotificationTotalCount = userNotificationSvc.findTotalCountByUser(authenticatedUser.getId(), onlyUnread);
39+
return ok(json(userNotifications, authenticatedUser, inAppNotificationFormat), userNotificationTotalCount);
3540
} catch (WrappedResponse wr) {
3641
return wr.getResponse();
3742
}

src/main/java/edu/harvard/iq/dataverse/util/json/InAppNotificationsJsonPrinter.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@
44
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
55
import edu.harvard.iq.dataverse.branding.BrandingUtil;
66
import edu.harvard.iq.dataverse.util.SystemConfig;
7+
78
import jakarta.ejb.EJB;
89
import jakarta.ejb.Stateless;
10+
import jakarta.json.Json;
11+
import jakarta.json.JsonException;
12+
import jakarta.json.JsonReader;
13+
import jakarta.json.JsonValue;
14+
15+
import java.io.StringReader;
916

1017
import static edu.harvard.iq.dataverse.dataset.DatasetUtil.getLocaleCurationStatusLabel;
1118
import static edu.harvard.iq.dataverse.util.json.JsonPrinter.jsonRoleAssignments;
@@ -80,8 +87,6 @@ public void addFieldsByType(final NullSafeJsonBuilder notificationJson, final Au
8087
addRequestFileAccessFields(notificationJson, userNotification, requestor);
8188
break;
8289
case REQUESTEDFILEACCESS:
83-
case GRANTFILEACCESS:
84-
case REJECTFILEACCESS:
8590
addDataFileFields(notificationJson, userNotification);
8691
break;
8792
case DATASETCREATED:
@@ -116,6 +121,8 @@ public void addFieldsByType(final NullSafeJsonBuilder notificationJson, final Au
116121
case GLOBUSUPLOADLOCALFAILURE:
117122
case GLOBUSDOWNLOADCOMPLETEDWITHERRORS:
118123
case CHECKSUMFAIL:
124+
case GRANTFILEACCESS:
125+
case REJECTFILEACCESS:
119126
addDatasetFields(notificationJson, userNotification);
120127
break;
121128
case INGESTCOMPLETED:
@@ -184,6 +191,8 @@ private void addDataFileFields(final NullSafeJsonBuilder notificationJson, final
184191
if (dataFile != null) {
185192
notificationJson.add(KEY_DATAFILE_ID, dataFile.getId());
186193
notificationJson.add(KEY_DATAFILE_DISPLAY_NAME, dataFile.getDisplayName());
194+
notificationJson.add(KEY_DATASET_DISPLAY_NAME, dataFile.getOwner().getDisplayName());
195+
notificationJson.add(KEY_DATASET_PERSISTENT_ID, dataFile.getOwner().getGlobalId().asString());
187196
} else {
188197
notificationJson.add(KEY_OBJECT_DELETED, true);
189198
}
@@ -260,6 +269,24 @@ private void addIngestFields(final NullSafeJsonBuilder notificationJson, final U
260269

261270
private void addDatasetMentionedFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification) {
262271
addDatasetFields(notificationJson, userNotification);
263-
notificationJson.add(KEY_ADDITIONAL_INFO, userNotification.getAdditionalInfo());
272+
273+
final String additionalInfo = userNotification.getAdditionalInfo();
274+
275+
if (additionalInfo != null && !additionalInfo.isEmpty()) {
276+
try (StringReader stringReader = new StringReader(additionalInfo);
277+
JsonReader jsonReader = Json.createReader(stringReader)) {
278+
279+
// Try to parse the string into a JSON value
280+
JsonValue additionalInfoJson = jsonReader.readValue();
281+
282+
// If successful, add the parsed JSON value.
283+
notificationJson.add(KEY_ADDITIONAL_INFO, additionalInfoJson);
284+
285+
} catch (JsonException e) {
286+
// If parsing fails, it's not a valid JSON string.
287+
// Fall back to adding it as a simple string.
288+
notificationJson.add(KEY_ADDITIONAL_INFO, additionalInfo);
289+
}
290+
}
264291
}
265292
}

0 commit comments

Comments
 (0)