Browse Source

Merge remote-tracking branch 'klimov/feature/notification-system' into feature/notification-system

pull/7980/head
Vladyslav_Prykhodko 3 years ago
parent
commit
052da292bb
  1. 10
      application/src/main/data/upgrade/3.4.3/schema_update.sql
  2. 10
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  3. 66
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  4. 21
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  5. 1
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  6. 16
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java
  8. 27
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java
  10. 12
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java
  11. 2
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  12. 36
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  13. 7
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiWsClient.java
  14. 12
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java
  15. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java
  16. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java
  17. 7
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/PushDeliveryMethodNotificationTemplate.java
  18. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  19. 6
      dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java
  20. 4
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java
  21. 27
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java
  22. 13
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  23. 9
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java
  24. 12
      dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java
  25. 25
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java
  26. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java
  27. 10
      dao/src/main/resources/sql/schema-entities.sql
  28. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java

10
application/src/main/data/upgrade/3.4.3/schema_update.sql

@ -19,7 +19,8 @@ CREATE TABLE IF NOT EXISTS notification_target (
created_time BIGINT NOT NULL,
tenant_id UUID NULL CONSTRAINT fk_notification_target_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
configuration VARCHAR(10000) NOT NULL
configuration VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_target_name UNIQUE (tenant_id, name)
);
CREATE INDEX IF NOT EXISTS idx_notification_target_tenant_id_created_time ON notification_target(tenant_id, created_time DESC);
@ -29,7 +30,8 @@ CREATE TABLE IF NOT EXISTS notification_template (
tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
notification_type VARCHAR(32) NOT NULL,
configuration VARCHAR(10000) NOT NULL
configuration VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_template_name UNIQUE (tenant_id, name)
);
CREATE INDEX IF NOT EXISTS idx_notification_template_tenant_id_created_time ON notification_template(tenant_id, created_time DESC);
@ -41,7 +43,8 @@ CREATE TABLE IF NOT EXISTS notification_rule (
template_id UUID NOT NULL CONSTRAINT fk_notification_rule_template_id REFERENCES notification_template(id),
trigger_type VARCHAR(50) NOT NULL,
trigger_config VARCHAR(1000) NOT NULL,
recipients_config VARCHAR(10000) NOT NULL
recipients_config VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_rule_name UNIQUE (tenant_id, name)
);
CREATE INDEX IF NOT EXISTS idx_notification_rule_tenant_id_created_time ON notification_rule(tenant_id, created_time DESC);
@ -71,6 +74,7 @@ CREATE TABLE IF NOT EXISTS notification (
type VARCHAR(32) NOT NULL,
subject VARCHAR(255),
text VARCHAR(1000) NOT NULL,
additional_config VARCHAR(500),
info VARCHAR(1000),
status VARCHAR(32)
) PARTITION BY RANGE (created_time);

