From ad853a7673a11c0b307fc28060b33b97b62e0f2a Mon Sep 17 00:00:00 2001 From: Kunal Patel Date: Thu, 9 Jul 2026 09:59:22 -0400 Subject: [PATCH 1/5] fix(notification): security - scope notification mutations to recipient --- .../notifications/NotificationDbUtils.java | 162 +++++++++++++----- .../DeleteNotificationReactor.java | 6 +- .../MarkNotificationReadReactor.java | 14 +- .../PollNotificationsReactor.java | 4 +- 4 files changed, 132 insertions(+), 54 deletions(-) diff --git a/src/prerna/notifications/NotificationDbUtils.java b/src/prerna/notifications/NotificationDbUtils.java index e64bcfd533c..c1ac49b29a1 100644 --- a/src/prerna/notifications/NotificationDbUtils.java +++ b/src/prerna/notifications/NotificationDbUtils.java @@ -33,6 +33,8 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -53,6 +55,7 @@ import prerna.query.querystruct.SelectQueryStruct; import prerna.query.querystruct.filters.AndQueryFilter; import prerna.query.querystruct.filters.IQueryFilter; +import prerna.query.querystruct.filters.OrQueryFilter; import prerna.query.querystruct.filters.SimpleQueryFilter; import prerna.query.querystruct.selectors.QueryColumnSelector; import prerna.util.ConnectionUtils; @@ -123,21 +126,10 @@ private static void initialize(List>>> db } // NOTIFICATION index (kept) - if (allowIfExistsIndexs) { - String sql = queryUtil.createIndexIfNotExists("NOTIFICATION_NOTIFICATIONID_INDEX", "NOTIFICATION", - "NOTIFICATIONID"); - classLogger.info("Running sql {}", sql); - notificationDb.insertData(sql); - } else { - // see if index exists - if (!queryUtil.indexExists(notificationDb, "NOTIFICATION_NOTIFICATIONID_INDEX", "NOTIFICATION", - database, schema)) { - String sql = queryUtil.createIndex("NOTIFICATION_NOTIFICATIONID_INDEX", "NOTIFICATION", - "NOTIFICATIONID"); - classLogger.info("Running sql {}", sql); - notificationDb.insertData(sql); - } - } + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_NOTIFICATIONID_INDEX", "NOTIFICATION", "NOTIFICATIONID"); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_RECIPIENT_INDEX", "NOTIFICATION", Arrays.asList("RECIPIENTID", "RECIPIENTTYPE")); if (!conn.getAutoCommit()) { conn.commit(); @@ -150,6 +142,27 @@ private static void initialize(List>>> db } } + private static void createIndexIfMissing(IRDBMSEngine notificationDb, AbstractSqlQueryUtil queryUtil, + boolean allowIfExistsIndexs, String database, String schema, String indexName, String tableName, + String columnName) throws Exception { + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, indexName, tableName, + Arrays.asList(columnName)); + } + + private static void createIndexIfMissing(IRDBMSEngine notificationDb, AbstractSqlQueryUtil queryUtil, + boolean allowIfExistsIndexs, String database, String schema, String indexName, String tableName, + Collection columns) throws Exception { + if (allowIfExistsIndexs) { + String sql = queryUtil.createIndexIfNotExists(indexName, tableName, columns); + classLogger.info("Running sql {}", sql); + notificationDb.insertData(sql); + } else if (!queryUtil.indexExists(notificationDb, indexName, tableName, database, schema)) { + String sql = queryUtil.createIndex(indexName, tableName, columns); + classLogger.info("Running sql {}", sql); + notificationDb.insertData(sql); + } + } + /** * Determine if the theme db is present to be able to set custom themes * @@ -288,17 +301,7 @@ public static List> fetchAllNotifications(User user, String qs.addSelector(new QueryColumnSelector("NOTIFICATION__CREATEDBY", "notification_createdby")); qs.addSelector(new QueryColumnSelector("NOTIFICATION__USERTYPE", "notification_usertype")); - Pair userPair = userIdAndTypeList.get(0); - String userId = userPair.getValue0(); - String userType = userPair.getValue1(); - - List andFilters = new ArrayList<>(); - andFilters.add(SimpleQueryFilter.makeColToValFilter("NOTIFICATION__RECIPIENTID", "==", userId)); - andFilters.add(SimpleQueryFilter.makeColToValFilter("NOTIFICATION__RECIPIENTTYPE", "==", userType)); - - // (RECIPIENTID == userId AND RECIPIENTTYPE == userType) - AndQueryFilter andCombined = new AndQueryFilter(andFilters); - qs.addExplicitFilter(andCombined); + qs.addExplicitFilter(buildRecipientFilter(userIdAndTypeList)); qs.addOrderBy("NOTIFICATION__CREATEDDATE", "desc"); Long long_limit = -1L; @@ -371,6 +374,14 @@ public static List> fetchAllNotifications(User user, String * @return */ public static int deleteNotification(String recipientId, String recipientType, String notificationId) { + List> recipientPairs = new ArrayList<>(); + if (recipientId != null && recipientType != null) { + recipientPairs.add(Pair.with(recipientId, recipientType)); + } + return deleteNotification(recipientPairs, notificationId); + } + + public static int deleteNotification(List> recipientPairs, String notificationId) { IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); StringBuilder deleteQuery = new StringBuilder("DELETE FROM NOTIFICATION WHERE "); List conditions = new ArrayList<>(); @@ -379,12 +390,12 @@ public static int deleteNotification(String recipientId, String recipientType, S if (notificationId != null) { conditions.add("NOTIFICATIONID = ?"); parameters.add(notificationId); - } else if (recipientId != null && recipientType != null) { - conditions.add("RECIPIENTID = ?"); - parameters.add(recipientId); - conditions.add("RECIPIENTTYPE = ?"); - parameters.add(recipientType); - } else { + } + String recipientCondition = buildRecipientSqlCondition(recipientPairs, parameters); + if (recipientCondition != null) { + conditions.add(recipientCondition); + } + if (conditions.isEmpty() || (notificationId == null && recipientCondition == null)) { return 0; // nothing to delete } @@ -404,8 +415,8 @@ public static int deleteNotification(String recipientId, String recipientType, S ps.getConnection().commit(); } } catch (SQLException e) { - classLogger.error("Failed to delete notification(s) [notificationId={}, recipientId={}, recipientType={}]", - notificationId, recipientId, recipientType, e); + classLogger.error("Failed to delete notification(s) [notificationId={}, recipientPairs={}]", + notificationId, recipientPairs, e); } finally { ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } @@ -455,23 +466,39 @@ public static void resetNotificationActionType(User user) { * @param readDate -the timestamp when the notification was read */ public static void markNotificationRead(String notificationId, Timestamp readDate) { + markNotificationRead(notificationId, readDate, null); + } + + public static int markNotificationRead(String notificationId, Timestamp readDate, + List> recipientPairs) { IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); - String query = "UPDATE NOTIFICATION SET ISREAD = TRUE, READDATE=? WHERE NOTIFICATIONID=?"; + List parameters = new ArrayList<>(); + parameters.add(readDate); + parameters.add(notificationId); + StringBuilder query = new StringBuilder("UPDATE NOTIFICATION SET ISREAD = TRUE, READDATE=? WHERE NOTIFICATIONID=?"); + String recipientCondition = buildRecipientSqlCondition(recipientPairs, parameters); + if (recipientCondition != null) { + query.append(" AND ").append(recipientCondition); + } PreparedStatement ps = null; try { - ps = notificationDb.getPreparedStatement(query); + ps = notificationDb.getPreparedStatement(query.toString()); int parameterIndex = 1; - ps.setTimestamp(parameterIndex++, readDate); - ps.setString(parameterIndex++, notificationId); - ps.executeUpdate(); + for (Object param : parameters) { + ps.setObject(parameterIndex++, param); + } + int updatedCount = ps.executeUpdate(); if (!ps.getConnection().getAutoCommit()) { ps.getConnection().commit(); } + return updatedCount; } catch (SQLException e) { - classLogger.error("Failed to mark notification {} as read (readDate={})", notificationId, readDate, e); + classLogger.error("Failed to mark notification {} as read (readDate={}, recipientPairs={})", notificationId, + readDate, recipientPairs, e); } finally { ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } + return 0; } /** @@ -482,27 +509,70 @@ public static void markNotificationRead(String notificationId, Timestamp readDat * @return the count of new notifications */ public static int fetchNewNotificationCount(String recipientId, String recipientType) { + List> recipientPairs = new ArrayList<>(); + if (recipientId != null && recipientType != null) { + recipientPairs.add(Pair.with(recipientId, recipientType)); + } + return fetchNewNotificationCount(recipientPairs); + } + + public static int fetchNewNotificationCount(List> recipientPairs) { IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); PreparedStatement ps = null; - String query = "SELECT COUNT(NOTIFICATIONID) FROM NOTIFICATION " - + "WHERE RECIPIENTID = ? AND RECIPIENTTYPE = ? AND ACTIONTYPE = 'NEW'"; + List parameters = new ArrayList<>(); + String recipientCondition = buildRecipientSqlCondition(recipientPairs, parameters); + if (recipientCondition == null) { + return 0; + } + String query = "SELECT COUNT(NOTIFICATIONID) FROM NOTIFICATION WHERE ACTIONTYPE = 'NEW' AND " + + recipientCondition; try { ps = notificationDb.getPreparedStatement(query); int parameterIndex = 1; - ps.setString(parameterIndex++, recipientId); - ps.setString(parameterIndex++, recipientType); + for (Object param : parameters) { + ps.setObject(parameterIndex++, param); + } try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return rs.getInt(1); } } } catch (SQLException e) { - classLogger.error("Failed to fetch new notification count for recipient {} (type {})", recipientId, - recipientType, e); + classLogger.error("Failed to fetch new notification count for recipient pairs {}", recipientPairs, e); } finally { ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } return 0; } + private static IQueryFilter buildRecipientFilter(List> recipientPairs) { + OrQueryFilter orCombined = new OrQueryFilter(); + for (Pair pair : recipientPairs) { + List andFilters = new ArrayList<>(); + andFilters.add(SimpleQueryFilter.makeColToValFilter("NOTIFICATION__RECIPIENTID", "==", pair.getValue0())); + andFilters.add(SimpleQueryFilter.makeColToValFilter("NOTIFICATION__RECIPIENTTYPE", "==", pair.getValue1())); + orCombined.addFilter(new AndQueryFilter(andFilters)); + } + return orCombined; + } + + private static String buildRecipientSqlCondition(List> recipientPairs, List parameters) { + if (recipientPairs == null || recipientPairs.isEmpty()) { + return null; + } + List recipientConditions = new ArrayList<>(); + for (Pair pair : recipientPairs) { + if (pair == null || pair.getValue0() == null || pair.getValue1() == null) { + continue; + } + recipientConditions.add("(RECIPIENTID = ? AND RECIPIENTTYPE = ?)"); + parameters.add(pair.getValue0()); + parameters.add(pair.getValue1()); + } + if (recipientConditions.isEmpty()) { + return null; + } + return "(" + String.join(" OR ", recipientConditions) + ")"; + } + } diff --git a/src/prerna/reactor/notification/DeleteNotificationReactor.java b/src/prerna/reactor/notification/DeleteNotificationReactor.java index 397b22d2f9d..954838da9ed 100644 --- a/src/prerna/reactor/notification/DeleteNotificationReactor.java +++ b/src/prerna/reactor/notification/DeleteNotificationReactor.java @@ -68,13 +68,11 @@ public NounMetadata execute() { PixelDataType.CONST_STRING, PixelOperationType.ERROR, PixelOperationType.LOGGIN_REQUIRED_ERROR)); } - String recipientId = userIdAndTypeList.get(0).getValue0(); - String recipientType = userIdAndTypeList.get(0).getValue1(); int deleteCount; if (notificationId != null) { - deleteCount = NotificationDbUtils.deleteNotification(null, null, notificationId); + deleteCount = NotificationDbUtils.deleteNotification(userIdAndTypeList, notificationId); } else { - deleteCount = NotificationDbUtils.deleteNotification(recipientId, recipientType, null); + deleteCount = NotificationDbUtils.deleteNotification(userIdAndTypeList, null); } return new NounMetadata(deleteCount, PixelDataType.CONST_INT); } diff --git a/src/prerna/reactor/notification/MarkNotificationReadReactor.java b/src/prerna/reactor/notification/MarkNotificationReadReactor.java index 6ad7376d844..bfa58d231b8 100644 --- a/src/prerna/reactor/notification/MarkNotificationReadReactor.java +++ b/src/prerna/reactor/notification/MarkNotificationReadReactor.java @@ -28,11 +28,18 @@ package prerna.reactor.notification; import java.sql.Timestamp; +import java.util.List; +import org.javatuples.Pair; + +import prerna.auth.User; import prerna.auth.utils.AbstractSecurityUtils; import prerna.notifications.NotificationDbUtils; import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.PixelOperationType; import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.execptions.SemossPixelException; import prerna.sablecc2.om.nounmeta.NounMetadata; import prerna.util.Utility; @@ -56,7 +63,12 @@ public NounMetadata execute() { organizeKeys(); String notificationId = this.keyValue.get(this.keysToGet[0]); Timestamp readAt = Utility.getCurrentSqlTimestampUTC(); - NotificationDbUtils.markNotificationRead(notificationId, readAt); + List> userIdAndTypeList = User.getUserIdAndType(this.insight.getUser()); + if (userIdAndTypeList == null || userIdAndTypeList.isEmpty()) { + throw new SemossPixelException(new NounMetadata("Unable to determine user type for notification update", + PixelDataType.CONST_STRING, PixelOperationType.ERROR, PixelOperationType.LOGGIN_REQUIRED_ERROR)); + } + NotificationDbUtils.markNotificationRead(notificationId, readAt, userIdAndTypeList); NounMetadata retNoun = NounMetadata.getSuccessNounMessage("Success!"); return retNoun; } diff --git a/src/prerna/reactor/notification/PollNotificationsReactor.java b/src/prerna/reactor/notification/PollNotificationsReactor.java index e48f1a2ea05..06601585400 100644 --- a/src/prerna/reactor/notification/PollNotificationsReactor.java +++ b/src/prerna/reactor/notification/PollNotificationsReactor.java @@ -59,9 +59,7 @@ public NounMetadata execute() { PixelDataType.CONST_STRING, PixelOperationType.ERROR, PixelOperationType.LOGGIN_REQUIRED_ERROR)); } - String recipientId = userIdAndTypeList.get(0).getValue0(); - String recipientType = userIdAndTypeList.get(0).getValue1(); - int newNotificationCount = NotificationDbUtils.fetchNewNotificationCount(recipientId, recipientType); + int newNotificationCount = NotificationDbUtils.fetchNewNotificationCount(userIdAndTypeList); return new NounMetadata(newNotificationCount, PixelDataType.CONST_INT); } From c7e384f6f7d267ad1aa6a168bafae6cc662287f6 Mon Sep 17 00:00:00 2001 From: Kunal Patel Date: Thu, 9 Jul 2026 11:49:55 -0400 Subject: [PATCH 2/5] feat: creating notification tables --- .../notifications/NotificationDbUtils.java | 848 +++++++++++------- .../notifications/NotificationOwlCreator.java | 60 +- src/prerna/util/NotificationConstants.java | 61 ++ 3 files changed, 648 insertions(+), 321 deletions(-) diff --git a/src/prerna/notifications/NotificationDbUtils.java b/src/prerna/notifications/NotificationDbUtils.java index c1ac49b29a1..7598dcc433d 100644 --- a/src/prerna/notifications/NotificationDbUtils.java +++ b/src/prerna/notifications/NotificationDbUtils.java @@ -27,6 +27,7 @@ *******************************************************************************/ package prerna.notifications; +import java.sql.Clob; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -46,21 +47,17 @@ import org.javatuples.Pair; import com.github.f4b6a3.uuid.alt.GUID; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import prerna.auth.User; import prerna.auth.utils.SecurityEngineUtils; import prerna.auth.utils.SecurityProjectUtils; import prerna.auth.utils.SecurityUserUtils; import prerna.engine.api.IRDBMSEngine; -import prerna.query.querystruct.SelectQueryStruct; -import prerna.query.querystruct.filters.AndQueryFilter; -import prerna.query.querystruct.filters.IQueryFilter; -import prerna.query.querystruct.filters.OrQueryFilter; -import prerna.query.querystruct.filters.SimpleQueryFilter; -import prerna.query.querystruct.selectors.QueryColumnSelector; import prerna.util.ConnectionUtils; import prerna.util.NotificationConstants; -import prerna.util.QueryExecutionUtility; import prerna.util.SystemEngineRegistry; import prerna.util.Utility; import prerna.util.sql.AbstractSqlQueryUtil; @@ -95,7 +92,6 @@ private static void initialize(List>>> db boolean allowIfExistsTable = queryUtil.allowsIfExistsTableSyntax(); boolean allowIfExistsIndexs = queryUtil.allowIfExistsIndexSyntax(); - // create the tables and columns from the OWL creator schema for (Pair>> tableSchema : dbSchema) { String tableName = tableSchema.getValue0(); String[] colNames = tableSchema.getValue1().stream().map(Pair::getValue0).toArray(String[]::new); @@ -104,12 +100,10 @@ private static void initialize(List>>> db String sql = queryUtil.createTableIfNotExists(tableName, colNames, types); classLogger.info("Running sql {}", sql); notificationDb.insertData(sql); - } else { - if (!queryUtil.tableExists(conn, tableName, database, schema)) { - String sql = queryUtil.createTable(tableName, colNames, types); - classLogger.info("Running sql {}", sql); - notificationDb.insertData(sql); - } + } else if (!queryUtil.tableExists(conn, tableName, database, schema)) { + String sql = queryUtil.createTable(tableName, colNames, types); + classLogger.info("Running sql {}", sql); + notificationDb.insertData(sql); } List allCols = queryUtil.getTableColumns(conn, tableName, database, schema); @@ -125,17 +119,30 @@ private static void initialize(List>>> db } } - // NOTIFICATION index (kept) createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, - "NOTIFICATION_NOTIFICATIONID_INDEX", "NOTIFICATION", "NOTIFICATIONID"); + "NOTIFICATION_EVENT_NOTIFICATION_ID_INDEX", "NOTIFICATION_EVENT", "NOTIFICATION_ID"); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_EVENT_SCOPE_INDEX", "NOTIFICATION_EVENT", Arrays.asList("SCOPE_TYPE", "SCOPE_ID")); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_EVENT_AUDIENCE_INDEX", "NOTIFICATION_EVENT", + Arrays.asList("AUDIENCE_TYPE", "AUDIENCE_ID")); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_EVENT_TARGET_INDEX", "NOTIFICATION_EVENT", Arrays.asList("TARGET_TYPE", "TARGET_ID")); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_EVENT_GROUP_INDEX", "NOTIFICATION_EVENT", "GROUP_ID"); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_USER_STATE_NOTIFICATION_USER_INDEX", "NOTIFICATION_USER_STATE", + Arrays.asList("NOTIFICATION_ID", "USER_ID", "USER_TYPE")); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_USER_STATE_USER_INDEX", "NOTIFICATION_USER_STATE", + Arrays.asList("USER_ID", "USER_TYPE")); createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, - "NOTIFICATION_RECIPIENT_INDEX", "NOTIFICATION", Arrays.asList("RECIPIENTID", "RECIPIENTTYPE")); + "NOTIFICATION_DELIVERY_NOTIFICATION_INDEX", "NOTIFICATION_DELIVERY", "NOTIFICATION_ID"); if (!conn.getAutoCommit()) { conn.commit(); } } finally { - // clean up the connection used for this method if (conn != null && notificationDb.isConnectionPooling()) { conn.close(); } @@ -164,7 +171,7 @@ private static void createIndexIfMissing(IRDBMSEngine notificationDb, AbstractSq } /** - * Determine if the theme db is present to be able to set custom themes + * Determine if the notification db is present. * * @return */ @@ -173,206 +180,145 @@ public static boolean isInitalized() { } /** - * Add notification into database - * - * @param loggedInUser - The logged-in user performing the action - * @param affectedUserId - The user whose role or permission changed - * @param catalogId - * @param notificationType - e.g. USER_REQUEST, REQUEST_APPROVAL - * @param notificationSource - * @param priority - e.g. HIGH, MEDIUM, LOW - * @param affectedUserPreviousRole - * @param affectedUserNewRole + * Adapter for existing access-request notification producers. */ public static void createNotification(User loggedInUser, String affectedUserId, String affectedUserType, String catalogId, String notificationType, String notificationSource, String priority, String affectedUserPreviousRole, String affectedUserNewRole) { IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); - // Fetch all authors based on source - List> authors = NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource) - ? SecurityProjectUtils.getProjectAuthors(catalogId) - : SecurityEngineUtils.getEngineAuthors(catalogId); - - // Check if initiator already present - boolean initiatorFound = false; - - if (affectedUserId != null && authors != null && !authors.isEmpty()) { - for (int i = 0; i < authors.size(); i++) { - Map user = authors.get(i); - if (user == null) { - continue; - } - String userId = (String) user.get("userId"); - if (userId == null) { + List> authorRecipients = NotificationConstants.APP_CATALOG + .equalsIgnoreCase(notificationSource) ? SecurityProjectUtils.getProjectAuthors(catalogId) + : SecurityEngineUtils.getEngineAuthors(catalogId); + List> recipients = authorRecipients == null ? new ArrayList<>() + : new ArrayList<>(authorRecipients); + + boolean affectedUserFound = false; + if (affectedUserId != null) { + for (Map recipient : recipients) { + if (recipient == null) { continue; } - - if (affectedUserId.equals(userId)) { - initiatorFound = true; + if (affectedUserId.equals(recipient.get("userId"))) { + affectedUserFound = true; break; } } } - if (!initiatorFound && affectedUserId != null) { - Map initiatorMap = new HashMap<>(); - initiatorMap.put("userId", affectedUserId); - initiatorMap.put("userType", affectedUserType); - authors.add(initiatorMap); + if (!affectedUserFound && affectedUserId != null) { + Map affectedUser = new HashMap<>(); + affectedUser.put("userId", affectedUserId); + affectedUser.put("userType", affectedUserType); + recipients.add(affectedUser); } String createdBy = loggedInUser.getAccessToken(loggedInUser.getLogins().get(0)).getId(); - Timestamp createdDate = Utility.getCurrentSqlTimestampUTC(); - - for (Map author : authors) { - String recipientId = (String) author.get("userId"); - String recipientType = (String) author.get("userType"); - - String query = "INSERT INTO NOTIFICATION (NOTIFICATIONID,RECIPIENTID,RECIPIENTTYPE,NOTIFICATIONTITLE,MESSAGE,ACTIONTYPE,ACTIONTARGET,ISREAD,PRIORITY,NOTIFICATIONTYPE,CATALOGID,CREATEDBY,CREATEDDATE,READDATE,NOTIFICATIONSOURCE,USERID,USERTYPE,USEREXISTINGROLE,USERNEWROLE) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + Timestamp createdAt = Utility.getCurrentSqlTimestampUTC(); + String kind = deriveKind(notificationType); + String type = deriveType(notificationType); + String scopeType = deriveScopeType(notificationSource); + String scopeId = NotificationConstants.Scope.APP.equals(scopeType) ? catalogId : null; + String sourceType = deriveSourceType(notificationSource); + String targetType = NotificationConstants.Scope.APP.equals(scopeType) ? NotificationConstants.Target.APP + : NotificationConstants.Target.NONE; + String metadataJson = buildLegacyMetadata(affectedUserId, affectedUserType, affectedUserPreviousRole, + affectedUserNewRole, notificationType, notificationSource); + + String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,KIND,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,TARGET_URL,ACTION_LABEL,STATUS,GROUP_ID,METADATA_JSON,CREATED_BY,CREATED_AT,RESOLVED_AT,EXPIRES_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + for (Map recipient : recipients) { + if (recipient == null || recipient.get("userId") == null) { + continue; + } PreparedStatement ps = null; try { ps = notificationDb.getPreparedStatement(query); int parameterIndex = 1; - ps.setString(parameterIndex++, GUID.v7().toUUID().toString()); // notificationId - ps.setString(parameterIndex++, recipientId); - ps.setString(parameterIndex++, recipientType); - ps.setString(parameterIndex++, "NOTIFICATION"); // notificationTitle - ps.setString(parameterIndex++, null); // message - ps.setString(parameterIndex++, "NEW"); // actionType - ps.setString(parameterIndex++, "IN-APP"); - ps.setBoolean(parameterIndex++, false); // isRead - ps.setString(parameterIndex++, priority); - ps.setString(parameterIndex++, notificationType); + ps.setString(parameterIndex++, GUID.v7().toUUID().toString()); + ps.setString(parameterIndex++, kind); + ps.setString(parameterIndex++, type); + ps.setString(parameterIndex++, scopeType); + ps.setString(parameterIndex++, scopeId); + ps.setString(parameterIndex++, NotificationConstants.Audience.USER); + ps.setString(parameterIndex++, String.valueOf(recipient.get("userId"))); + ps.setString(parameterIndex++, + recipient.get("userType") == null ? null : String.valueOf(recipient.get("userType"))); + ps.setString(parameterIndex++, "NOTIFICATION"); + ps.setString(parameterIndex++, null); + ps.setString(parameterIndex++, normalizePriority(priority)); + ps.setString(parameterIndex++, sourceType); ps.setString(parameterIndex++, catalogId); + ps.setString(parameterIndex++, targetType); + ps.setString(parameterIndex++, catalogId); + ps.setString(parameterIndex++, null); + ps.setString(parameterIndex++, NotificationConstants.Kind.ACTION.equals(kind) ? "Review" : null); + ps.setString(parameterIndex++, NotificationConstants.Status.ACTIVE); + ps.setString(parameterIndex++, null); + ps.setString(parameterIndex++, metadataJson); ps.setString(parameterIndex++, createdBy); - ps.setTimestamp(parameterIndex++, createdDate); - ps.setTimestamp(parameterIndex++, null); // readDate - ps.setString(parameterIndex++, notificationSource); - ps.setString(parameterIndex++, affectedUserId); - ps.setString(parameterIndex++, affectedUserType);// userType - ps.setString(parameterIndex++, affectedUserPreviousRole); - ps.setString(parameterIndex++, affectedUserNewRole); + ps.setTimestamp(parameterIndex++, createdAt); + ps.setTimestamp(parameterIndex++, null); + ps.setTimestamp(parameterIndex++, null); ps.execute(); if (!ps.getConnection().getAutoCommit()) { ps.getConnection().commit(); } } catch (SQLException e) { - classLogger.error("Failed to insert notification for recipient {} (type {}) on catalog {}", recipientId, - recipientType, catalogId, e); + classLogger.error("Failed to insert notification for recipient {} (type {}) on catalog {}", + recipient.get("userId"), recipient.get("userType"), catalogId, e); } finally { ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } } } - /** - * Get all notifications for user - * - * @param memberId - * @param limit - * @param offset - * @return list of notifications - */ public static List> fetchAllNotifications(User user, String limit, String offset) { - IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); List> userIdAndTypeList = User.getUserIdAndType(user); if (userIdAndTypeList.isEmpty()) { return new ArrayList<>(); } - SelectQueryStruct qs = new SelectQueryStruct(); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__NOTIFICATIONID", "notification_id")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__RECIPIENTID", "recipient_id")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__RECIPIENTTYPE", "recipient_type")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__NOTIFICATIONTITLE", "notification_title")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__MESSAGE", "notification_message")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__ACTIONTYPE", "notification_actiontype")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__ACTIONTARGET", "notification_actiontarget")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__ISREAD", "notification_isread")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__PRIORITY", "notification_priority")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__NOTIFICATIONTYPE", "notification_type")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__CATALOGID", "catalog_id")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__CREATEDDATE", "notification_createddate")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__READDATE", "notification_readdate")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__NOTIFICATIONSOURCE", "notification_source")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__USERID", "recipient_user_id")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__USEREXISTINGROLE", "user_existingrole")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__USERNEWROLE", "user_newrole")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__CREATEDBY", "notification_createdby")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__USERTYPE", "notification_usertype")); - - qs.addExplicitFilter(buildRecipientFilter(userIdAndTypeList)); - qs.addOrderBy("NOTIFICATION__CREATEDDATE", "desc"); - - Long long_limit = -1L; - Long long_offset = -1L; - if (limit != null && !limit.trim().isEmpty()) { - long_limit = ((Number) Double.parseDouble(limit)).longValue(); - } - if (offset != null && !offset.trim().isEmpty()) { - long_offset = ((Number) Double.parseDouble(offset)).longValue(); - } - qs.setLimit(long_limit); - qs.setOffSet(long_offset); - - List> notificationList = QueryExecutionUtility.flushRsToMap(notificationDb, qs); - if (notificationList == null || notificationList.isEmpty()) { - return new ArrayList<>(); - } - - // Collect all unique IDs - Set userIds = new HashSet<>(); - Set catalogIds = new HashSet<>(); - - for (Map row : notificationList) { - if (row.get("recipient_user_id") != null) { - userIds.add(String.valueOf(row.get("recipient_user_id"))); - } - if (row.get("catalog_id") != null) { - catalogIds.add(String.valueOf(row.get("catalog_id"))); - } - if (row.get("notification_createdby") != null) { - userIds.add(String.valueOf(row.get("notification_createdby"))); - } + List audienceParameters = new ArrayList<>(); + List dismissedParameters = new ArrayList<>(); + List readParameters = new ArrayList<>(); + String audienceCondition = buildVisibleAudienceSqlCondition(userIdAndTypeList, audienceParameters); + String dismissedCondition = buildStateExistsSqlCondition("us", userIdAndTypeList, dismissedParameters, + "us.IS_DISMISSED = TRUE"); + String readCondition = buildStateExistsSqlCondition("urs", userIdAndTypeList, readParameters, + "urs.IS_READ = TRUE"); + + StringBuilder query = new StringBuilder(); + query.append("SELECT n.NOTIFICATION_ID, n.KIND, n.TYPE, n.SCOPE_TYPE, n.SCOPE_ID, n.AUDIENCE_TYPE, ") + .append("n.AUDIENCE_ID, n.AUDIENCE_USER_TYPE, n.TITLE, n.MESSAGE, n.PRIORITY, n.SOURCE_TYPE, ") + .append("n.SOURCE_ID, n.TARGET_TYPE, n.TARGET_ID, n.TARGET_URL, n.ACTION_LABEL, n.STATUS, ") + .append("n.GROUP_ID, n.METADATA_JSON, n.CREATED_BY, n.CREATED_AT, n.RESOLVED_AT, n.EXPIRES_AT, ") + .append("CASE WHEN EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE urs WHERE urs.NOTIFICATION_ID = n.NOTIFICATION_ID AND ") + .append(readCondition).append(") THEN TRUE ELSE FALSE END AS IS_READ ") + .append("FROM NOTIFICATION_EVENT n WHERE (").append(audienceCondition).append(") ") + .append("AND n.STATUS <> ? ").append("AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) ") + .append("AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND ") + .append(dismissedCondition).append(") ").append("ORDER BY n.CREATED_AT DESC"); + List parameters = new ArrayList<>(); + parameters.addAll(readParameters); + parameters.addAll(audienceParameters); + parameters.add(NotificationConstants.Status.EXPIRED); + parameters.addAll(dismissedParameters); + + Long longLimit = parseLong(limit); + Long longOffset = parseLong(offset); + if (longLimit != null && longLimit >= 0) { + query.append(" LIMIT ?"); + parameters.add(longLimit); } - // bulk fetch - Map userIdToNameMap = SecurityUserUtils.getUserNamesByIds(userIds); - Map projectIdToNameMap = SecurityProjectUtils.getProjectNamesByIds(catalogIds); - Map engineIdToNameMap = SecurityEngineUtils.getEngineNamesByIds(catalogIds); - - for (Map row : notificationList) { - String catalogId = String.valueOf(row.get("catalog_id")); - String notificationSource = String.valueOf(row.get("notification_source")).trim(); - - // user name from cached map - row.put("recipient_user_name", userIdToNameMap.getOrDefault(row.get("recipient_user_id"), "Unknown User")); - - // project and engine names from cached maps - String projectName = projectIdToNameMap.get(catalogId); - String engineName = engineIdToNameMap.get(catalogId); - - // final catalog name - String finalCatalogName; - if (NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource)) { - finalCatalogName = (projectName != null && !projectName.isEmpty()) ? projectName - : ((engineName != null && !engineName.isEmpty()) ? engineName : null); - } else { - finalCatalogName = (engineName != null && !engineName.isEmpty()) ? engineName - : ((projectName != null && !projectName.isEmpty()) ? projectName : null); - } - row.put("catalog_name", finalCatalogName); + if (longOffset != null && longOffset >= 0) { + query.append(" OFFSET ?"); + parameters.add(longOffset); } + List> notificationList = executeNotificationFetch(query.toString(), parameters); + hydrateLegacyDisplayFields(notificationList); return notificationList; } - /** - * - * @param recipientId -the ID of the recipient - * @param recipientType -the type of the recipient (e.g., NATIVE, MS) - * @param notificationId -the ID of the notification - * @return - */ public static int deleteNotification(String recipientId, String recipientType, String notificationId) { List> recipientPairs = new ArrayList<>(); if (recipientId != null && recipientType != null) { @@ -382,132 +328,61 @@ public static int deleteNotification(String recipientId, String recipientType, S } public static int deleteNotification(List> recipientPairs, String notificationId) { - IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); - StringBuilder deleteQuery = new StringBuilder("DELETE FROM NOTIFICATION WHERE "); - List conditions = new ArrayList<>(); - List parameters = new ArrayList<>(); - + if (recipientPairs == null || recipientPairs.isEmpty()) { + return 0; + } + List notificationIds = new ArrayList<>(); if (notificationId != null) { - conditions.add("NOTIFICATIONID = ?"); - parameters.add(notificationId); + if (isNotificationVisibleToUser(notificationId, recipientPairs)) { + notificationIds.add(notificationId); + } + } else { + notificationIds.addAll(fetchVisibleNotificationIds(recipientPairs)); } - String recipientCondition = buildRecipientSqlCondition(recipientPairs, parameters); - if (recipientCondition != null) { - conditions.add(recipientCondition); + int count = 0; + Timestamp dismissedAt = Utility.getCurrentSqlTimestampUTC(); + Pair statePair = firstValidPair(recipientPairs); + if (statePair == null) { + return 0; } - if (conditions.isEmpty() || (notificationId == null && recipientCondition == null)) { - return 0; // nothing to delete + for (String id : notificationIds) { + count += upsertNotificationState(id, statePair, true, dismissedAt, true, dismissedAt); } - - deleteQuery.append(String.join(" AND ", conditions)); - PreparedStatement ps = null; - int deletedCount = 0; - try { - ps = notificationDb.getPreparedStatement(deleteQuery.toString()); - int index = 1; - for (Object param : parameters) { - ps.setObject(index++, param); - } - - deletedCount = ps.executeUpdate(); - - if (!ps.getConnection().getAutoCommit()) { - ps.getConnection().commit(); - } - } catch (SQLException e) { - classLogger.error("Failed to delete notification(s) [notificationId={}, recipientPairs={}]", - notificationId, recipientPairs, e); - } finally { - ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); - } - return deletedCount; + return count; } - /** - * Updates notification action type for a given user. - * - * @param user the user whose notifications need to be updated - */ public static void resetNotificationActionType(User user) { - IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); List> userIdAndTypeList = User.getUserIdAndType(user); if (userIdAndTypeList.isEmpty()) { return; } - String updateQuery = "UPDATE NOTIFICATION SET ACTIONTYPE = 'NONE' WHERE ACTIONTYPE = 'NEW' AND RECIPIENTID=? AND RECIPIENTTYPE = ?"; - PreparedStatement ps = null; - try { - ps = notificationDb.getPreparedStatement(updateQuery); - for (Pair pair : userIdAndTypeList) { - String recipientId = pair.getValue0(); - String recipientType = pair.getValue1(); - - ps.setString(1, recipientId); - ps.setString(2, recipientType); - ps.executeUpdate(); - } - - Connection conn = (ps != null) ? ps.getConnection() : null; - if (conn != null && !conn.getAutoCommit()) { - conn.commit(); - } - } catch (SQLException e) { - classLogger.error("Failed to reset notification action type from NEW to NONE for user id/type pairs {}", - userIdAndTypeList, e); - } finally { - ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); + Pair statePair = firstValidPair(userIdAndTypeList); + if (statePair == null) { + return; + } + Timestamp readAt = Utility.getCurrentSqlTimestampUTC(); + for (String notificationId : fetchVisibleNotificationIds(userIdAndTypeList)) { + upsertNotificationState(notificationId, statePair, true, readAt, null, null); } } - /** - * Marks a notification as read and updates the read date. - * - * @param notificationId -the ID of the notification - * @param readDate -the timestamp when the notification was read - */ public static void markNotificationRead(String notificationId, Timestamp readDate) { markNotificationRead(notificationId, readDate, null); } public static int markNotificationRead(String notificationId, Timestamp readDate, List> recipientPairs) { - IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); - List parameters = new ArrayList<>(); - parameters.add(readDate); - parameters.add(notificationId); - StringBuilder query = new StringBuilder("UPDATE NOTIFICATION SET ISREAD = TRUE, READDATE=? WHERE NOTIFICATIONID=?"); - String recipientCondition = buildRecipientSqlCondition(recipientPairs, parameters); - if (recipientCondition != null) { - query.append(" AND ").append(recipientCondition); + if (notificationId == null || recipientPairs == null || recipientPairs.isEmpty() + || !isNotificationVisibleToUser(notificationId, recipientPairs)) { + return 0; } - PreparedStatement ps = null; - try { - ps = notificationDb.getPreparedStatement(query.toString()); - int parameterIndex = 1; - for (Object param : parameters) { - ps.setObject(parameterIndex++, param); - } - int updatedCount = ps.executeUpdate(); - if (!ps.getConnection().getAutoCommit()) { - ps.getConnection().commit(); - } - return updatedCount; - } catch (SQLException e) { - classLogger.error("Failed to mark notification {} as read (readDate={}, recipientPairs={})", notificationId, - readDate, recipientPairs, e); - } finally { - ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); + Pair statePair = firstValidPair(recipientPairs); + if (statePair == null) { + return 0; } - return 0; + return upsertNotificationState(notificationId, statePair, true, readDate, null, null); } - /** - * Retrieves the count of new notifications for a given user and type. - * - * @param recipientId -the ID of the recipient - * @param recipientType -the type of the recipient (e.g., NATIVE, MS) - * @return the count of new notifications - */ public static int fetchNewNotificationCount(String recipientId, String recipientType) { List> recipientPairs = new ArrayList<>(); if (recipientId != null && recipientType != null) { @@ -517,21 +392,34 @@ public static int fetchNewNotificationCount(String recipientId, String recipient } public static int fetchNewNotificationCount(List> recipientPairs) { - IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); - PreparedStatement ps = null; - List parameters = new ArrayList<>(); - String recipientCondition = buildRecipientSqlCondition(recipientPairs, parameters); - if (recipientCondition == null) { + if (recipientPairs == null || recipientPairs.isEmpty()) { return 0; } - String query = "SELECT COUNT(NOTIFICATIONID) FROM NOTIFICATION WHERE ACTIONTYPE = 'NEW' AND " - + recipientCondition; + IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); + List audienceParameters = new ArrayList<>(); + List dismissedParameters = new ArrayList<>(); + List readParameters = new ArrayList<>(); + String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, audienceParameters); + String dismissedCondition = buildStateExistsSqlCondition("us", recipientPairs, dismissedParameters, + "us.IS_DISMISSED = TRUE"); + String readCondition = buildStateExistsSqlCondition("urs", recipientPairs, readParameters, + "urs.IS_READ = TRUE"); + List parameters = new ArrayList<>(); + parameters.addAll(audienceParameters); + parameters.add(NotificationConstants.Status.EXPIRED); + parameters.addAll(dismissedParameters); + parameters.addAll(readParameters); + String query = "SELECT COUNT(n.NOTIFICATION_ID) FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + + "AND n.STATUS <> ? " + "AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) " + + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + + dismissedCondition + ") " + + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE urs WHERE urs.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + + readCondition + ")"; + + PreparedStatement ps = null; try { ps = notificationDb.getPreparedStatement(query); - int parameterIndex = 1; - for (Object param : parameters) { - ps.setObject(parameterIndex++, param); - } + setParameters(ps, parameters); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return rs.getInt(1); @@ -545,34 +433,388 @@ public static int fetchNewNotificationCount(List> recipient return 0; } - private static IQueryFilter buildRecipientFilter(List> recipientPairs) { - OrQueryFilter orCombined = new OrQueryFilter(); - for (Pair pair : recipientPairs) { - List andFilters = new ArrayList<>(); - andFilters.add(SimpleQueryFilter.makeColToValFilter("NOTIFICATION__RECIPIENTID", "==", pair.getValue0())); - andFilters.add(SimpleQueryFilter.makeColToValFilter("NOTIFICATION__RECIPIENTTYPE", "==", pair.getValue1())); - orCombined.addFilter(new AndQueryFilter(andFilters)); + private static List> executeNotificationFetch(String query, List parameters) { + IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); + PreparedStatement ps = null; + List> rows = new ArrayList<>(); + try { + ps = notificationDb.getPreparedStatement(query); + setParameters(ps, parameters); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + rows.add(mapNotificationRow(rs)); + } + } + } catch (SQLException e) { + classLogger.error("Failed to fetch notifications", e); + } finally { + ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } - return orCombined; + return rows; } - private static String buildRecipientSqlCondition(List> recipientPairs, List parameters) { - if (recipientPairs == null || recipientPairs.isEmpty()) { - return null; + private static Map mapNotificationRow(ResultSet rs) throws SQLException { + Map row = new HashMap<>(); + JsonObject metadata = parseMetadata(getString(rs, "METADATA_JSON")); + String legacyType = getMetadataString(metadata, "legacyNotificationType"); + String sourceType = getString(rs, "SOURCE_TYPE"); + String sourceId = getString(rs, "SOURCE_ID"); + String targetId = getString(rs, "TARGET_ID"); + String catalogId = targetId != null ? targetId : sourceId; + + row.put("notification_id", getString(rs, "NOTIFICATION_ID")); + row.put("recipient_id", getString(rs, "AUDIENCE_ID")); + row.put("recipient_type", getString(rs, "AUDIENCE_USER_TYPE")); + row.put("notification_title", getString(rs, "TITLE")); + row.put("notification_message", getString(rs, "MESSAGE")); + row.put("notification_actiontype", Boolean.TRUE.equals(rs.getObject("IS_READ")) ? "NONE" : "NEW"); + row.put("notification_actiontarget", getString(rs, "TARGET_URL")); + row.put("notification_isread", rs.getBoolean("IS_READ")); + row.put("notification_priority", getString(rs, "PRIORITY")); + row.put("notification_type", legacyType == null ? getString(rs, "TYPE") : legacyType); + row.put("catalog_id", catalogId); + row.put("notification_createddate", rs.getTimestamp("CREATED_AT")); + row.put("notification_readdate", null); + row.put("notification_source", deriveLegacyNotificationSource(sourceType, getString(rs, "SCOPE_TYPE"))); + row.put("recipient_user_id", getMetadataString(metadata, "affectedUserId")); + row.put("notification_usertype", getMetadataString(metadata, "affectedUserType")); + row.put("user_existingrole", getMetadataString(metadata, "affectedUserPreviousRole")); + row.put("user_newrole", getMetadataString(metadata, "affectedUserNewRole")); + row.put("notification_createdby", getString(rs, "CREATED_BY")); + row.put("kind", getString(rs, "KIND")); + row.put("type", getString(rs, "TYPE")); + row.put("scope_type", getString(rs, "SCOPE_TYPE")); + row.put("scope_id", getString(rs, "SCOPE_ID")); + row.put("target_type", getString(rs, "TARGET_TYPE")); + row.put("target_id", targetId); + row.put("target_url", getString(rs, "TARGET_URL")); + row.put("action_label", getString(rs, "ACTION_LABEL")); + row.put("status", getString(rs, "STATUS")); + row.put("group_id", getString(rs, "GROUP_ID")); + return row; + } + + private static void hydrateLegacyDisplayFields(List> notificationList) { + if (notificationList == null || notificationList.isEmpty()) { + return; + } + Set userIds = new HashSet<>(); + Set catalogIds = new HashSet<>(); + for (Map row : notificationList) { + if (row.get("recipient_user_id") != null) { + userIds.add(String.valueOf(row.get("recipient_user_id"))); + } + if (row.get("notification_createdby") != null) { + userIds.add(String.valueOf(row.get("notification_createdby"))); + } + if (row.get("catalog_id") != null) { + catalogIds.add(String.valueOf(row.get("catalog_id"))); + } + } + Map userIdToNameMap = SecurityUserUtils.getUserNamesByIds(userIds); + Map projectIdToNameMap = SecurityProjectUtils.getProjectNamesByIds(catalogIds); + Map engineIdToNameMap = SecurityEngineUtils.getEngineNamesByIds(catalogIds); + + for (Map row : notificationList) { + String catalogId = row.get("catalog_id") == null ? null : String.valueOf(row.get("catalog_id")); + String notificationSource = row.get("notification_source") == null ? null + : String.valueOf(row.get("notification_source")); + Object affectedUserId = row.get("recipient_user_id"); + row.put("recipient_user_name", affectedUserId == null ? "Unknown User" + : userIdToNameMap.getOrDefault(affectedUserId, "Unknown User")); + + String projectName = catalogId == null ? null : projectIdToNameMap.get(catalogId); + String engineName = catalogId == null ? null : engineIdToNameMap.get(catalogId); + if (NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource)) { + row.put("catalog_name", projectName != null && !projectName.isEmpty() ? projectName : engineName); + } else { + row.put("catalog_name", engineName != null && !engineName.isEmpty() ? engineName : projectName); + } + } + } + + private static List fetchVisibleNotificationIds(List> recipientPairs) { + List audienceParameters = new ArrayList<>(); + List dismissedParameters = new ArrayList<>(); + String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, audienceParameters); + String dismissedCondition = buildStateExistsSqlCondition("us", recipientPairs, dismissedParameters, + "us.IS_DISMISSED = TRUE"); + List parameters = new ArrayList<>(); + parameters.addAll(audienceParameters); + parameters.add(NotificationConstants.Status.EXPIRED); + parameters.addAll(dismissedParameters); + String query = "SELECT n.NOTIFICATION_ID FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + + "AND n.STATUS <> ? " + "AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) " + + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + + dismissedCondition + ")"; + List ids = new ArrayList<>(); + IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); + PreparedStatement ps = null; + try { + ps = notificationDb.getPreparedStatement(query); + setParameters(ps, parameters); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + ids.add(rs.getString(1)); + } + } + } catch (SQLException e) { + classLogger.error("Failed to fetch visible notification ids for recipient pairs {}", recipientPairs, e); + } finally { + ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); + } + return ids; + } + + private static boolean isNotificationVisibleToUser(String notificationId, + List> recipientPairs) { + List audienceParameters = new ArrayList<>(); + List dismissedParameters = new ArrayList<>(); + String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, audienceParameters); + String dismissedCondition = buildStateExistsSqlCondition("us", recipientPairs, dismissedParameters, + "us.IS_DISMISSED = TRUE"); + List parameters = new ArrayList<>(); + parameters.addAll(audienceParameters); + parameters.add(notificationId); + parameters.add(NotificationConstants.Status.EXPIRED); + parameters.addAll(dismissedParameters); + String query = "SELECT 1 FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + + "AND n.NOTIFICATION_ID = ? AND n.STATUS <> ? " + + "AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) " + + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + + dismissedCondition + ")"; + IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); + PreparedStatement ps = null; + try { + ps = notificationDb.getPreparedStatement(query); + setParameters(ps, parameters); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } catch (SQLException e) { + classLogger.error("Failed to check notification visibility [notificationId={}, recipientPairs={}]", + notificationId, recipientPairs, e); + } finally { + ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); + } + return false; + } + + private static int upsertNotificationState(String notificationId, Pair userPair, Boolean isRead, + Timestamp readAt, Boolean isDismissed, Timestamp dismissedAt) { + IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); + PreparedStatement updatePs = null; + PreparedStatement insertPs = null; + try { + List sets = new ArrayList<>(); + List parameters = new ArrayList<>(); + if (isRead != null) { + sets.add("IS_READ = ?"); + parameters.add(isRead); + sets.add("READ_AT = ?"); + parameters.add(readAt); + } + if (isDismissed != null) { + sets.add("IS_DISMISSED = ?"); + parameters.add(isDismissed); + sets.add("DISMISSED_AT = ?"); + parameters.add(dismissedAt); + } + if (sets.isEmpty()) { + return 0; + } + parameters.add(notificationId); + parameters.add(userPair.getValue0()); + parameters.add(userPair.getValue1()); + String update = "UPDATE NOTIFICATION_USER_STATE SET " + String.join(", ", sets) + + " WHERE NOTIFICATION_ID = ? AND USER_ID = ? AND USER_TYPE = ?"; + updatePs = notificationDb.getPreparedStatement(update); + setParameters(updatePs, parameters); + int updated = updatePs.executeUpdate(); + if (updated == 0) { + insertPs = notificationDb.getPreparedStatement( + "INSERT INTO NOTIFICATION_USER_STATE (NOTIFICATION_ID,USER_ID,USER_TYPE,IS_READ,READ_AT,IS_DISMISSED,DISMISSED_AT) VALUES (?,?,?,?,?,?,?)"); + insertPs.setString(1, notificationId); + insertPs.setString(2, userPair.getValue0()); + insertPs.setString(3, userPair.getValue1()); + insertPs.setBoolean(4, isRead != null && isRead.booleanValue()); + insertPs.setTimestamp(5, readAt); + insertPs.setBoolean(6, isDismissed != null && isDismissed.booleanValue()); + insertPs.setTimestamp(7, dismissedAt); + insertPs.executeUpdate(); + } + Connection conn = updatePs.getConnection(); + if (conn != null && !conn.getAutoCommit()) { + conn.commit(); + } + return 1; + } catch (SQLException e) { + classLogger.error("Failed to upsert notification state [notificationId={}, userPair={}]", notificationId, + userPair, e); + } finally { + ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, insertPs); + ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, updatePs); } - List recipientConditions = new ArrayList<>(); + return 0; + } + + private static String buildVisibleAudienceSqlCondition(List> recipientPairs, + List parameters) { + List conditions = new ArrayList<>(); + for (Pair pair : recipientPairs) { + if (pair == null || pair.getValue0() == null) { + continue; + } + conditions.add( + "(n.AUDIENCE_TYPE = ? AND n.AUDIENCE_ID = ? AND (n.AUDIENCE_USER_TYPE = ? OR n.AUDIENCE_USER_TYPE IS NULL))"); + parameters.add(NotificationConstants.Audience.USER); + parameters.add(pair.getValue0()); + parameters.add(pair.getValue1()); + } + conditions.add("(n.AUDIENCE_TYPE = ?)"); + parameters.add(NotificationConstants.Audience.GLOBAL); + return String.join(" OR ", conditions); + } + + private static String buildStateExistsSqlCondition(String alias, List> recipientPairs, + List parameters, String statePredicate) { + List pairConditions = new ArrayList<>(); for (Pair pair : recipientPairs) { if (pair == null || pair.getValue0() == null || pair.getValue1() == null) { continue; } - recipientConditions.add("(RECIPIENTID = ? AND RECIPIENTTYPE = ?)"); + pairConditions.add("(" + alias + ".USER_ID = ? AND " + alias + ".USER_TYPE = ?)"); parameters.add(pair.getValue0()); parameters.add(pair.getValue1()); } - if (recipientConditions.isEmpty()) { + if (pairConditions.isEmpty()) { + pairConditions.add("1 = 0"); + } + return "(" + String.join(" OR ", pairConditions) + ") AND " + statePredicate; + } + + private static Pair firstValidPair(List> recipientPairs) { + for (Pair pair : recipientPairs) { + if (pair != null && pair.getValue0() != null && pair.getValue1() != null) { + return pair; + } + } + return null; + } + + private static void setParameters(PreparedStatement ps, List parameters) throws SQLException { + for (int i = 0; i < parameters.size(); i++) { + ps.setObject(i + 1, parameters.get(i)); + } + } + + private static Long parseLong(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + return ((Number) Double.parseDouble(value)).longValue(); + } + + private static String deriveKind(String notificationType) { + if (NotificationConstants.Type.USER_REQUEST.equalsIgnoreCase(notificationType)) { + return NotificationConstants.Kind.ACTION; + } + return NotificationConstants.Kind.INFO; + } + + private static String deriveType(String notificationType) { + if (NotificationConstants.Type.USER_REQUEST.equalsIgnoreCase(notificationType)) { + return NotificationConstants.Type.ACCESS_REQUEST; + } + return notificationType; + } + + private static String deriveScopeType(String notificationSource) { + if (NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource)) { + return NotificationConstants.Scope.APP; + } + return NotificationConstants.Scope.SYSTEM; + } + + private static String deriveSourceType(String notificationSource) { + if (NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource)) { + return NotificationConstants.Source.PROJECT; + } + if (notificationSource == null || notificationSource.trim().isEmpty()) { + return NotificationConstants.Source.SYSTEM; + } + return NotificationConstants.Source.ENGINE; + } + + private static String deriveLegacyNotificationSource(String sourceType, String scopeType) { + if (NotificationConstants.Scope.APP.equalsIgnoreCase(scopeType) + || NotificationConstants.Source.PROJECT.equalsIgnoreCase(sourceType)) { + return NotificationConstants.APP_CATALOG; + } + return sourceType; + } + + private static String normalizePriority(String priority) { + if (priority == null || priority.trim().isEmpty()) { + return NotificationConstants.Priority.NORMAL; + } + if (NotificationConstants.Priority.MEDIUM.equalsIgnoreCase(priority)) { + return NotificationConstants.Priority.NORMAL; + } + return priority.toUpperCase(); + } + + private static String buildLegacyMetadata(String affectedUserId, String affectedUserType, + String affectedUserPreviousRole, String affectedUserNewRole, String legacyNotificationType, + String legacyNotificationSource) { + JsonObject metadata = new JsonObject(); + addJsonProperty(metadata, "affectedUserId", affectedUserId); + addJsonProperty(metadata, "affectedUserType", affectedUserType); + addJsonProperty(metadata, "affectedUserPreviousRole", affectedUserPreviousRole); + addJsonProperty(metadata, "affectedUserNewRole", affectedUserNewRole); + addJsonProperty(metadata, "legacyNotificationType", legacyNotificationType); + addJsonProperty(metadata, "legacyNotificationSource", legacyNotificationSource); + return metadata.toString(); + } + + private static void addJsonProperty(JsonObject metadata, String key, String value) { + if (value == null) { + return; + } + metadata.addProperty(key, value); + } + + private static JsonObject parseMetadata(String metadataJson) { + if (metadataJson == null || metadataJson.trim().isEmpty()) { + return new JsonObject(); + } + try { + JsonElement element = JsonParser.parseString(metadataJson); + return element != null && element.isJsonObject() ? element.getAsJsonObject() : new JsonObject(); + } catch (Exception e) { + classLogger.warn("Failed to parse notification metadata json", e); + return new JsonObject(); + } + } + + private static String getMetadataString(JsonObject metadata, String key) { + JsonElement element = metadata.get(key); + if (element == null || element.isJsonNull()) { + return null; + } + return element.getAsString(); + } + + private static String getString(ResultSet rs, String column) throws SQLException { + Object value = rs.getObject(column); + if (value == null) { return null; } - return "(" + String.join(" OR ", recipientConditions) + ")"; + if (value instanceof Clob) { + Clob clob = (Clob) value; + return clob.getSubString(1L, (int) clob.length()); + } + return String.valueOf(value); } } diff --git a/src/prerna/notifications/NotificationOwlCreator.java b/src/prerna/notifications/NotificationOwlCreator.java index aab8139d714..620452d0e4a 100644 --- a/src/prerna/notifications/NotificationOwlCreator.java +++ b/src/prerna/notifications/NotificationOwlCreator.java @@ -50,26 +50,50 @@ public void createColumnsAndTypes(AbstractSqlQueryUtil queryUtil) { this.allSchemas = new ArrayList<>(); // @formatter:off - addTable("NOTIFICATION", Arrays.asList( - Pair.with("NOTIFICATIONID", VARCHAR_255), - Pair.with("RECIPIENTID", VARCHAR_255), - Pair.with("RECIPIENTTYPE", VARCHAR_255), - Pair.with("NOTIFICATIONTITLE", VARCHAR_255), + addTable("NOTIFICATION_EVENT", Arrays.asList( + Pair.with("NOTIFICATION_ID", "VARCHAR(50)"), + Pair.with("KIND", "VARCHAR(20)"), + Pair.with("TYPE", "VARCHAR(50)"), + Pair.with("SCOPE_TYPE", "VARCHAR(20)"), + Pair.with("SCOPE_ID", "VARCHAR(50)"), + Pair.with("AUDIENCE_TYPE", "VARCHAR(20)"), + Pair.with("AUDIENCE_ID", VARCHAR_255), + Pair.with("AUDIENCE_USER_TYPE", "VARCHAR(50)"), + Pair.with("TITLE", VARCHAR_255), Pair.with("MESSAGE", CLOB_DATATYPE_NAME), - Pair.with("ACTIONTYPE", "VARCHAR(50)"), - Pair.with("ACTIONTARGET", VARCHAR_255), - Pair.with("ISREAD", BOOLEAN_DATATYPE_NAME), Pair.with("PRIORITY", "VARCHAR(20)"), - Pair.with("NOTIFICATIONTYPE", VARCHAR_255), - Pair.with("CATALOGID", VARCHAR_255), - Pair.with("CREATEDBY", VARCHAR_255), - Pair.with("CREATEDDATE", TIMESTAMP_DATATYPE_NAME), - Pair.with("READDATE", TIMESTAMP_DATATYPE_NAME), - Pair.with("NOTIFICATIONSOURCE", VARCHAR_255), - Pair.with("USERID", VARCHAR_255), - Pair.with("USERTYPE", VARCHAR_255), - Pair.with("USEREXISTINGROLE", VARCHAR_255), - Pair.with("USERNEWROLE", VARCHAR_255))); + Pair.with("SOURCE_TYPE", "VARCHAR(20)"), + Pair.with("SOURCE_ID", "VARCHAR(50)"), + Pair.with("TARGET_TYPE", "VARCHAR(30)"), + Pair.with("TARGET_ID", "VARCHAR(50)"), + Pair.with("TARGET_URL", CLOB_DATATYPE_NAME), + Pair.with("ACTION_LABEL", "VARCHAR(50)"), + Pair.with("STATUS", "VARCHAR(20)"), + Pair.with("GROUP_ID", "VARCHAR(50)"), + Pair.with("METADATA_JSON", CLOB_DATATYPE_NAME), + Pair.with("CREATED_BY", VARCHAR_255), + Pair.with("CREATED_AT", TIMESTAMP_DATATYPE_NAME), + Pair.with("RESOLVED_AT", TIMESTAMP_DATATYPE_NAME), + Pair.with("EXPIRES_AT", TIMESTAMP_DATATYPE_NAME))); + addTable("NOTIFICATION_USER_STATE", Arrays.asList( + Pair.with("NOTIFICATION_ID", "VARCHAR(50)"), + Pair.with("USER_ID", VARCHAR_255), + Pair.with("USER_TYPE", "VARCHAR(50)"), + Pair.with("IS_READ", BOOLEAN_DATATYPE_NAME), + Pair.with("READ_AT", TIMESTAMP_DATATYPE_NAME), + Pair.with("IS_DISMISSED", BOOLEAN_DATATYPE_NAME), + Pair.with("DISMISSED_AT", TIMESTAMP_DATATYPE_NAME))); + addTable("NOTIFICATION_DELIVERY", Arrays.asList( + Pair.with("DELIVERY_ID", "VARCHAR(50)"), + Pair.with("NOTIFICATION_ID", "VARCHAR(50)"), + Pair.with("USER_ID", VARCHAR_255), + Pair.with("USER_TYPE", "VARCHAR(50)"), + Pair.with("CHANNEL", "VARCHAR(20)"), + Pair.with("STATUS", "VARCHAR(20)"), + Pair.with("ATTEMPTS", "INT"), + Pair.with("LAST_ERROR", CLOB_DATATYPE_NAME), + Pair.with("CREATED_AT", TIMESTAMP_DATATYPE_NAME), + Pair.with("SENT_AT", TIMESTAMP_DATATYPE_NAME))); // @formatter:on } } diff --git a/src/prerna/util/NotificationConstants.java b/src/prerna/util/NotificationConstants.java index 0a013b67c1d..711535c0dc8 100644 --- a/src/prerna/util/NotificationConstants.java +++ b/src/prerna/util/NotificationConstants.java @@ -36,11 +36,18 @@ private NotificationConstants() { public static final String APP_CATALOG = "APP"; public static final class Priority { + public static final String URGENT = "URGENT"; public static final String HIGH = "HIGH"; + public static final String NORMAL = "NORMAL"; public static final String MEDIUM = "MEDIUM"; public static final String LOW = "LOW"; } + public static final class Kind { + public static final String INFO = "INFO"; + public static final String ACTION = "ACTION"; + } + public static final class Type { public static final String USER_REQUEST = "USER_REQUEST"; public static final String USER_ADDITION = "USER_ADDITION"; @@ -48,6 +55,60 @@ public static final class Type { public static final String PERMISSION_CHANGE = "PERMISSION_CHANGE"; public static final String REQUEST_DENIAL = "REQUEST_DENIAL"; public static final String SMSS_UPDATE = "SMSS_UPDATE"; + public static final String ACCESS_REQUEST = "ACCESS_REQUEST"; + public static final String AGENT_ACTION_REQUIRED = "AGENT_ACTION_REQUIRED"; + public static final String ANNOUNCEMENT = "ANNOUNCEMENT"; + public static final String APP_TASK_COMPLETE = "APP_TASK_COMPLETE"; + } + + public static final class Scope { + public static final String SYSTEM = "SYSTEM"; + public static final String APP = "APP"; + } + + public static final class Audience { + public static final String USER = "USER"; + public static final String APP_MEMBERS = "APP_MEMBERS"; + public static final String APP_OWNERS = "APP_OWNERS"; + public static final String APP_EDITORS = "APP_EDITORS"; + public static final String GLOBAL = "GLOBAL"; + } + + public static final class Source { + public static final String SYSTEM = "SYSTEM"; + public static final String USER = "USER"; + public static final String AGENT = "AGENT"; + public static final String ENGINE = "ENGINE"; + public static final String PROJECT = "PROJECT"; + } + + public static final class Target { + public static final String NONE = "NONE"; + public static final String ROUTE = "ROUTE"; + public static final String APP = "APP"; + public static final String ROOM = "ROOM"; + public static final String AGENT_RUN = "AGENT_RUN"; + public static final String AGENT_ACTION = "AGENT_ACTION"; + } + + public static final class Status { + public static final String ACTIVE = "ACTIVE"; + public static final String RESOLVED = "RESOLVED"; + public static final String EXPIRED = "EXPIRED"; + } + + public static final class DeliveryChannel { + public static final String EMAIL = "EMAIL"; + public static final String SLACK = "SLACK"; + public static final String TEAMS = "TEAMS"; + public static final String WEBHOOK = "WEBHOOK"; + } + + public static final class DeliveryStatus { + public static final String PENDING = "PENDING"; + public static final String SENT = "SENT"; + public static final String FAILED = "FAILED"; + public static final String SKIPPED = "SKIPPED"; } } From 4a080fef1293b6450202a7a481eedeca0ab57af2 Mon Sep 17 00:00:00 2001 From: Kunal Patel Date: Thu, 9 Jul 2026 13:05:51 -0400 Subject: [PATCH 3/5] feat: allow multiple display types for notifications --- .../auth/utils/SecurityEngineUtils.java | 38 +++++++++-------- .../auth/utils/SecurityProjectUtils.java | 42 ++++++++++--------- .../notifications/NotificationDbUtils.java | 19 +++++++-- .../notifications/NotificationOwlCreator.java | 1 + .../reactor/engine/RequestEngineReactor.java | 5 ++- .../project/RequestProjectReactor.java | 6 +-- src/prerna/util/NotificationConstants.java | 16 +++++++ 7 files changed, 81 insertions(+), 46 deletions(-) diff --git a/src/prerna/auth/utils/SecurityEngineUtils.java b/src/prerna/auth/utils/SecurityEngineUtils.java index 27893e22708..74966e2c365 100644 --- a/src/prerna/auth/utils/SecurityEngineUtils.java +++ b/src/prerna/auth/utils/SecurityEngineUtils.java @@ -665,9 +665,10 @@ public static void approveEngineUserAccessRequests(User user, String engineId, L if (Utility.isNotificationDatabaseEnabled()) { String engineType = String.valueOf(getEngineType(engineId)).toLowerCase(); for (int i = 0; i < requests.size(); i++) { - NotificationDbUtils.createNotification(user, requests.get(i).get("userid"), - requests.get(i).get("type"), engineId, NotificationConstants.Type.REQUEST_APPROVAL, - engineType, NotificationConstants.Priority.MEDIUM, null, requests.get(i).get("permission")); + NotificationDbUtils.createNotification(user, requests.get(i).get("userid"), + requests.get(i).get("type"), engineId, NotificationConstants.Type.REQUEST_APPROVAL, + engineType, NotificationConstants.Priority.MEDIUM, null, requests.get(i).get("permission"), + NotificationConstants.DisplaySurface.BELL); // Adding email notification EmailUtility.sendAccessRequestApprovalEmailNotification(user, requests.get(i).get("userid"), engineId, requests.get(i).get("permission"), EmailUtility.RESOURCE_TYPE.ENGINE); @@ -738,10 +739,11 @@ public static void denyEngineUserAccessRequests(User user, String engineId, List List> deniedUserDetails = getUserDetailsFromEngineAccessRequest(requestId); String permission = AccessPermissionEnum .getPermissionValueById((Integer) deniedUserDetails.get(i).get("permission")); - NotificationDbUtils.createNotification(user, (String) deniedUserDetails.get(i).get("userId"), - (String) deniedUserDetails.get(i).get("type"), engineId, - NotificationConstants.Type.REQUEST_DENIAL, engineType, - NotificationConstants.Priority.MEDIUM, null, permission); + NotificationDbUtils.createNotification(user, (String) deniedUserDetails.get(i).get("userId"), + (String) deniedUserDetails.get(i).get("type"), engineId, + NotificationConstants.Type.REQUEST_DENIAL, engineType, + NotificationConstants.Priority.MEDIUM, null, permission, + NotificationConstants.DisplaySurface.BELL); } } @@ -1154,10 +1156,10 @@ public static void addEngineUserPermissions(User user, String engineId, List> deniedUserDetails = getUserDetailsFromProjectAccessRequest(requestId); String permission = AccessPermissionEnum .getPermissionValueById((Integer) deniedUserDetails.get(i).get("permission")); - NotificationDbUtils.createNotification(user, (String) deniedUserDetails.get(i).get("userId"), - (String) deniedUserDetails.get(i).get("type"), projectId, - NotificationConstants.Type.REQUEST_DENIAL, NotificationConstants.APP_CATALOG, - NotificationConstants.Priority.MEDIUM, null, permission); + NotificationDbUtils.createNotification(user, (String) deniedUserDetails.get(i).get("userId"), + (String) deniedUserDetails.get(i).get("type"), projectId, + NotificationConstants.Type.REQUEST_DENIAL, NotificationConstants.APP_CATALOG, + NotificationConstants.Priority.MEDIUM, null, permission, + NotificationConstants.DisplaySurface.BELL); } } @@ -4825,10 +4827,10 @@ public static void addProjectUserPermissions(User user, String projectId, List> authorRecipients = NotificationConstants.APP_CATALOG .equalsIgnoreCase(notificationSource) ? SecurityProjectUtils.getProjectAuthors(catalogId) @@ -224,7 +224,7 @@ public static void createNotification(User loggedInUser, String affectedUserId, String metadataJson = buildLegacyMetadata(affectedUserId, affectedUserType, affectedUserPreviousRole, affectedUserNewRole, notificationType, notificationSource); - String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,KIND,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,TARGET_URL,ACTION_LABEL,STATUS,GROUP_ID,METADATA_JSON,CREATED_BY,CREATED_AT,RESOLVED_AT,EXPIRES_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,KIND,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,DISPLAY_SURFACE,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,TARGET_URL,ACTION_LABEL,STATUS,GROUP_ID,METADATA_JSON,CREATED_BY,CREATED_AT,RESOLVED_AT,EXPIRES_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; for (Map recipient : recipients) { if (recipient == null || recipient.get("userId") == null) { continue; @@ -245,6 +245,7 @@ public static void createNotification(User loggedInUser, String affectedUserId, ps.setString(parameterIndex++, "NOTIFICATION"); ps.setString(parameterIndex++, null); ps.setString(parameterIndex++, normalizePriority(priority)); + ps.setString(parameterIndex++, normalizeDisplaySurface(displaySurface)); ps.setString(parameterIndex++, sourceType); ps.setString(parameterIndex++, catalogId); ps.setString(parameterIndex++, targetType); @@ -288,7 +289,7 @@ public static List> fetchAllNotifications(User user, String StringBuilder query = new StringBuilder(); query.append("SELECT n.NOTIFICATION_ID, n.KIND, n.TYPE, n.SCOPE_TYPE, n.SCOPE_ID, n.AUDIENCE_TYPE, ") - .append("n.AUDIENCE_ID, n.AUDIENCE_USER_TYPE, n.TITLE, n.MESSAGE, n.PRIORITY, n.SOURCE_TYPE, ") + .append("n.AUDIENCE_ID, n.AUDIENCE_USER_TYPE, n.TITLE, n.MESSAGE, n.PRIORITY, n.DISPLAY_SURFACE, n.SOURCE_TYPE, ") .append("n.SOURCE_ID, n.TARGET_TYPE, n.TARGET_ID, n.TARGET_URL, n.ACTION_LABEL, n.STATUS, ") .append("n.GROUP_ID, n.METADATA_JSON, n.CREATED_BY, n.CREATED_AT, n.RESOLVED_AT, n.EXPIRES_AT, ") .append("CASE WHEN EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE urs WHERE urs.NOTIFICATION_ID = n.NOTIFICATION_ID AND ") @@ -471,6 +472,7 @@ private static Map mapNotificationRow(ResultSet rs) throws SQLEx row.put("notification_actiontarget", getString(rs, "TARGET_URL")); row.put("notification_isread", rs.getBoolean("IS_READ")); row.put("notification_priority", getString(rs, "PRIORITY")); + row.put("display_surface", normalizeDisplaySurface(getString(rs, "DISPLAY_SURFACE"))); row.put("notification_type", legacyType == null ? getString(rs, "TYPE") : legacyType); row.put("catalog_id", catalogId); row.put("notification_createddate", rs.getTimestamp("CREATED_AT")); @@ -764,6 +766,17 @@ private static String normalizePriority(String priority) { return priority.toUpperCase(); } + private static String normalizeDisplaySurface(String displaySurface) { + if (displaySurface == null || displaySurface.trim().isEmpty()) { + return NotificationConstants.DisplaySurface.BELL; + } + String normalized = displaySurface.trim().toUpperCase(); + if (NotificationConstants.DisplaySurface.isValid(normalized)) { + return normalized; + } + return NotificationConstants.DisplaySurface.BELL; + } + private static String buildLegacyMetadata(String affectedUserId, String affectedUserType, String affectedUserPreviousRole, String affectedUserNewRole, String legacyNotificationType, String legacyNotificationSource) { diff --git a/src/prerna/notifications/NotificationOwlCreator.java b/src/prerna/notifications/NotificationOwlCreator.java index 620452d0e4a..ea2f8dea12e 100644 --- a/src/prerna/notifications/NotificationOwlCreator.java +++ b/src/prerna/notifications/NotificationOwlCreator.java @@ -62,6 +62,7 @@ public void createColumnsAndTypes(AbstractSqlQueryUtil queryUtil) { Pair.with("TITLE", VARCHAR_255), Pair.with("MESSAGE", CLOB_DATATYPE_NAME), Pair.with("PRIORITY", "VARCHAR(20)"), + Pair.with("DISPLAY_SURFACE", "VARCHAR(20)"), Pair.with("SOURCE_TYPE", "VARCHAR(20)"), Pair.with("SOURCE_ID", "VARCHAR(50)"), Pair.with("TARGET_TYPE", "VARCHAR(30)"), diff --git a/src/prerna/reactor/engine/RequestEngineReactor.java b/src/prerna/reactor/engine/RequestEngineReactor.java index 2cd79178159..5cd829cfb0a 100644 --- a/src/prerna/reactor/engine/RequestEngineReactor.java +++ b/src/prerna/reactor/engine/RequestEngineReactor.java @@ -106,8 +106,9 @@ public NounMetadata execute() { if (Utility.isNotificationDatabaseEnabled()) { String priority = AccessPermissionEnum.isOwner(requestPermission) ? NotificationConstants.Priority.HIGH : NotificationConstants.Priority.MEDIUM; - NotificationDbUtils.createNotification(user, userId, userType, engineId, - NotificationConstants.Type.USER_REQUEST, engineType, priority, null, permission); + NotificationDbUtils.createNotification(user, userId, userType, engineId, + NotificationConstants.Type.USER_REQUEST, engineType, priority, null, permission, + NotificationConstants.DisplaySurface.BELL); EmailUtility.sendAccessRequestEmailNotification(user, engineId, permission, requestComment, EmailUtility.RESOURCE_TYPE.ENGINE); diff --git a/src/prerna/reactor/project/RequestProjectReactor.java b/src/prerna/reactor/project/RequestProjectReactor.java index c8b33b36f33..d20158c1efa 100644 --- a/src/prerna/reactor/project/RequestProjectReactor.java +++ b/src/prerna/reactor/project/RequestProjectReactor.java @@ -103,9 +103,9 @@ public NounMetadata execute() { if (Utility.isNotificationDatabaseEnabled()) { String priority = AccessPermissionEnum.isOwner(requestPermission) ? NotificationConstants.Priority.HIGH : NotificationConstants.Priority.MEDIUM; - NotificationDbUtils.createNotification(user, userId, userType, projectId, - NotificationConstants.Type.USER_REQUEST, NotificationConstants.APP_CATALOG, priority, null, - permission); + NotificationDbUtils.createNotification(user, userId, userType, projectId, + NotificationConstants.Type.USER_REQUEST, NotificationConstants.APP_CATALOG, priority, null, + permission, NotificationConstants.DisplaySurface.BELL); EmailUtility.sendAccessRequestEmailNotification(user, projectId, permission, requestComment, EmailUtility.RESOURCE_TYPE.PROJECT); diff --git a/src/prerna/util/NotificationConstants.java b/src/prerna/util/NotificationConstants.java index 711535c0dc8..e108597c7dd 100644 --- a/src/prerna/util/NotificationConstants.java +++ b/src/prerna/util/NotificationConstants.java @@ -27,6 +27,8 @@ *******************************************************************************/ package prerna.util; +import java.util.Set; + public final class NotificationConstants { private NotificationConstants() { @@ -48,6 +50,20 @@ public static final class Kind { public static final String ACTION = "ACTION"; } + // in-app render surface (NOT external delivery - that is DeliveryChannel) + public static final class DisplaySurface { + public static final String BELL = "BELL"; + public static final String MODAL = "MODAL"; + public static final String TOAST = "TOAST"; + public static final String BANNER = "BANNER"; + + private static final Set VALUES = Set.of(BELL, MODAL, TOAST, BANNER); + + public static boolean isValid(String displaySurface) { + return VALUES.contains(displaySurface); + } + } + public static final class Type { public static final String USER_REQUEST = "USER_REQUEST"; public static final String USER_ADDITION = "USER_ADDITION"; From f9cd2b1d28e1938203248a61b266af13417350ad Mon Sep 17 00:00:00 2001 From: Kunal Patel Date: Thu, 9 Jul 2026 15:25:33 -0400 Subject: [PATCH 4/5] feat: enable app based notifications --- .../notifications/NotificationDbUtils.java | 179 ++++++++++++++++-- .../notifications/NotificationService.java | 119 ++++++++++++ .../CreateAppNotificationReactor.java | 77 ++++++++ .../DeleteNotificationReactor.java | 19 +- .../FetchAppNotificationsReactor.java | 89 +++++++++ .../FetchNotificationsReactor.java | 34 +++- .../MarkNotificationReadReactor.java | 14 +- .../PollAppNotificationsReactor.java | 70 +++++++ .../PollNotificationsReactor.java | 40 ++-- src/prerna/util/NotificationConstants.java | 19 ++ 10 files changed, 601 insertions(+), 59 deletions(-) create mode 100644 src/prerna/notifications/NotificationService.java create mode 100644 src/prerna/reactor/notification/CreateAppNotificationReactor.java create mode 100644 src/prerna/reactor/notification/FetchAppNotificationsReactor.java create mode 100644 src/prerna/reactor/notification/PollAppNotificationsReactor.java diff --git a/src/prerna/notifications/NotificationDbUtils.java b/src/prerna/notifications/NotificationDbUtils.java index e26ff04db07..89c940e731f 100644 --- a/src/prerna/notifications/NotificationDbUtils.java +++ b/src/prerna/notifications/NotificationDbUtils.java @@ -273,15 +273,75 @@ public static void createNotification(User loggedInUser, String affectedUserId, } } + static String insertNotificationEvent(String kind, String type, String scopeType, String scopeId, + String audienceType, String audienceId, String audienceUserType, String title, String message, + String priority, String displaySurface, String sourceType, String sourceId, String targetType, + String targetId, String targetUrl, String actionLabel, String status, String groupId, String metadataJson, + String createdBy) { + IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); + String notificationId = GUID.v7().toUUID().toString(); + String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,KIND,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,DISPLAY_SURFACE,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,TARGET_URL,ACTION_LABEL,STATUS,GROUP_ID,METADATA_JSON,CREATED_BY,CREATED_AT,RESOLVED_AT,EXPIRES_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + PreparedStatement ps = null; + try { + ps = notificationDb.getPreparedStatement(query); + int parameterIndex = 1; + ps.setString(parameterIndex++, notificationId); + ps.setString(parameterIndex++, kind); + ps.setString(parameterIndex++, type); + ps.setString(parameterIndex++, scopeType); + ps.setString(parameterIndex++, scopeId); + ps.setString(parameterIndex++, audienceType); + ps.setString(parameterIndex++, audienceId); + ps.setString(parameterIndex++, audienceUserType); + ps.setString(parameterIndex++, title); + ps.setString(parameterIndex++, message); + ps.setString(parameterIndex++, priority); + ps.setString(parameterIndex++, normalizeDisplaySurface(displaySurface)); + ps.setString(parameterIndex++, sourceType); + ps.setString(parameterIndex++, sourceId); + ps.setString(parameterIndex++, targetType); + ps.setString(parameterIndex++, targetId); + ps.setString(parameterIndex++, targetUrl); + ps.setString(parameterIndex++, actionLabel); + ps.setString(parameterIndex++, status); + ps.setString(parameterIndex++, groupId); + ps.setString(parameterIndex++, metadataJson); + ps.setString(parameterIndex++, createdBy); + ps.setTimestamp(parameterIndex++, Utility.getCurrentSqlTimestampUTC()); + ps.setTimestamp(parameterIndex++, null); + ps.setTimestamp(parameterIndex++, null); + ps.execute(); + if (!ps.getConnection().getAutoCommit()) { + ps.getConnection().commit(); + } + return notificationId; + } catch (SQLException e) { + classLogger.error("Failed to insert notification event [type={}, scopeType={}, scopeId={}]", type, + scopeType, scopeId, e); + throw new IllegalStateException("Unable to create notification", e); + } finally { + ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); + } + } + public static List> fetchAllNotifications(User user, String limit, String offset) { + return fetchNotifications(user, NotificationConstants.FetchScope.ALL, null, limit, offset); + } + + public static List> fetchNotifications(User user, String scopeType, String scopeId, String limit, + String offset) { List> userIdAndTypeList = User.getUserIdAndType(user); if (userIdAndTypeList.isEmpty()) { return new ArrayList<>(); } + List accessibleProjectIds = getAccessibleProjectIds(user); List audienceParameters = new ArrayList<>(); + List scopeParameters = new ArrayList<>(); List dismissedParameters = new ArrayList<>(); List readParameters = new ArrayList<>(); - String audienceCondition = buildVisibleAudienceSqlCondition(userIdAndTypeList, audienceParameters); + String audienceCondition = buildVisibleAudienceSqlCondition(userIdAndTypeList, accessibleProjectIds, + audienceParameters); + String scopeCondition = buildVisibleScopeSqlCondition(scopeType, scopeId, accessibleProjectIds, scopeParameters); String dismissedCondition = buildStateExistsSqlCondition("us", userIdAndTypeList, dismissedParameters, "us.IS_DISMISSED = TRUE"); String readCondition = buildStateExistsSqlCondition("urs", userIdAndTypeList, readParameters, @@ -295,12 +355,14 @@ public static List> fetchAllNotifications(User user, String .append("CASE WHEN EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE urs WHERE urs.NOTIFICATION_ID = n.NOTIFICATION_ID AND ") .append(readCondition).append(") THEN TRUE ELSE FALSE END AS IS_READ ") .append("FROM NOTIFICATION_EVENT n WHERE (").append(audienceCondition).append(") ") + .append("AND (").append(scopeCondition).append(") ") .append("AND n.STATUS <> ? ").append("AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) ") .append("AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND ") .append(dismissedCondition).append(") ").append("ORDER BY n.CREATED_AT DESC"); List parameters = new ArrayList<>(); parameters.addAll(readParameters); parameters.addAll(audienceParameters); + parameters.addAll(scopeParameters); parameters.add(NotificationConstants.Status.EXPIRED); parameters.addAll(dismissedParameters); @@ -328,17 +390,27 @@ public static int deleteNotification(String recipientId, String recipientType, S return deleteNotification(recipientPairs, notificationId); } + public static int deleteNotification(User user, String notificationId) { + return deleteNotification(User.getUserIdAndType(user), getAccessibleProjectIds(user), notificationId); + } + public static int deleteNotification(List> recipientPairs, String notificationId) { + return deleteNotification(recipientPairs, new ArrayList<>(), notificationId); + } + + private static int deleteNotification(List> recipientPairs, List accessibleProjectIds, + String notificationId) { if (recipientPairs == null || recipientPairs.isEmpty()) { return 0; } List notificationIds = new ArrayList<>(); if (notificationId != null) { - if (isNotificationVisibleToUser(notificationId, recipientPairs)) { + if (isNotificationVisibleToUser(notificationId, recipientPairs, accessibleProjectIds)) { notificationIds.add(notificationId); } } else { - notificationIds.addAll(fetchVisibleNotificationIds(recipientPairs)); + notificationIds.addAll(fetchVisibleNotificationIds(recipientPairs, accessibleProjectIds, + NotificationConstants.FetchScope.ALL, null)); } int count = 0; Timestamp dismissedAt = Utility.getCurrentSqlTimestampUTC(); @@ -353,6 +425,10 @@ public static int deleteNotification(List> recipientPairs, } public static void resetNotificationActionType(User user) { + resetNotificationActionType(user, NotificationConstants.FetchScope.ALL, null); + } + + public static void resetNotificationActionType(User user, String scopeType, String scopeId) { List> userIdAndTypeList = User.getUserIdAndType(user); if (userIdAndTypeList.isEmpty()) { return; @@ -362,7 +438,8 @@ public static void resetNotificationActionType(User user) { return; } Timestamp readAt = Utility.getCurrentSqlTimestampUTC(); - for (String notificationId : fetchVisibleNotificationIds(userIdAndTypeList)) { + for (String notificationId : fetchVisibleNotificationIds(userIdAndTypeList, getAccessibleProjectIds(user), + scopeType, scopeId)) { upsertNotificationState(notificationId, statePair, true, readAt, null, null); } } @@ -373,8 +450,17 @@ public static void markNotificationRead(String notificationId, Timestamp readDat public static int markNotificationRead(String notificationId, Timestamp readDate, List> recipientPairs) { + return markNotificationRead(notificationId, readDate, recipientPairs, new ArrayList<>()); + } + + public static int markNotificationRead(User user, String notificationId, Timestamp readDate) { + return markNotificationRead(notificationId, readDate, User.getUserIdAndType(user), getAccessibleProjectIds(user)); + } + + private static int markNotificationRead(String notificationId, Timestamp readDate, + List> recipientPairs, List accessibleProjectIds) { if (notificationId == null || recipientPairs == null || recipientPairs.isEmpty() - || !isNotificationVisibleToUser(notificationId, recipientPairs)) { + || !isNotificationVisibleToUser(notificationId, recipientPairs, accessibleProjectIds)) { return 0; } Pair statePair = firstValidPair(recipientPairs); @@ -392,25 +478,39 @@ public static int fetchNewNotificationCount(String recipientId, String recipient return fetchNewNotificationCount(recipientPairs); } + public static int fetchNewNotificationCount(User user, String scopeType, String scopeId) { + return fetchNewNotificationCount(User.getUserIdAndType(user), getAccessibleProjectIds(user), scopeType, scopeId); + } + public static int fetchNewNotificationCount(List> recipientPairs) { + return fetchNewNotificationCount(recipientPairs, new ArrayList<>(), NotificationConstants.FetchScope.ALL, null); + } + + private static int fetchNewNotificationCount(List> recipientPairs, + List accessibleProjectIds, String scopeType, String scopeId) { if (recipientPairs == null || recipientPairs.isEmpty()) { return 0; } IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); List audienceParameters = new ArrayList<>(); + List scopeParameters = new ArrayList<>(); List dismissedParameters = new ArrayList<>(); List readParameters = new ArrayList<>(); - String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, audienceParameters); + String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, accessibleProjectIds, + audienceParameters); + String scopeCondition = buildVisibleScopeSqlCondition(scopeType, scopeId, accessibleProjectIds, scopeParameters); String dismissedCondition = buildStateExistsSqlCondition("us", recipientPairs, dismissedParameters, "us.IS_DISMISSED = TRUE"); String readCondition = buildStateExistsSqlCondition("urs", recipientPairs, readParameters, "urs.IS_READ = TRUE"); List parameters = new ArrayList<>(); parameters.addAll(audienceParameters); + parameters.addAll(scopeParameters); parameters.add(NotificationConstants.Status.EXPIRED); parameters.addAll(dismissedParameters); parameters.addAll(readParameters); String query = "SELECT COUNT(n.NOTIFICATION_ID) FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + + "AND (" + scopeCondition + ") " + "AND n.STATUS <> ? " + "AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) " + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + dismissedCondition + ") " @@ -535,17 +635,23 @@ private static void hydrateLegacyDisplayFields(List> notific } } - private static List fetchVisibleNotificationIds(List> recipientPairs) { + private static List fetchVisibleNotificationIds(List> recipientPairs, + List accessibleProjectIds, String scopeType, String scopeId) { List audienceParameters = new ArrayList<>(); + List scopeParameters = new ArrayList<>(); List dismissedParameters = new ArrayList<>(); - String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, audienceParameters); + String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, accessibleProjectIds, + audienceParameters); + String scopeCondition = buildVisibleScopeSqlCondition(scopeType, scopeId, accessibleProjectIds, scopeParameters); String dismissedCondition = buildStateExistsSqlCondition("us", recipientPairs, dismissedParameters, "us.IS_DISMISSED = TRUE"); List parameters = new ArrayList<>(); parameters.addAll(audienceParameters); + parameters.addAll(scopeParameters); parameters.add(NotificationConstants.Status.EXPIRED); parameters.addAll(dismissedParameters); String query = "SELECT n.NOTIFICATION_ID FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + + "AND (" + scopeCondition + ") " + "AND n.STATUS <> ? " + "AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) " + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + dismissedCondition + ")"; @@ -569,19 +675,24 @@ private static List fetchVisibleNotificationIds(List> recipientPairs) { + List> recipientPairs, List accessibleProjectIds) { List audienceParameters = new ArrayList<>(); + List scopeParameters = new ArrayList<>(); List dismissedParameters = new ArrayList<>(); - String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, audienceParameters); + String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, accessibleProjectIds, + audienceParameters); + String scopeCondition = buildVisibleScopeSqlCondition(NotificationConstants.FetchScope.ALL, null, + accessibleProjectIds, scopeParameters); String dismissedCondition = buildStateExistsSqlCondition("us", recipientPairs, dismissedParameters, "us.IS_DISMISSED = TRUE"); List parameters = new ArrayList<>(); parameters.addAll(audienceParameters); + parameters.addAll(scopeParameters); parameters.add(notificationId); parameters.add(NotificationConstants.Status.EXPIRED); parameters.addAll(dismissedParameters); String query = "SELECT 1 FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " - + "AND n.NOTIFICATION_ID = ? AND n.STATUS <> ? " + + "AND (" + scopeCondition + ") " + "AND n.NOTIFICATION_ID = ? AND n.STATUS <> ? " + "AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) " + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + dismissedCondition + ")"; @@ -661,7 +772,7 @@ private static int upsertNotificationState(String notificationId, Pair> recipientPairs, - List parameters) { + List accessibleProjectIds, List parameters) { List conditions = new ArrayList<>(); for (Pair pair : recipientPairs) { if (pair == null || pair.getValue0() == null) { @@ -673,11 +784,55 @@ private static String buildVisibleAudienceSqlCondition(List parameters.add(pair.getValue0()); parameters.add(pair.getValue1()); } + if (accessibleProjectIds != null && !accessibleProjectIds.isEmpty()) { + conditions.add("(n.AUDIENCE_TYPE = ? AND n.SCOPE_TYPE = ? AND n.SCOPE_ID IN (" + + String.join(",", java.util.Collections.nCopies(accessibleProjectIds.size(), "?")) + "))"); + parameters.add(NotificationConstants.Audience.APP_MEMBERS); + parameters.add(NotificationConstants.Scope.APP); + parameters.addAll(accessibleProjectIds); + } conditions.add("(n.AUDIENCE_TYPE = ?)"); parameters.add(NotificationConstants.Audience.GLOBAL); return String.join(" OR ", conditions); } + private static String buildVisibleScopeSqlCondition(String scopeType, String scopeId, + List accessibleProjectIds, List parameters) { + String normalizedScopeType = scopeType == null ? NotificationConstants.FetchScope.ALL : scopeType.trim().toUpperCase(); + if (!NotificationConstants.FetchScope.isValid(normalizedScopeType)) { + throw new IllegalArgumentException("Notification scopeType must be ALL, SYSTEM, or APP"); + } + if (NotificationConstants.FetchScope.SYSTEM.equals(normalizedScopeType)) { + parameters.add(NotificationConstants.Scope.SYSTEM); + return "n.SCOPE_TYPE = ?"; + } + if (NotificationConstants.FetchScope.APP.equals(normalizedScopeType)) { + if (scopeId == null || scopeId.trim().isEmpty()) { + throw new IllegalArgumentException("Notification scopeId is required when scopeType is APP"); + } + parameters.add(NotificationConstants.Scope.APP); + parameters.add(scopeId.trim()); + return "n.SCOPE_TYPE = ? AND n.SCOPE_ID = ?"; + } + if (accessibleProjectIds == null || accessibleProjectIds.isEmpty()) { + parameters.add(NotificationConstants.Scope.SYSTEM); + return "n.SCOPE_TYPE = ?"; + } + parameters.add(NotificationConstants.Scope.SYSTEM); + parameters.add(NotificationConstants.Scope.APP); + parameters.addAll(accessibleProjectIds); + return "n.SCOPE_TYPE = ? OR (n.SCOPE_TYPE = ? AND n.SCOPE_ID IN (" + + String.join(",", java.util.Collections.nCopies(accessibleProjectIds.size(), "?")) + "))"; + } + + private static List getAccessibleProjectIds(User user) { + if (user == null) { + return new ArrayList<>(); + } + return SecurityProjectUtils.getUserProjectIdList(user, true, false, true).stream().distinct() + .collect(java.util.stream.Collectors.toList()); + } + private static String buildStateExistsSqlCondition(String alias, List> recipientPairs, List parameters, String statePredicate) { List pairConditions = new ArrayList<>(); diff --git a/src/prerna/notifications/NotificationService.java b/src/prerna/notifications/NotificationService.java new file mode 100644 index 00000000000..cc6e45ffe30 --- /dev/null +++ b/src/prerna/notifications/NotificationService.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.notifications; + +import org.apache.commons.lang3.StringUtils; + +import prerna.auth.AccessToken; +import prerna.auth.AuthProvider; +import prerna.auth.User; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.om.Insight; +import prerna.util.NotificationConstants; + +/** Public notification creation boundary for application-facing flows. */ +public final class NotificationService { + + private NotificationService() { + } + + public static String createAppAnnouncement(User creator, String projectId, String title, String message, + String priority) { + if (!SecurityProjectUtils.userCanEditProject(creator, projectId)) { + throw new IllegalArgumentException("Project does not exist or user is not an editor of the project"); + } + return createAppNotification(projectId, NotificationConstants.Audience.APP_MEMBERS, null, null, + creator == null ? null : User.getSingleLogginName(creator), title, message, priority); + } + + /** + * Notifies the authenticated insight user within the insight's active app context. + * This has no public reactor because callers must not be able to target other users. + */ + public static String createAppUserNotification(Insight insight, String title, String message, String priority) { + if (insight == null) { + throw new IllegalArgumentException("Insight is required to create an app user notification"); + } + String projectId = requireValue(insight.getContextProjectId(), "app context project id"); + User user = insight.getUser(); + if (user == null || user.isAnonymous()) { + throw new IllegalArgumentException("A signed-in insight user is required to create an app user notification"); + } + if (!SecurityProjectUtils.userCanViewProject(user, projectId)) { + throw new IllegalArgumentException("Insight user does not have access to the active app project"); + } + + AuthProvider provider = user.getPrimaryLogin(); + if (provider == null) { + throw new IllegalArgumentException("Insight user does not have a primary login provider"); + } + AccessToken token = user.getAccessToken(provider); + if (token == null) { + throw new IllegalArgumentException("Insight user does not have an access token for the primary login provider"); + } + return createAppUserNotification(projectId, requireValue(token.getId(), "user id"), provider.toString(), title, + message, priority); + } + + private static String createAppUserNotification(String projectId, String userId, String userType, String title, + String message, String priority) { + return createAppNotification(projectId, NotificationConstants.Audience.USER, requireValue(userId, "user id"), + requireValue(userType, "user type"), userId, title, message, priority); + } + + private static String createAppNotification(String projectId, String audienceType, String audienceId, + String audienceUserType, String createdBy, String title, String message, String priority) { + String normalizedProjectId = requireValue(projectId, "project id"); + String normalizedTitle = requireValue(title, "title"); + if (normalizedTitle.length() > 255) { + throw new IllegalArgumentException("Notification title cannot exceed 255 characters"); + } + String normalizedMessage = requireValue(message, "message"); + String normalizedPriority = normalizePriority(priority); + return NotificationDbUtils.insertNotificationEvent(NotificationConstants.Kind.INFO, + NotificationConstants.Type.ANNOUNCEMENT, NotificationConstants.Scope.APP, normalizedProjectId, + audienceType, audienceId, audienceUserType, normalizedTitle, normalizedMessage, normalizedPriority, + NotificationConstants.DisplaySurface.BELL, NotificationConstants.Source.PROJECT, normalizedProjectId, + NotificationConstants.Target.APP, normalizedProjectId, null, null, NotificationConstants.Status.ACTIVE, + null, null, createdBy); + } + + private static String normalizePriority(String priority) { + String normalized = StringUtils.defaultIfBlank(priority, NotificationConstants.Priority.NORMAL).trim() + .toUpperCase(); + if (!NotificationConstants.Priority.isValid(normalized)) { + throw new IllegalArgumentException("Notification priority must be LOW, NORMAL, HIGH, or URGENT"); + } + return normalized; + } + + private static String requireValue(String value, String label) { + if (StringUtils.isBlank(value)) { + throw new IllegalArgumentException("Notification " + label + " is required"); + } + return value.trim(); + } +} diff --git a/src/prerna/reactor/notification/CreateAppNotificationReactor.java b/src/prerna/reactor/notification/CreateAppNotificationReactor.java new file mode 100644 index 00000000000..2342a4ef486 --- /dev/null +++ b/src/prerna/reactor/notification/CreateAppNotificationReactor.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.notification; + +import java.util.HashMap; +import java.util.Map; + +import prerna.auth.User; +import prerna.auth.utils.AbstractSecurityUtils; +import prerna.notifications.NotificationService; +import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.Utility; + +/** Creates an app-wide announcement for the authenticated app editor/owner. */ +public class CreateAppNotificationReactor extends AbstractReactor { + + private static final String TITLE = "title"; + private static final String PRIORITY = "priority"; + + public CreateAppNotificationReactor() { + this.keysToGet = new String[] { ReactorKeysEnum.PROJECT.getKey(), TITLE, ReactorKeysEnum.MESSAGE.getKey(), + PRIORITY }; + this.keyRequired = new int[] { 1, 1, 1, 0 }; + } + + @Override + public NounMetadata execute() { + if (!Utility.isNotificationDatabaseEnabled()) { + throw new IllegalArgumentException("Notifications are not enabled on this instance"); + } + organizeKeys(); + User user = this.insight.getUser(); + if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { + throwAnonymousUserError(); + } + + String notificationId = NotificationService.createAppAnnouncement(user, + this.keyValue.get(ReactorKeysEnum.PROJECT.getKey()), this.keyValue.get(TITLE), + this.keyValue.get(ReactorKeysEnum.MESSAGE.getKey()), this.keyValue.get(PRIORITY)); + Map response = new HashMap<>(); + response.put("notificationId", notificationId); + response.put("scopeType", "APP"); + response.put("audienceType", "APP_MEMBERS"); + return new NounMetadata(response, PixelDataType.MAP); + } + + @Override + public String getReactorDescription() { + return "Create an announcement for all users of an app"; + } +} diff --git a/src/prerna/reactor/notification/DeleteNotificationReactor.java b/src/prerna/reactor/notification/DeleteNotificationReactor.java index 954838da9ed..928efae6274 100644 --- a/src/prerna/reactor/notification/DeleteNotificationReactor.java +++ b/src/prerna/reactor/notification/DeleteNotificationReactor.java @@ -27,18 +27,12 @@ *******************************************************************************/ package prerna.reactor.notification; -import java.util.List; - -import org.javatuples.Pair; - import prerna.auth.User; import prerna.auth.utils.AbstractSecurityUtils; import prerna.notifications.NotificationDbUtils; import prerna.reactor.AbstractReactor; import prerna.sablecc2.om.PixelDataType; -import prerna.sablecc2.om.PixelOperationType; import prerna.sablecc2.om.ReactorKeysEnum; -import prerna.sablecc2.om.execptions.SemossPixelException; import prerna.sablecc2.om.nounmeta.NounMetadata; import prerna.util.Utility; @@ -62,18 +56,7 @@ public NounMetadata execute() { organizeKeys(); String notificationId = this.keyValue.get(this.keysToGet[0]); - List> userIdAndTypeList = User.getUserIdAndType(user); - if (userIdAndTypeList == null || userIdAndTypeList.isEmpty()) { - throw new SemossPixelException(new NounMetadata("Unable to determine user type for deletion", - PixelDataType.CONST_STRING, PixelOperationType.ERROR, PixelOperationType.LOGGIN_REQUIRED_ERROR)); - } - - int deleteCount; - if (notificationId != null) { - deleteCount = NotificationDbUtils.deleteNotification(userIdAndTypeList, notificationId); - } else { - deleteCount = NotificationDbUtils.deleteNotification(userIdAndTypeList, null); - } + int deleteCount = NotificationDbUtils.deleteNotification(user, notificationId); return new NounMetadata(deleteCount, PixelDataType.CONST_INT); } diff --git a/src/prerna/reactor/notification/FetchAppNotificationsReactor.java b/src/prerna/reactor/notification/FetchAppNotificationsReactor.java new file mode 100644 index 00000000000..96f5db4f27c --- /dev/null +++ b/src/prerna/reactor/notification/FetchAppNotificationsReactor.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.notification; + +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import prerna.auth.User; +import prerna.auth.utils.AbstractSecurityUtils; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.notifications.NotificationDbUtils; +import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.NotificationConstants; +import prerna.util.Utility; + +/** Fetches notifications scoped to the project bound to the current insight. */ +public class FetchAppNotificationsReactor extends AbstractReactor { + + public FetchAppNotificationsReactor() { + this.keysToGet = new String[] { ReactorKeysEnum.LIMIT.getKey(), ReactorKeysEnum.OFFSET.getKey() }; + this.keyRequired = new int[] { 0, 0 }; + } + + @Override + public NounMetadata execute() { + if (!Utility.isNotificationDatabaseEnabled()) { + throw new IllegalArgumentException("Notifications are not enabled on this instance"); + } + organizeKeys(); + User user = this.insight.getUser(); + if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { + throwAnonymousUserError(); + } + + String projectId = getInsightProjectId(); + if (!SecurityProjectUtils.userCanViewProject(user, projectId)) { + throw new IllegalArgumentException("Project does not exist or user does not have access to the project"); + } + + List> notifications = NotificationDbUtils.fetchNotifications(user, + NotificationConstants.FetchScope.APP, projectId, this.keyValue.get(ReactorKeysEnum.LIMIT.getKey()), + this.keyValue.get(ReactorKeysEnum.OFFSET.getKey())); + if (!notifications.isEmpty()) { + NotificationDbUtils.resetNotificationActionType(user, NotificationConstants.FetchScope.APP, projectId); + } + return new NounMetadata(notifications, PixelDataType.MAP); + } + + @Override + public String getReactorDescription() { + return "Fetch notifications for the project bound to the current insight"; + } + + private String getInsightProjectId() { + String projectId = this.insight.getContextProjectId(); + if (StringUtils.isBlank(projectId)) { + throw new IllegalStateException("Current insight is not associated with an app project"); + } + return projectId; + } +} diff --git a/src/prerna/reactor/notification/FetchNotificationsReactor.java b/src/prerna/reactor/notification/FetchNotificationsReactor.java index 37cd7e72b1a..a02fa24803b 100644 --- a/src/prerna/reactor/notification/FetchNotificationsReactor.java +++ b/src/prerna/reactor/notification/FetchNotificationsReactor.java @@ -32,6 +32,7 @@ import prerna.auth.User; import prerna.auth.utils.AbstractSecurityUtils; +import prerna.auth.utils.SecurityProjectUtils; import prerna.notifications.NotificationDbUtils; import prerna.reactor.AbstractReactor; import prerna.sablecc2.om.PixelDataType; @@ -39,13 +40,17 @@ import prerna.sablecc2.om.ReactorKeysEnum; import prerna.sablecc2.om.execptions.SemossPixelException; import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.NotificationConstants; import prerna.util.Utility; public class FetchNotificationsReactor extends AbstractReactor { + private static final String SCOPE_TYPE = "scopeType"; + private static final String SCOPE_ID = "scopeId"; public FetchNotificationsReactor() { - this.keysToGet = new String[] { ReactorKeysEnum.LIMIT.getKey(), ReactorKeysEnum.OFFSET.getKey() }; - this.keyRequired = new int[] { 0, 0 }; + this.keysToGet = new String[] { ReactorKeysEnum.LIMIT.getKey(), ReactorKeysEnum.OFFSET.getKey(), SCOPE_TYPE, + SCOPE_ID }; + this.keyRequired = new int[] { 0, 0, 0, 0 }; } @Override @@ -57,6 +62,8 @@ public NounMetadata execute() { User user = this.insight.getUser(); String limit = this.keyValue.get(ReactorKeysEnum.LIMIT.getKey()); String offset = this.keyValue.get(ReactorKeysEnum.OFFSET.getKey()); + String scopeType = normalizeScopeType(this.keyValue.get(SCOPE_TYPE)); + String scopeId = this.keyValue.get(SCOPE_ID); if (user == null) { NounMetadata noun = new NounMetadata( "User must be signed into an account to retrieve the function engine files", @@ -68,11 +75,15 @@ public NounMetadata execute() { if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { throwAnonymousUserError(); } + if (NotificationConstants.FetchScope.APP.equals(scopeType) + && !SecurityProjectUtils.userCanViewProject(user, scopeId)) { + throw new IllegalArgumentException("Project does not exist or user does not have access to the project"); + } - List> allNotifications = null; - allNotifications = NotificationDbUtils.fetchAllNotifications(user, limit, offset); + List> allNotifications = NotificationDbUtils.fetchNotifications(user, scopeType, scopeId, limit, + offset); if (!allNotifications.isEmpty()) { - NotificationDbUtils.resetNotificationActionType(user); + NotificationDbUtils.resetNotificationActionType(user, scopeType, scopeId); } return new NounMetadata(allNotifications, PixelDataType.MAP); @@ -82,4 +93,17 @@ public NounMetadata execute() { public String getReactorDescription() { return "Fetch all user notifications"; } + + private String normalizeScopeType(String scopeType) { + String normalized = scopeType == null || scopeType.trim().isEmpty() ? NotificationConstants.FetchScope.ALL + : scopeType.trim().toUpperCase(); + if (!NotificationConstants.FetchScope.isValid(normalized)) { + throw new IllegalArgumentException("Notification scopeType must be ALL, SYSTEM, or APP"); + } + if (NotificationConstants.FetchScope.APP.equals(normalized) + && (this.keyValue.get(SCOPE_ID) == null || this.keyValue.get(SCOPE_ID).trim().isEmpty())) { + throw new IllegalArgumentException("Notification scopeId is required when scopeType is APP"); + } + return normalized; + } } diff --git a/src/prerna/reactor/notification/MarkNotificationReadReactor.java b/src/prerna/reactor/notification/MarkNotificationReadReactor.java index bfa58d231b8..c6138ef2e56 100644 --- a/src/prerna/reactor/notification/MarkNotificationReadReactor.java +++ b/src/prerna/reactor/notification/MarkNotificationReadReactor.java @@ -28,18 +28,11 @@ package prerna.reactor.notification; import java.sql.Timestamp; -import java.util.List; -import org.javatuples.Pair; - -import prerna.auth.User; import prerna.auth.utils.AbstractSecurityUtils; import prerna.notifications.NotificationDbUtils; import prerna.reactor.AbstractReactor; -import prerna.sablecc2.om.PixelDataType; -import prerna.sablecc2.om.PixelOperationType; import prerna.sablecc2.om.ReactorKeysEnum; -import prerna.sablecc2.om.execptions.SemossPixelException; import prerna.sablecc2.om.nounmeta.NounMetadata; import prerna.util.Utility; @@ -63,12 +56,7 @@ public NounMetadata execute() { organizeKeys(); String notificationId = this.keyValue.get(this.keysToGet[0]); Timestamp readAt = Utility.getCurrentSqlTimestampUTC(); - List> userIdAndTypeList = User.getUserIdAndType(this.insight.getUser()); - if (userIdAndTypeList == null || userIdAndTypeList.isEmpty()) { - throw new SemossPixelException(new NounMetadata("Unable to determine user type for notification update", - PixelDataType.CONST_STRING, PixelOperationType.ERROR, PixelOperationType.LOGGIN_REQUIRED_ERROR)); - } - NotificationDbUtils.markNotificationRead(notificationId, readAt, userIdAndTypeList); + NotificationDbUtils.markNotificationRead(this.insight.getUser(), notificationId, readAt); NounMetadata retNoun = NounMetadata.getSuccessNounMessage("Success!"); return retNoun; } diff --git a/src/prerna/reactor/notification/PollAppNotificationsReactor.java b/src/prerna/reactor/notification/PollAppNotificationsReactor.java new file mode 100644 index 00000000000..e1068d0ce48 --- /dev/null +++ b/src/prerna/reactor/notification/PollAppNotificationsReactor.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.notification; + +import org.apache.commons.lang3.StringUtils; + +import prerna.auth.User; +import prerna.auth.utils.AbstractSecurityUtils; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.notifications.NotificationDbUtils; +import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.NotificationConstants; +import prerna.util.Utility; + +/** Counts unread notifications for the project bound to the current insight. */ +public class PollAppNotificationsReactor extends AbstractReactor { + + @Override + public NounMetadata execute() { + if (!Utility.isNotificationDatabaseEnabled()) { + throw new IllegalArgumentException("Notifications are not enabled on this instance"); + } + User user = this.insight.getUser(); + if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { + throwAnonymousUserError(); + } + + String projectId = this.insight.getContextProjectId(); + if (StringUtils.isBlank(projectId)) { + throw new IllegalStateException("Current insight is not associated with an app project"); + } + if (!SecurityProjectUtils.userCanViewProject(user, projectId)) { + throw new IllegalArgumentException("Project does not exist or user does not have access to the project"); + } + + int count = NotificationDbUtils.fetchNewNotificationCount(user, NotificationConstants.FetchScope.APP, + projectId); + return new NounMetadata(count, PixelDataType.CONST_INT); + } + + @Override + public String getReactorDescription() { + return "Get unread notifications for the project bound to the current insight"; + } +} diff --git a/src/prerna/reactor/notification/PollNotificationsReactor.java b/src/prerna/reactor/notification/PollNotificationsReactor.java index 06601585400..4bd93803b9f 100644 --- a/src/prerna/reactor/notification/PollNotificationsReactor.java +++ b/src/prerna/reactor/notification/PollNotificationsReactor.java @@ -27,39 +27,44 @@ *******************************************************************************/ package prerna.reactor.notification; -import java.util.List; - -import org.javatuples.Pair; - import prerna.auth.User; import prerna.auth.utils.AbstractSecurityUtils; +import prerna.auth.utils.SecurityProjectUtils; import prerna.notifications.NotificationDbUtils; import prerna.reactor.AbstractReactor; import prerna.sablecc2.om.PixelDataType; -import prerna.sablecc2.om.PixelOperationType; -import prerna.sablecc2.om.execptions.SemossPixelException; import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.NotificationConstants; import prerna.util.Utility; public class PollNotificationsReactor extends AbstractReactor { + private static final String SCOPE_TYPE = "scopeType"; + private static final String SCOPE_ID = "scopeId"; + + public PollNotificationsReactor() { + this.keysToGet = new String[] { SCOPE_TYPE, SCOPE_ID }; + this.keyRequired = new int[] { 0, 0 }; + } @Override public NounMetadata execute() { if (!Utility.isNotificationDatabaseEnabled()) { throw new IllegalArgumentException("Notifications are not enabled on this instance"); } + organizeKeys(); User user = this.insight.getUser(); if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { throwAnonymousUserError(); } - List> userIdAndTypeList = User.getUserIdAndType(user); - if (userIdAndTypeList == null || userIdAndTypeList.isEmpty()) { - throw new SemossPixelException(new NounMetadata("Unable to determine user type for deletion", - PixelDataType.CONST_STRING, PixelOperationType.ERROR, PixelOperationType.LOGGIN_REQUIRED_ERROR)); + String scopeType = normalizeScopeType(this.keyValue.get(SCOPE_TYPE)); + String scopeId = this.keyValue.get(SCOPE_ID); + if (NotificationConstants.FetchScope.APP.equals(scopeType) + && !SecurityProjectUtils.userCanViewProject(user, scopeId)) { + throw new IllegalArgumentException("Project does not exist or user does not have access to the project"); } - int newNotificationCount = NotificationDbUtils.fetchNewNotificationCount(userIdAndTypeList); + int newNotificationCount = NotificationDbUtils.fetchNewNotificationCount(user, scopeType, scopeId); return new NounMetadata(newNotificationCount, PixelDataType.CONST_INT); } @@ -67,4 +72,17 @@ public NounMetadata execute() { public String getReactorDescription() { return "Get the number of new notifications for the user"; } + + private String normalizeScopeType(String scopeType) { + String normalized = scopeType == null || scopeType.trim().isEmpty() ? NotificationConstants.FetchScope.ALL + : scopeType.trim().toUpperCase(); + if (!NotificationConstants.FetchScope.isValid(normalized)) { + throw new IllegalArgumentException("Notification scopeType must be ALL, SYSTEM, or APP"); + } + if (NotificationConstants.FetchScope.APP.equals(normalized) + && (this.keyValue.get(SCOPE_ID) == null || this.keyValue.get(SCOPE_ID).trim().isEmpty())) { + throw new IllegalArgumentException("Notification scopeId is required when scopeType is APP"); + } + return normalized; + } } diff --git a/src/prerna/util/NotificationConstants.java b/src/prerna/util/NotificationConstants.java index e108597c7dd..59ab7d21660 100644 --- a/src/prerna/util/NotificationConstants.java +++ b/src/prerna/util/NotificationConstants.java @@ -43,6 +43,12 @@ public static final class Priority { public static final String NORMAL = "NORMAL"; public static final String MEDIUM = "MEDIUM"; public static final String LOW = "LOW"; + + private static final Set VALUES = Set.of(URGENT, HIGH, NORMAL, LOW); + + public static boolean isValid(String priority) { + return VALUES.contains(priority); + } } public static final class Kind { @@ -82,6 +88,19 @@ public static final class Scope { public static final String APP = "APP"; } + /** Scope selectors accepted by notification read APIs. ALL is never persisted. */ + public static final class FetchScope { + public static final String ALL = "ALL"; + public static final String SYSTEM = Scope.SYSTEM; + public static final String APP = Scope.APP; + + private static final Set VALUES = Set.of(ALL, SYSTEM, APP); + + public static boolean isValid(String scope) { + return VALUES.contains(scope); + } + } + public static final class Audience { public static final String USER = "USER"; public static final String APP_MEMBERS = "APP_MEMBERS"; From adb1a36eac69f74f5f271aa6e6463fe38631e0c9 Mon Sep 17 00:00:00 2001 From: Kunal Patel Date: Thu, 9 Jul 2026 16:39:40 -0400 Subject: [PATCH 5/5] fix: consolidating changes for now --- .../notifications/NotificationDbUtils.java | 57 +++---------------- .../notifications/NotificationOwlCreator.java | 20 +------ .../notifications/NotificationService.java | 7 +-- src/prerna/util/NotificationConstants.java | 32 +---------- 4 files changed, 13 insertions(+), 103 deletions(-) diff --git a/src/prerna/notifications/NotificationDbUtils.java b/src/prerna/notifications/NotificationDbUtils.java index 89c940e731f..a99d6bf2cb0 100644 --- a/src/prerna/notifications/NotificationDbUtils.java +++ b/src/prerna/notifications/NotificationDbUtils.java @@ -128,16 +128,12 @@ private static void initialize(List>>> db Arrays.asList("AUDIENCE_TYPE", "AUDIENCE_ID")); createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, "NOTIFICATION_EVENT_TARGET_INDEX", "NOTIFICATION_EVENT", Arrays.asList("TARGET_TYPE", "TARGET_ID")); - createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, - "NOTIFICATION_EVENT_GROUP_INDEX", "NOTIFICATION_EVENT", "GROUP_ID"); createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, "NOTIFICATION_USER_STATE_NOTIFICATION_USER_INDEX", "NOTIFICATION_USER_STATE", Arrays.asList("NOTIFICATION_ID", "USER_ID", "USER_TYPE")); createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, "NOTIFICATION_USER_STATE_USER_INDEX", "NOTIFICATION_USER_STATE", Arrays.asList("USER_ID", "USER_TYPE")); - createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, - "NOTIFICATION_DELIVERY_NOTIFICATION_INDEX", "NOTIFICATION_DELIVERY", "NOTIFICATION_ID"); if (!conn.getAutoCommit()) { conn.commit(); @@ -214,7 +210,6 @@ public static void createNotification(User loggedInUser, String affectedUserId, String createdBy = loggedInUser.getAccessToken(loggedInUser.getLogins().get(0)).getId(); Timestamp createdAt = Utility.getCurrentSqlTimestampUTC(); - String kind = deriveKind(notificationType); String type = deriveType(notificationType); String scopeType = deriveScopeType(notificationSource); String scopeId = NotificationConstants.Scope.APP.equals(scopeType) ? catalogId : null; @@ -224,7 +219,7 @@ public static void createNotification(User loggedInUser, String affectedUserId, String metadataJson = buildLegacyMetadata(affectedUserId, affectedUserType, affectedUserPreviousRole, affectedUserNewRole, notificationType, notificationSource); - String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,KIND,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,DISPLAY_SURFACE,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,TARGET_URL,ACTION_LABEL,STATUS,GROUP_ID,METADATA_JSON,CREATED_BY,CREATED_AT,RESOLVED_AT,EXPIRES_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,DISPLAY_SURFACE,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,METADATA_JSON,CREATED_BY,CREATED_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; for (Map recipient : recipients) { if (recipient == null || recipient.get("userId") == null) { continue; @@ -234,7 +229,6 @@ public static void createNotification(User loggedInUser, String affectedUserId, ps = notificationDb.getPreparedStatement(query); int parameterIndex = 1; ps.setString(parameterIndex++, GUID.v7().toUUID().toString()); - ps.setString(parameterIndex++, kind); ps.setString(parameterIndex++, type); ps.setString(parameterIndex++, scopeType); ps.setString(parameterIndex++, scopeId); @@ -250,15 +244,9 @@ public static void createNotification(User loggedInUser, String affectedUserId, ps.setString(parameterIndex++, catalogId); ps.setString(parameterIndex++, targetType); ps.setString(parameterIndex++, catalogId); - ps.setString(parameterIndex++, null); - ps.setString(parameterIndex++, NotificationConstants.Kind.ACTION.equals(kind) ? "Review" : null); - ps.setString(parameterIndex++, NotificationConstants.Status.ACTIVE); - ps.setString(parameterIndex++, null); ps.setString(parameterIndex++, metadataJson); ps.setString(parameterIndex++, createdBy); ps.setTimestamp(parameterIndex++, createdAt); - ps.setTimestamp(parameterIndex++, null); - ps.setTimestamp(parameterIndex++, null); ps.execute(); if (!ps.getConnection().getAutoCommit()) { @@ -273,20 +261,18 @@ public static void createNotification(User loggedInUser, String affectedUserId, } } - static String insertNotificationEvent(String kind, String type, String scopeType, String scopeId, + static String insertNotificationEvent(String type, String scopeType, String scopeId, String audienceType, String audienceId, String audienceUserType, String title, String message, String priority, String displaySurface, String sourceType, String sourceId, String targetType, - String targetId, String targetUrl, String actionLabel, String status, String groupId, String metadataJson, - String createdBy) { + String targetId, String metadataJson, String createdBy) { IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); String notificationId = GUID.v7().toUUID().toString(); - String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,KIND,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,DISPLAY_SURFACE,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,TARGET_URL,ACTION_LABEL,STATUS,GROUP_ID,METADATA_JSON,CREATED_BY,CREATED_AT,RESOLVED_AT,EXPIRES_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,DISPLAY_SURFACE,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,METADATA_JSON,CREATED_BY,CREATED_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; PreparedStatement ps = null; try { ps = notificationDb.getPreparedStatement(query); int parameterIndex = 1; ps.setString(parameterIndex++, notificationId); - ps.setString(parameterIndex++, kind); ps.setString(parameterIndex++, type); ps.setString(parameterIndex++, scopeType); ps.setString(parameterIndex++, scopeId); @@ -301,15 +287,9 @@ static String insertNotificationEvent(String kind, String type, String scopeType ps.setString(parameterIndex++, sourceId); ps.setString(parameterIndex++, targetType); ps.setString(parameterIndex++, targetId); - ps.setString(parameterIndex++, targetUrl); - ps.setString(parameterIndex++, actionLabel); - ps.setString(parameterIndex++, status); - ps.setString(parameterIndex++, groupId); ps.setString(parameterIndex++, metadataJson); ps.setString(parameterIndex++, createdBy); ps.setTimestamp(parameterIndex++, Utility.getCurrentSqlTimestampUTC()); - ps.setTimestamp(parameterIndex++, null); - ps.setTimestamp(parameterIndex++, null); ps.execute(); if (!ps.getConnection().getAutoCommit()) { ps.getConnection().commit(); @@ -348,22 +328,19 @@ public static List> fetchNotifications(User user, String sco "urs.IS_READ = TRUE"); StringBuilder query = new StringBuilder(); - query.append("SELECT n.NOTIFICATION_ID, n.KIND, n.TYPE, n.SCOPE_TYPE, n.SCOPE_ID, n.AUDIENCE_TYPE, ") + query.append("SELECT n.NOTIFICATION_ID, n.TYPE, n.SCOPE_TYPE, n.SCOPE_ID, n.AUDIENCE_TYPE, ") .append("n.AUDIENCE_ID, n.AUDIENCE_USER_TYPE, n.TITLE, n.MESSAGE, n.PRIORITY, n.DISPLAY_SURFACE, n.SOURCE_TYPE, ") - .append("n.SOURCE_ID, n.TARGET_TYPE, n.TARGET_ID, n.TARGET_URL, n.ACTION_LABEL, n.STATUS, ") - .append("n.GROUP_ID, n.METADATA_JSON, n.CREATED_BY, n.CREATED_AT, n.RESOLVED_AT, n.EXPIRES_AT, ") + .append("n.SOURCE_ID, n.TARGET_TYPE, n.TARGET_ID, n.METADATA_JSON, n.CREATED_BY, n.CREATED_AT, ") .append("CASE WHEN EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE urs WHERE urs.NOTIFICATION_ID = n.NOTIFICATION_ID AND ") .append(readCondition).append(") THEN TRUE ELSE FALSE END AS IS_READ ") .append("FROM NOTIFICATION_EVENT n WHERE (").append(audienceCondition).append(") ") .append("AND (").append(scopeCondition).append(") ") - .append("AND n.STATUS <> ? ").append("AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) ") .append("AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND ") .append(dismissedCondition).append(") ").append("ORDER BY n.CREATED_AT DESC"); List parameters = new ArrayList<>(); parameters.addAll(readParameters); parameters.addAll(audienceParameters); parameters.addAll(scopeParameters); - parameters.add(NotificationConstants.Status.EXPIRED); parameters.addAll(dismissedParameters); Long longLimit = parseLong(limit); @@ -506,12 +483,10 @@ private static int fetchNewNotificationCount(List> recipien List parameters = new ArrayList<>(); parameters.addAll(audienceParameters); parameters.addAll(scopeParameters); - parameters.add(NotificationConstants.Status.EXPIRED); parameters.addAll(dismissedParameters); parameters.addAll(readParameters); String query = "SELECT COUNT(n.NOTIFICATION_ID) FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + "AND (" + scopeCondition + ") " - + "AND n.STATUS <> ? " + "AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) " + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + dismissedCondition + ") " + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE urs WHERE urs.NOTIFICATION_ID = n.NOTIFICATION_ID AND " @@ -569,7 +544,7 @@ private static Map mapNotificationRow(ResultSet rs) throws SQLEx row.put("notification_title", getString(rs, "TITLE")); row.put("notification_message", getString(rs, "MESSAGE")); row.put("notification_actiontype", Boolean.TRUE.equals(rs.getObject("IS_READ")) ? "NONE" : "NEW"); - row.put("notification_actiontarget", getString(rs, "TARGET_URL")); + row.put("notification_actiontarget", null); row.put("notification_isread", rs.getBoolean("IS_READ")); row.put("notification_priority", getString(rs, "PRIORITY")); row.put("display_surface", normalizeDisplaySurface(getString(rs, "DISPLAY_SURFACE"))); @@ -583,16 +558,11 @@ private static Map mapNotificationRow(ResultSet rs) throws SQLEx row.put("user_existingrole", getMetadataString(metadata, "affectedUserPreviousRole")); row.put("user_newrole", getMetadataString(metadata, "affectedUserNewRole")); row.put("notification_createdby", getString(rs, "CREATED_BY")); - row.put("kind", getString(rs, "KIND")); row.put("type", getString(rs, "TYPE")); row.put("scope_type", getString(rs, "SCOPE_TYPE")); row.put("scope_id", getString(rs, "SCOPE_ID")); row.put("target_type", getString(rs, "TARGET_TYPE")); row.put("target_id", targetId); - row.put("target_url", getString(rs, "TARGET_URL")); - row.put("action_label", getString(rs, "ACTION_LABEL")); - row.put("status", getString(rs, "STATUS")); - row.put("group_id", getString(rs, "GROUP_ID")); return row; } @@ -648,11 +618,9 @@ private static List fetchVisibleNotificationIds(List parameters = new ArrayList<>(); parameters.addAll(audienceParameters); parameters.addAll(scopeParameters); - parameters.add(NotificationConstants.Status.EXPIRED); parameters.addAll(dismissedParameters); String query = "SELECT n.NOTIFICATION_ID FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + "AND (" + scopeCondition + ") " - + "AND n.STATUS <> ? " + "AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) " + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + dismissedCondition + ")"; List ids = new ArrayList<>(); @@ -689,11 +657,9 @@ private static boolean isNotificationVisibleToUser(String notificationId, parameters.addAll(audienceParameters); parameters.addAll(scopeParameters); parameters.add(notificationId); - parameters.add(NotificationConstants.Status.EXPIRED); parameters.addAll(dismissedParameters); String query = "SELECT 1 FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " - + "AND (" + scopeCondition + ") " + "AND n.NOTIFICATION_ID = ? AND n.STATUS <> ? " - + "AND (n.EXPIRES_AT IS NULL OR n.EXPIRES_AT > CURRENT_TIMESTAMP) " + + "AND (" + scopeCondition + ") " + "AND n.NOTIFICATION_ID = ? " + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + dismissedCondition + ")"; IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); @@ -872,13 +838,6 @@ private static Long parseLong(String value) { return ((Number) Double.parseDouble(value)).longValue(); } - private static String deriveKind(String notificationType) { - if (NotificationConstants.Type.USER_REQUEST.equalsIgnoreCase(notificationType)) { - return NotificationConstants.Kind.ACTION; - } - return NotificationConstants.Kind.INFO; - } - private static String deriveType(String notificationType) { if (NotificationConstants.Type.USER_REQUEST.equalsIgnoreCase(notificationType)) { return NotificationConstants.Type.ACCESS_REQUEST; diff --git a/src/prerna/notifications/NotificationOwlCreator.java b/src/prerna/notifications/NotificationOwlCreator.java index ea2f8dea12e..be9b7091c03 100644 --- a/src/prerna/notifications/NotificationOwlCreator.java +++ b/src/prerna/notifications/NotificationOwlCreator.java @@ -52,7 +52,6 @@ public void createColumnsAndTypes(AbstractSqlQueryUtil queryUtil) { // @formatter:off addTable("NOTIFICATION_EVENT", Arrays.asList( Pair.with("NOTIFICATION_ID", "VARCHAR(50)"), - Pair.with("KIND", "VARCHAR(20)"), Pair.with("TYPE", "VARCHAR(50)"), Pair.with("SCOPE_TYPE", "VARCHAR(20)"), Pair.with("SCOPE_ID", "VARCHAR(50)"), @@ -67,15 +66,9 @@ public void createColumnsAndTypes(AbstractSqlQueryUtil queryUtil) { Pair.with("SOURCE_ID", "VARCHAR(50)"), Pair.with("TARGET_TYPE", "VARCHAR(30)"), Pair.with("TARGET_ID", "VARCHAR(50)"), - Pair.with("TARGET_URL", CLOB_DATATYPE_NAME), - Pair.with("ACTION_LABEL", "VARCHAR(50)"), - Pair.with("STATUS", "VARCHAR(20)"), - Pair.with("GROUP_ID", "VARCHAR(50)"), Pair.with("METADATA_JSON", CLOB_DATATYPE_NAME), Pair.with("CREATED_BY", VARCHAR_255), - Pair.with("CREATED_AT", TIMESTAMP_DATATYPE_NAME), - Pair.with("RESOLVED_AT", TIMESTAMP_DATATYPE_NAME), - Pair.with("EXPIRES_AT", TIMESTAMP_DATATYPE_NAME))); + Pair.with("CREATED_AT", TIMESTAMP_DATATYPE_NAME))); addTable("NOTIFICATION_USER_STATE", Arrays.asList( Pair.with("NOTIFICATION_ID", "VARCHAR(50)"), Pair.with("USER_ID", VARCHAR_255), @@ -84,17 +77,6 @@ public void createColumnsAndTypes(AbstractSqlQueryUtil queryUtil) { Pair.with("READ_AT", TIMESTAMP_DATATYPE_NAME), Pair.with("IS_DISMISSED", BOOLEAN_DATATYPE_NAME), Pair.with("DISMISSED_AT", TIMESTAMP_DATATYPE_NAME))); - addTable("NOTIFICATION_DELIVERY", Arrays.asList( - Pair.with("DELIVERY_ID", "VARCHAR(50)"), - Pair.with("NOTIFICATION_ID", "VARCHAR(50)"), - Pair.with("USER_ID", VARCHAR_255), - Pair.with("USER_TYPE", "VARCHAR(50)"), - Pair.with("CHANNEL", "VARCHAR(20)"), - Pair.with("STATUS", "VARCHAR(20)"), - Pair.with("ATTEMPTS", "INT"), - Pair.with("LAST_ERROR", CLOB_DATATYPE_NAME), - Pair.with("CREATED_AT", TIMESTAMP_DATATYPE_NAME), - Pair.with("SENT_AT", TIMESTAMP_DATATYPE_NAME))); // @formatter:on } } diff --git a/src/prerna/notifications/NotificationService.java b/src/prerna/notifications/NotificationService.java index cc6e45ffe30..59a1df2bd93 100644 --- a/src/prerna/notifications/NotificationService.java +++ b/src/prerna/notifications/NotificationService.java @@ -93,12 +93,11 @@ private static String createAppNotification(String projectId, String audienceTyp } String normalizedMessage = requireValue(message, "message"); String normalizedPriority = normalizePriority(priority); - return NotificationDbUtils.insertNotificationEvent(NotificationConstants.Kind.INFO, - NotificationConstants.Type.ANNOUNCEMENT, NotificationConstants.Scope.APP, normalizedProjectId, + return NotificationDbUtils.insertNotificationEvent(NotificationConstants.Type.ANNOUNCEMENT, + NotificationConstants.Scope.APP, normalizedProjectId, audienceType, audienceId, audienceUserType, normalizedTitle, normalizedMessage, normalizedPriority, NotificationConstants.DisplaySurface.BELL, NotificationConstants.Source.PROJECT, normalizedProjectId, - NotificationConstants.Target.APP, normalizedProjectId, null, null, NotificationConstants.Status.ACTIVE, - null, null, createdBy); + NotificationConstants.Target.APP, normalizedProjectId, null, createdBy); } private static String normalizePriority(String priority) { diff --git a/src/prerna/util/NotificationConstants.java b/src/prerna/util/NotificationConstants.java index 59ab7d21660..232f43d8db2 100644 --- a/src/prerna/util/NotificationConstants.java +++ b/src/prerna/util/NotificationConstants.java @@ -51,12 +51,7 @@ public static boolean isValid(String priority) { } } - public static final class Kind { - public static final String INFO = "INFO"; - public static final String ACTION = "ACTION"; - } - - // in-app render surface (NOT external delivery - that is DeliveryChannel) + // In-app render surface; external channels are modeled separately in Phase 5. public static final class DisplaySurface { public static final String BELL = "BELL"; public static final String MODAL = "MODAL"; @@ -78,7 +73,6 @@ public static final class Type { public static final String REQUEST_DENIAL = "REQUEST_DENIAL"; public static final String SMSS_UPDATE = "SMSS_UPDATE"; public static final String ACCESS_REQUEST = "ACCESS_REQUEST"; - public static final String AGENT_ACTION_REQUIRED = "AGENT_ACTION_REQUIRED"; public static final String ANNOUNCEMENT = "ANNOUNCEMENT"; public static final String APP_TASK_COMPLETE = "APP_TASK_COMPLETE"; } @@ -112,7 +106,6 @@ public static final class Audience { public static final class Source { public static final String SYSTEM = "SYSTEM"; public static final String USER = "USER"; - public static final String AGENT = "AGENT"; public static final String ENGINE = "ENGINE"; public static final String PROJECT = "PROJECT"; } @@ -121,29 +114,6 @@ public static final class Target { public static final String NONE = "NONE"; public static final String ROUTE = "ROUTE"; public static final String APP = "APP"; - public static final String ROOM = "ROOM"; - public static final String AGENT_RUN = "AGENT_RUN"; - public static final String AGENT_ACTION = "AGENT_ACTION"; - } - - public static final class Status { - public static final String ACTIVE = "ACTIVE"; - public static final String RESOLVED = "RESOLVED"; - public static final String EXPIRED = "EXPIRED"; - } - - public static final class DeliveryChannel { - public static final String EMAIL = "EMAIL"; - public static final String SLACK = "SLACK"; - public static final String TEAMS = "TEAMS"; - public static final String WEBHOOK = "WEBHOOK"; - } - - public static final class DeliveryStatus { - public static final String PENDING = "PENDING"; - public static final String SENT = "SENT"; - public static final String FAILED = "FAILED"; - public static final String SKIPPED = "SKIPPED"; } }