10
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -90,6 +90,8 @@ public class NotificationController extends BaseController {
"```\n{\n \"unsubCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" +
"To mark certain notifications as read, use following command:\n" +
"```\n{\n \"markAsReadCmd\": {\n \"cmdId\": 1234,\n \"notifications\": [\n \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\",\n \"5b6dfee0-8d0d-11ed-b61f-35a57b03dade\"\n ]\n }\n}\n\n```\n" +
"To mark all notifications as read:\n" +
"```\n{\n \"markAllAsReadCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" +
"\n\n" +
"Update structure for unread **notifications count subscription**:\n" +
"```\n{\n \"cmdId\": 1234,\n \"totalUnreadCount\": 55\n}\n```\n" +
@ -136,7 +138,7 @@ public class NotificationController extends BaseController {
@RequestParam(defaultValue = "false") boolean unreadOnly,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return notificationService.findNotificationsByUserIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink);
return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink);
}
@PutMapping("/notification/{id}/read")
@ -147,6 +149,12 @@ public class NotificationController extends BaseController {
notificationCenter.markNotificationAsRead(user.getTenantId(), user.getId(), notificationId);
}
@PutMapping("/notifications/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) {
notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId());
}
@DeleteMapping("/notification/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void deleteNotification(@PathVariable UUID id,

66
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -55,11 +55,13 @@ import java.io.IOException;
import java.net.URI;
import java.security.InvalidParameterException;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.thingsboard.server.service.ws.DefaultWebSocketService.NUMBER_OF_PING_ATTEMPTS;
@ -140,7 +142,10 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
if (!checkLimits(session, sessionRef)) {
return;
}
internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef));
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef,
tenantProfileConfiguration != null && tenantProfileConfiguration.getWsMsgQueueLimitPerSession() > 0 ?
tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : 500));
externalSessionMap.put(externalSessionId, internalSessionId);
processInWebSocketService(sessionRef, SessionEvent.onEstablished());
@ -215,21 +220,22 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private final RemoteEndpoint.Async asyncRemote;
private final WebSocketSessionRef sessionRef;
// TODO: carefully review ( + discuss removal of the msgQueue)
private final Semaphore sendingSemaphore = new Semaphore(1);
private final AtomicBoolean isSending = new AtomicBoolean(false);
private final Queue<TbWebSocketMsg<?>> msgQueue;
private volatile long lastActivityTime;
SessionMetaData(WebSocketSession session, WebSocketSessionRef sessionRef) {
SessionMetaData(WebSocketSession session, WebSocketSessionRef sessionRef, int maxMsgQueuePerSession) {
super();
this.session = session;
Session nativeSession = ((NativeWebSocketSession) session).getNativeSession(Session.class);
this.asyncRemote = nativeSession.getAsyncRemote();
this.sessionRef = sessionRef;
this.msgQueue = new LinkedBlockingQueue<>(maxMsgQueuePerSession);
this.lastActivityTime = System.currentTimeMillis();
}
void sendPing(long currentTime) {
synchronized void sendPing(long currentTime) {
try {
long timeSinceLastActivity = currentTime - lastActivityTime;
if (timeSinceLastActivity >= pingTimeout) {
@ -244,37 +250,40 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
void closeSession(CloseStatus reason) {
private void closeSession(CloseStatus reason) {
try {
close(this.sessionRef, reason);
} catch (IOException ioe) {
log.trace("[{}] Session transport error", session.getId(), ioe);
} finally {
sendingSemaphore.release();
}
}
void processPongMessage(long currentTime) {
synchronized void processPongMessage(long currentTime) {
lastActivityTime = currentTime;
}
void sendMsg(String msg) {
synchronized void sendMsg(String msg) {
sendMsg(new TbWebSocketTextMsg(msg));
}
void sendMsg(TbWebSocketMsg<?> msg) {
try {
sendingSemaphore.acquire();
synchronized void sendMsg(TbWebSocketMsg<?> msg) {
if (isSending.compareAndSet(false, true)) {
sendMsgInternal(msg);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (Exception e) {
sendingSemaphore.release();
throw e;
} else {
try {
msgQueue.add(msg);
} catch (RuntimeException e) {
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId(), e);
} else {
log.info("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId());
}
closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!"));
}
}
}
void sendMsgInternal(TbWebSocketMsg<?> msg) {
private void sendMsgInternal(TbWebSocketMsg<?> msg) {
try {
if (TbWebSocketMsgType.TEXT.equals(msg.getType())) {
TbWebSocketTextMsg textMsg = (TbWebSocketTextMsg) msg;
@ -282,6 +291,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} else {
TbWebSocketPingMsg pingMsg = (TbWebSocketPingMsg) msg;
this.asyncRemote.sendPing(pingMsg.getMsg());
processNextMsg();
}
} catch (Exception e) {
log.trace("[{}] Failed to send msg", session.getId(), e);
@ -295,7 +305,16 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
log.trace("[{}] Failed to send msg", session.getId(), result.getException());
closeSession(CloseStatus.SESSION_NOT_RELIABLE);
} else {
sendingSemaphore.release();
processNextMsg();
}
}
private void processNextMsg() {
TbWebSocketMsg<?> msg = msgQueue.poll();
if (msg != null) {
sendMsgInternal(msg);
} else {
isSending.set(false);
}
}
}
@ -476,4 +495,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
.map(TenantProfile::getDefaultProfileConfiguration).orElse(null);
}
private DefaultTenantProfileConfiguration getTenantProfileConfiguration(WebSocketSessionRef sessionRef) {
return Optional.ofNullable(tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()))
.map(TenantProfile::getDefaultProfileConfiguration).orElse(null);
}
}

21
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -140,7 +140,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
return notificationTargetService.findRecipientsForNotificationTarget(tenantId, ctx.getCustomerId(), new NotificationTargetId(targetId), pageLink);
}, 200, recipientsBatch -> {
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
if (deliveryMethod.isIndependent()) continue;
if (deliveryMethod.isStandalone()) continue;
List<User> recipients = recipientsBatch.getData();
log.debug("Sending {} notifications for request {} to recipients batch ({})", deliveryMethod, savedNotificationRequest.getId(), recipients.size());
@ -158,7 +158,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
});
}
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
if (deliveryMethod.isIndependent()) {
if (deliveryMethod.isStandalone()) {
NotificationChannel notificationChannel = channels.get(deliveryMethod);
ListenableFuture<Void> resultFuture = process(notificationChannel, null, ctx);
DonAsynchron.withCallback(resultFuture, result -> {
@ -221,6 +221,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.type(ctx.getNotificationTemplate().getNotificationType())
.subject(processedTemplate.getSubject())
.text(processedTemplate.getBody())
.additionalConfig(processedTemplate.getAdditionalConfig())
.info(request.getInfo())
.status(NotificationStatus.SENT)
.build();
@ -260,7 +261,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
public void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId);
if (updated) {
log.debug("Marking notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId);
log.trace("Marked notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId);
NotificationUpdate update = NotificationUpdate.builder()
.notificationId(notificationId)
.updatedStatus(NotificationStatus.READ)
@ -270,6 +271,20 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
}
}
@Override
public void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) {
int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, recipientId);
if (updatedCount > 0) {
log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId);
NotificationUpdate update = NotificationUpdate.builder()
.allNotifications(true)
.updatedStatus(NotificationStatus.READ)
.updateType(ComponentLifecycleEvent.UPDATED)
.build();
onNotificationUpdate(tenantId, recipientId, update);
}
}
@Override
public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
Notification notification = notificationService.findNotificationById(tenantId, notificationId);

1
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -174,6 +174,7 @@ public class DefaultWebSocketService implements WebSocketService {
newCmdHandler(NotificationCmdsWrapper::getUnreadSubCmd, notificationCmdsHandler::handleUnreadNotificationsSubCmd),
newCmdHandler(NotificationCmdsWrapper::getUnreadCountSubCmd, notificationCmdsHandler::handleUnreadNotificationsCountSubCmd),
newCmdHandler(NotificationCmdsWrapper::getMarkAsReadCmd, notificationCmdsHandler::handleMarkAsReadCmd),
newCmdHandler(NotificationCmdsWrapper::getMarkAllAsReadCmd, notificationCmdsHandler::handleMarkAllAsReadCmd),
newCmdHandler(NotificationCmdsWrapper::getUnsubCmd, notificationCmdsHandler::handleUnsubCmd)
);
}

16
application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java

@ -34,6 +34,7 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
@ -101,7 +102,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotifications(NotificationsSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId());
PageData<Notification> notifications = notificationService.findLatestUnreadNotificationsByUserId(subscription.getTenantId(),
PageData<Notification> notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(),
(UserId) subscription.getEntityId(), subscription.getLimit());
subscription.getLatestUnreadNotifications().clear();
notifications.getData().forEach(notification -> {
@ -112,7 +113,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId());
int unreadCount = notificationService.countUnreadNotificationsByUserId(subscription.getTenantId(), (UserId) subscription.getEntityId());
int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId());
subscription.getUnreadCounter().set(unreadCount);
}
@ -129,7 +130,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void handleNotificationUpdate(NotificationsSubscription subscription, NotificationUpdate update) {
log.trace("[{}, subId: {}] Handling notification update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
Notification notification = update.getNotification();
UUID notificationId = update.getNotificationId().getId();
UUID notificationId = update.getNotificationId();
switch (update.getUpdateType()) {
case CREATED: {
subscription.getLatestUnreadNotifications().put(notificationId, notification);
@ -144,7 +145,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
}
case UPDATED: {
if (update.getUpdatedStatus() == NotificationStatus.READ) {
if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
if (update.isAllNotifications() || subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
fetchUnreadNotifications(subscription);
sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
} else {
@ -234,10 +235,15 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
.map(NotificationId::new)
.forEach(notificationId -> {
notificationCenter.markNotificationAsRead(securityCtx.getTenantId(), securityCtx.getId(), notificationId);
// fixme: should send bulk update event, not a separate event for each notification
});
}
@Override
public void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd) {
SecurityUser securityCtx = sessionRef.getSecurityCtx();
notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), securityCtx.getId());
}
@Override
public void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) {
localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), cmd.getCmdId());

3
application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.ws.notification;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
@ -29,6 +30,8 @@ public interface NotificationCommandsHandler {
void handleMarkAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationsAsReadCmd cmd);
void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd);
void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd);
}

27
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MarkAllNotificationsAsReadCmd implements WsCmd {
private int cmdId;
}

2
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java

@ -26,6 +26,8 @@ public class NotificationCmdsWrapper {
private MarkNotificationsAsReadCmd markAsReadCmd;
private MarkAllNotificationsAsReadCmd markAllAsReadCmd;
private NotificationsUnsubCmd unsubCmd;
}

12
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java

@ -24,6 +24,8 @@ import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ -31,12 +33,16 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
public class NotificationUpdate {
private NotificationId notificationId;
private NotificationStatus updatedStatus;
private Notification notification;
boolean allNotifications;
private NotificationStatus updatedStatus;
private ComponentLifecycleEvent updateType;
public NotificationId getNotificationId() {
return notificationId != null ? notificationId : notification.getId();
public UUID getNotificationId() {
return notificationId != null ? notificationId.getId() :
notification != null ? notification.getUuidId() : null;
}
}

2
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -126,7 +126,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
String text, NotificationDeliveryMethod... deliveryMethods) {
NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setTenantId(tenantId);
notificationTemplate.setName("Notification template for testing");
notificationTemplate.setName("Notification template: " + text);
notificationTemplate.setNotificationType(notificationType);
NotificationTemplateConfig config = new NotificationTemplateConfig();
config.setDefaultTextTemplate(text);

36
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -22,6 +22,7 @@ import org.java_websocket.client.WebSocketClient;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.NotificationTargetId;
@ -64,7 +65,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@ -74,9 +74,7 @@ import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@DaoSqlTest
@ -154,10 +152,8 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test
public void testReceivingNotificationUpdates_multipleSessions() throws Exception {
connectOtherWsClient();
wsClient.subscribeForUnreadNotifications(10);
otherWsClient.subscribeForUnreadNotifications(10);
wsClient.waitForReply(true);
otherWsClient.waitForReply(true);
wsClient.subscribeForUnreadNotifications(10).waitForReply(true);
otherWsClient.subscribeForUnreadNotifications(10).waitForReply(true);
UnreadNotificationsUpdate notificationsUpdate = wsClient.getLastDataUpdate();
assertThat(notificationsUpdate.getTotalUnreadCount()).isZero();
@ -213,6 +209,26 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(otherWsClient.getLastCountUpdate().getTotalUnreadCount()).isOne();
}
@Test
public void testMarkingAllAsRead() {
wsClient.subscribeForUnreadNotifications(10).waitForReply(true);
NotificationTarget target = createNotificationTarget(customerUserId);
int notificationsCount = 20;
wsClient.registerWaitForUpdate(notificationsCount);
for (int i = 1; i <= notificationsCount; i++) {
submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.PUSH);
}
wsClient.waitForUpdate(true);
assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(notificationsCount);
wsClient.registerWaitForUpdate(1);
wsClient.markAllNotificationsAsRead();
wsClient.waitForUpdate(true);
assertThat(wsClient.getLastDataUpdate().getNotifications()).isEmpty();
assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isZero();
}
@Test
public void testDelayedNotificationRequest() throws Exception {
wsClient.subscribeForUnreadNotifications(5);
@ -285,7 +301,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test
public void testNotificationUpdatesForALotOfUsers() throws Exception {
int usersCount = 100;
int usersCount = 80;
Map<User, NotificationApiWsClient> sessions = new HashMap<>();
List<NotificationTargetId> targets = new ArrayList<>();
@ -311,7 +327,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
sessions.forEach((user, wsClient) -> wsClient.registerWaitForUpdate(2));
NotificationRequest notificationRequest = submitNotificationRequest(targets, "Hello, ${email}", 0,
NotificationDeliveryMethod.PUSH, NotificationDeliveryMethod.EMAIL);
NotificationDeliveryMethod.PUSH);
await().atMost(10, TimeUnit.SECONDS)
.pollDelay(1, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS)
.until(() -> {
@ -322,7 +338,6 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
System.err.println("WS sessions received update: " + receivedUpdate);
return receivedUpdate == sessions.size();
});
verify(mailService, timeout(1500).times(usersCount)).sendEmail(eq(tenantId), startsWith("test-user-"), any(), any());
sessions.forEach((user, wsClient) -> {
assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isOne();
@ -338,7 +353,6 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
.until(() -> findNotificationRequest(notificationRequest.getId()).isSent());
NotificationRequestStats stats = getStats(notificationRequest.getId());
assertThat(stats.getSent().get(NotificationDeliveryMethod.PUSH)).hasValue(usersCount);
assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).hasValue(usersCount);
sessions.values().forEach(wsClient -> wsClient.registerWaitForUpdate(2));
deleteNotificationRequest(notificationRequest.getId());

7
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiWsClient.java

@ -22,6 +22,7 @@ import org.apache.commons.lang3.RandomUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
@ -73,6 +74,12 @@ public class NotificationApiWsClient extends TbTestWebSocketClient {
sendCmd(cmdsWrapper);
}
public void markAllNotificationsAsRead() {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper();
cmdsWrapper.setMarkAllAsReadCmd(new MarkAllNotificationsAsReadCmd(newCmdId()));
sendCmd(cmdsWrapper);
}
public void sendCmd(NotificationCmdsWrapper cmdsWrapper) {
String cmd = JacksonUtil.toString(cmdsWrapper);
send(cmd);

12
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java

@ -30,16 +30,18 @@ public interface NotificationService {
Notification findNotificationById(TenantId tenantId, NotificationId notificationId);
boolean markNotificationAsRead(TenantId tenantId, UserId userId, NotificationId notificationId);
boolean markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId);
PageData<Notification> findNotificationsByUserIdAndReadStatus(TenantId tenantId, UserId userId, boolean unreadOnly, PageLink pageLink);
int markAllNotificationsAsRead(TenantId tenantId, UserId recipientId);
PageData<Notification> findLatestUnreadNotificationsByUserId(TenantId tenantId, UserId userId, int limit);
PageData<Notification> findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, UserId recipientId, boolean unreadOnly, PageLink pageLink);
int countUnreadNotificationsByUserId(TenantId tenantId, UserId userId);
PageData<Notification> findLatestUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId, int limit);
int countUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId);
void updateNotificationsStatusByRequestId(TenantId tenantId, NotificationRequestId requestId, NotificationStatus status);
boolean deleteNotification(TenantId tenantId, UserId userId, NotificationId notificationId);
boolean deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId);
}

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.notification;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -40,6 +41,7 @@ public class Notification extends BaseData<NotificationId> {
private NotificationType type;
private String subject;
private String text;
private JsonNode additionalConfig;
private NotificationInfo info;
private NotificationStatus status;

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java

@ -25,14 +25,14 @@ public enum NotificationDeliveryMethod {
SLACK(true);
@Getter
private final boolean independent; // TODO: think of a better name
private final boolean standalone; // means that notifications for the delivery method are sent independently of specified recipients
NotificationDeliveryMethod() {
this(false);
}
NotificationDeliveryMethod(boolean independent) {
this.independent = independent;
NotificationDeliveryMethod(boolean standalone) {
this.standalone = standalone;
}
}

7
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/PushDeliveryMethodNotificationTemplate.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.notification.template;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@ -28,14 +29,12 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho
public class PushDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject {
private String subject;
private String icon;
private String actionButtonConfig;
private JsonNode additionalConfig;
public PushDeliveryMethodNotificationTemplate(PushDeliveryMethodNotificationTemplate other) {
super(other);
this.subject = other.subject;
this.icon = other.icon;
this.actionButtonConfig = other.actionButtonConfig;
this.additionalConfig = other.additionalConfig;
}
@Override

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -659,6 +659,7 @@ public class ModelConstants {
public static final String NOTIFICATION_TYPE_PROPERTY = "type";
public static final String NOTIFICATION_SUBJECT_PROPERTY = "subject";
public static final String NOTIFICATION_TEXT_PROPERTY = "text";
public static final String NOTIFICATION_ADDITIONAL_CONFIG_PROPERTY = "additional_config";
public static final String NOTIFICATION_STATUS_PROPERTY = "status";
public static final String NOTIFICATION_REQUEST_TABLE_NAME = "notification_request";

6
dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java

@ -62,6 +62,10 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
@Column(name = ModelConstants.NOTIFICATION_TEXT_PROPERTY, nullable = false)
private String text;
@Type(type = "json")
@Column(name = ModelConstants.NOTIFICATION_ADDITIONAL_CONFIG_PROPERTY)
private JsonNode additionalConfig;
@Type(type = "json")
@Formula("(SELECT r.info FROM notification_request r WHERE r.id = request_id)")
private JsonNode info;
@ -80,6 +84,7 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
setType(notification.getType());
setSubject(notification.getSubject());
setText(notification.getText());
setAdditionalConfig(notification.getAdditionalConfig());
setInfo(toJson(notification.getInfo()));
setStatus(notification.getStatus());
}
@ -94,6 +99,7 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
notification.setType(type);
notification.setSubject(subject);
notification.setText(text);
notification.setAdditionalConfig(additionalConfig);
notification.setInfo(fromJson(info, NotificationInfo.class));
notification.setStatus(status);
return notification;

4
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.dao.notification.cache.NotificationRuleCacheKey;
import org.thingsboard.server.dao.notification.cache.NotificationRuleCacheValue;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
@ -49,6 +50,9 @@ public class DefaultNotificationRuleService extends AbstractCachedEntityService<
publishEvictEvent(notificationRule);
} catch (Exception e) {
handleEvictEvent(notificationRule);
checkConstraintViolation(e, Map.of(
"uq_notification_rule_name", "Notification rule with such name already exists"
));
throw e;
}
return notificationRule;

27
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java

@ -47,29 +47,34 @@ public class DefaultNotificationService implements NotificationService {
}
@Override
public boolean markNotificationAsRead(TenantId tenantId, UserId userId, NotificationId notificationId) {
return notificationDao.updateStatusByIdAndUserId(tenantId, userId, notificationId, NotificationStatus.READ);
public boolean markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
return notificationDao.updateStatusByIdAndRecipientId(tenantId, recipientId, notificationId, NotificationStatus.READ);
}
@Override
public PageData<Notification> findNotificationsByUserIdAndReadStatus(TenantId tenantId, UserId userId, boolean unreadOnly, PageLink pageLink) {
public int markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) {
return notificationDao.updateStatusByRecipientId(tenantId, recipientId, NotificationStatus.READ);
}
@Override
public PageData<Notification> findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, UserId recipientId, boolean unreadOnly, PageLink pageLink) {
if (unreadOnly) {
return notificationDao.findUnreadByUserIdAndPageLink(tenantId, userId, pageLink);
return notificationDao.findUnreadByRecipientIdAndPageLink(tenantId, recipientId, pageLink);
} else {
return notificationDao.findByUserIdAndPageLink(tenantId, userId, pageLink);
return notificationDao.findByRecipientIdAndPageLink(tenantId, recipientId, pageLink);
}
}
@Override
public PageData<Notification> findLatestUnreadNotificationsByUserId(TenantId tenantId, UserId userId, int limit) {
public PageData<Notification> findLatestUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId, int limit) {
SortOrder sortOrder = new SortOrder(EntityKeyMapping.CREATED_TIME, SortOrder.Direction.DESC);
PageLink pageLink = new PageLink(limit, 0, null, sortOrder);
return findNotificationsByUserIdAndReadStatus(tenantId, userId, true, pageLink);
return findNotificationsByRecipientIdAndReadStatus(tenantId, recipientId, true, pageLink);
}
@Override
public int countUnreadNotificationsByUserId(TenantId tenantId, UserId userId) {
return notificationDao.countUnreadByUserId(tenantId, userId);
public int countUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId) {
return notificationDao.countUnreadByRecipientId(tenantId, recipientId);
}
@Override
@ -78,8 +83,8 @@ public class DefaultNotificationService implements NotificationService {
}
@Override
public boolean deleteNotification(TenantId tenantId, UserId userId, NotificationId notificationId) {
return notificationDao.deleteByIdAndUserId(tenantId, userId, notificationId);
public boolean deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
return notificationDao.deleteByIdAndRecipientId(tenantId, recipientId, notificationId);
}
}

13
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java

@ -30,16 +30,18 @@ import org.thingsboard.server.common.data.notification.targets.NotificationTarge
import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.user.UserService;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class DefaultNotificationTargetService implements NotificationTargetService {
public class DefaultNotificationTargetService extends AbstractEntityService implements NotificationTargetService {
private final NotificationTargetDao notificationTargetDao;
private final NotificationRequestDao notificationRequestDao;
@ -48,7 +50,14 @@ public class DefaultNotificationTargetService implements NotificationTargetServi
@Override
public NotificationTarget saveNotificationTarget(TenantId tenantId, NotificationTarget notificationTarget) {
return notificationTargetDao.save(tenantId, notificationTarget);
try {
return notificationTargetDao.saveAndFlush(tenantId, notificationTarget);
} catch (Exception e) {
checkConstraintViolation(e, Map.of(
"uq_notification_target_name", "Notification target with such name already exists"
));
throw e;
}
}
@Override

9
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java

@ -43,7 +43,14 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im
@Override
public NotificationTemplate saveNotificationTemplate(TenantId tenantId, NotificationTemplate notificationTemplate) {
return notificationTemplateDao.save(tenantId, notificationTemplate);
try {
return notificationTemplateDao.saveAndFlush(tenantId, notificationTemplate);
} catch (Exception e) {
checkConstraintViolation(e, Map.of(
"uq_notification_template_name", "Notification template with such name already exists"
));
throw e;
}
}
@Override

12
dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java

@ -27,18 +27,20 @@ import org.thingsboard.server.dao.Dao;
public interface NotificationDao extends Dao<Notification> {
PageData<Notification> findUnreadByUserIdAndPageLink(TenantId tenantId, UserId userId, PageLink pageLink);
PageData<Notification> findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink);
PageData<Notification> findByUserIdAndPageLink(TenantId tenantId, UserId userId, PageLink pageLink);
PageData<Notification> findByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink);
boolean updateStatusByIdAndUserId(TenantId tenantId, UserId userId, NotificationId notificationId, NotificationStatus status);
boolean updateStatusByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId, NotificationStatus status);
int countUnreadByUserId(TenantId tenantId, UserId userId);
int countUnreadByRecipientId(TenantId tenantId, UserId recipientId);
PageData<Notification> findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink);
void updateStatusesByRequestId(TenantId tenantId, NotificationRequestId requestId, NotificationStatus status);
boolean deleteByIdAndUserId(TenantId tenantId, UserId userId, NotificationId notificationId);
boolean deleteByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId);
int updateStatusByRecipientId(TenantId tenantId, UserId recipientId, NotificationStatus status);
}

25
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java

@ -64,23 +64,23 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
}
@Override
public PageData<Notification> findUnreadByUserIdAndPageLink(TenantId tenantId, UserId userId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(userId.getId(), NotificationStatus.READ, DaoUtil.toPageable(pageLink)));
public PageData<Notification> findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ, DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<Notification> findByUserIdAndPageLink(TenantId tenantId, UserId userId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientId(userId.getId(), DaoUtil.toPageable(pageLink)));
public PageData<Notification> findByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientId(recipientId.getId(), DaoUtil.toPageable(pageLink)));
}
@Override
public boolean updateStatusByIdAndUserId(TenantId tenantId, UserId userId, NotificationId notificationId, NotificationStatus status) {
return notificationRepository.updateStatusByIdAndRecipientId(notificationId.getId(), userId.getId(), status) != 0;
public boolean updateStatusByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId, NotificationStatus status) {
return notificationRepository.updateStatusByIdAndRecipientId(notificationId.getId(), recipientId.getId(), status) != 0;
}
@Override
public int countUnreadByUserId(TenantId tenantId, UserId userId) {
return notificationRepository.countByRecipientIdAndStatusNot(userId.getId(), NotificationStatus.READ);
public int countUnreadByRecipientId(TenantId tenantId, UserId recipientId) {
return notificationRepository.countByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ);
}
@Override
@ -94,8 +94,13 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
}
@Override
public boolean deleteByIdAndUserId(TenantId tenantId, UserId userId, NotificationId notificationId) {
return notificationRepository.deleteByIdAndRecipientId(notificationId.getId(), userId.getId()) != 0;
public boolean deleteByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
return notificationRepository.deleteByIdAndRecipientId(notificationId.getId(), recipientId.getId()) != 0;
}
@Override
public int updateStatusByRecipientId(TenantId tenantId, UserId recipientId, NotificationStatus status) {
return notificationRepository.updateStatusByRecipientId(recipientId.getId(), status);
}
@Override

6
dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java

@ -57,4 +57,10 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
int deleteByIdAndRecipientId(UUID id, UUID recipientId);
@Modifying
@Transactional
@Query("UPDATE NotificationEntity n SET n.status = :status WHERE n.recipientId = :recipientId")
int updateStatusByRecipientId(@Param("recipientId") UUID recipientId,
@Param("status") NotificationStatus status);
}

10
dao/src/main/resources/sql/schema-entities.sql

@ -785,7 +785,8 @@ CREATE TABLE IF NOT EXISTS notification_target (
created_time BIGINT NOT NULL,
tenant_id UUID NULL CONSTRAINT fk_notification_target_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
configuration VARCHAR(10000) NOT NULL
configuration VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_target_name UNIQUE (tenant_id, name)
);
CREATE TABLE IF NOT EXISTS notification_template (
@ -794,7 +795,8 @@ CREATE TABLE IF NOT EXISTS notification_template (
tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
notification_type VARCHAR(32) NOT NULL,
configuration VARCHAR(10000) NOT NULL
configuration VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_template_name UNIQUE (tenant_id, name)
);
CREATE TABLE IF NOT EXISTS notification_rule (
@ -805,7 +807,8 @@ CREATE TABLE IF NOT EXISTS notification_rule (
template_id UUID NOT NULL CONSTRAINT fk_notification_rule_template_id REFERENCES notification_template(id),
trigger_type VARCHAR(50) NOT NULL,
trigger_config VARCHAR(1000) NOT NULL,
recipients_config VARCHAR(10000) NOT NULL
recipients_config VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_rule_name UNIQUE (tenant_id, name)
);
CREATE TABLE IF NOT EXISTS notification_request (
@ -831,6 +834,7 @@ CREATE TABLE IF NOT EXISTS notification (
type VARCHAR(32) NOT NULL,
subject VARCHAR(255),
text VARCHAR(1000) NOT NULL,
additional_config VARCHAR(500),
info VARCHAR(1000),
status VARCHAR(32)
) PARTITION BY RANGE (created_time);

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java

@ -34,6 +34,8 @@ public interface NotificationCenter {
void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId);
void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId);
void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId);
}

Loading…
Cancel
Save