Merge pull request #4571 from appsmithorg/feature/notifications-api
Basics and infrastructure for a Notifications system
This commit is contained in:
commit
94d4302bcb
|
|
@ -233,6 +233,34 @@ async function watchMongoDB(io) {
|
|||
}
|
||||
})
|
||||
|
||||
const notificationsStream = db.collection("notification").watch(
|
||||
[
|
||||
// Prevent server-internal fields from being sent to the client.
|
||||
{
|
||||
$unset: [
|
||||
"deletedAt",
|
||||
"deleted",
|
||||
"_class",
|
||||
].map(f => "fullDocument." + f)
|
||||
},
|
||||
],
|
||||
{ fullDocument: "updateLookup" }
|
||||
);
|
||||
|
||||
notificationsStream.on("change", async (event: mongodb.ChangeEventCR) => {
|
||||
console.log("notification event", event)
|
||||
const notification = event.fullDocument
|
||||
|
||||
if (notification == null) {
|
||||
// This happens when `event.operationType === "drop"`, when a notification is deleted.
|
||||
console.error("Null document recieved for notification change event", event)
|
||||
return
|
||||
}
|
||||
|
||||
const eventName = event.operationType + ":" + event.ns.coll
|
||||
io.to("email:" + notification.forUsername).emit(eventName, { notification })
|
||||
})
|
||||
|
||||
process.on("exit", () => {
|
||||
(commentChangeStream != null ? commentChangeStream.close() : Promise.bind(client).resolve())
|
||||
.then(client.close.bind(client))
|
||||
|
|
|
|||
|
|
@ -29,4 +29,5 @@ public interface Url {
|
|||
String MARKETPLACE_ITEM_URL = BASE_URL + VERSION + "/items";
|
||||
String ASSET_URL = BASE_URL + VERSION + "/assets";
|
||||
String COMMENT_URL = BASE_URL + VERSION + "/comments";
|
||||
String NOTIFICATION_URL = BASE_URL + VERSION + "/notifications";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.appsmith.server.controllers;
|
||||
|
||||
import com.appsmith.server.constants.Url;
|
||||
import com.appsmith.server.domains.Notification;
|
||||
import com.appsmith.server.services.NotificationService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping(Url.NOTIFICATION_URL)
|
||||
public class NotificationController extends BaseController<NotificationService, Notification, String> {
|
||||
|
||||
@Autowired
|
||||
public NotificationController(NotificationService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -36,6 +36,12 @@ public class Comment extends BaseDomain {
|
|||
|
||||
List<Reaction> reactions;
|
||||
|
||||
/**
|
||||
* Indicates whether this comment is the leading comment in it's thread. Such a comment cannot be deleted.
|
||||
*/
|
||||
@JsonIgnore
|
||||
Boolean leading;
|
||||
|
||||
@Data
|
||||
public static class Body {
|
||||
List<Block> blocks;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.appsmith.server.domains;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Document
|
||||
public class CommentNotification extends Notification {
|
||||
|
||||
Comment comment;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.appsmith.server.domains;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Document
|
||||
public class CommentThreadNotification extends Notification {
|
||||
|
||||
CommentThread commentThread;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.appsmith.server.domains;
|
||||
|
||||
import com.appsmith.external.models.BaseDomain;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Document
|
||||
public class Notification extends BaseDomain {
|
||||
|
||||
// TODO: This class extends BaseDomain, so it has policies. Should we use information from policies instead of this field?
|
||||
String forUsername;
|
||||
|
||||
/**
|
||||
* Read status for this notification. If it is `true`, then this notification is read. If `false` or `null`, it's unread.
|
||||
*/
|
||||
Boolean isRead;
|
||||
|
||||
public String getType() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -13,11 +13,13 @@ import com.appsmith.server.repositories.ApplicationRepository;
|
|||
import com.appsmith.server.repositories.DatasourceRepository;
|
||||
import com.appsmith.server.repositories.NewActionRepository;
|
||||
import com.appsmith.server.repositories.NewPageRepository;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
|
@ -282,4 +284,18 @@ public class PolicyUtils {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Set<String> findUsernamesWithPermission(Set<Policy> policies, AclPermission permission) {
|
||||
if (CollectionUtils.isNotEmpty(policies) && permission != null) {
|
||||
final String permissionString = permission.getValue();
|
||||
for (Policy policy : policies) {
|
||||
if (permissionString.equals(policy.getPermission())) {
|
||||
return policy.getUsers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.appsmith.server.repositories;
|
||||
|
||||
import com.appsmith.server.domains.Notification;
|
||||
|
||||
public interface CustomNotificationRepository extends AppsmithRepository<Notification> {
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.appsmith.server.repositories;
|
||||
|
||||
import com.appsmith.server.domains.Notification;
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
|
||||
import org.springframework.data.mongodb.core.convert.MongoConverter;
|
||||
|
||||
public class CustomNotificationRepositoryImpl extends BaseAppsmithRepositoryImpl<Notification> implements CustomNotificationRepository {
|
||||
|
||||
public CustomNotificationRepositoryImpl(ReactiveMongoOperations mongoOperations, MongoConverter mongoConverter) {
|
||||
super(mongoOperations, mongoConverter);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.appsmith.server.repositories;
|
||||
|
||||
import com.appsmith.server.domains.Notification;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@Repository
|
||||
public interface NotificationRepository extends BaseRepository<Notification, String>, CustomNotificationRepository {
|
||||
|
||||
Flux<Notification> findByForUsername(String userId);
|
||||
|
||||
}
|
||||
|
|
@ -6,7 +6,10 @@ import com.appsmith.server.acl.PolicyGenerator;
|
|||
import com.appsmith.server.constants.FieldName;
|
||||
import com.appsmith.server.domains.Application;
|
||||
import com.appsmith.server.domains.Comment;
|
||||
import com.appsmith.server.domains.CommentNotification;
|
||||
import com.appsmith.server.domains.CommentThread;
|
||||
import com.appsmith.server.domains.CommentThreadNotification;
|
||||
import com.appsmith.server.domains.Notification;
|
||||
import com.appsmith.server.domains.User;
|
||||
import com.appsmith.server.exceptions.AppsmithError;
|
||||
import com.appsmith.server.exceptions.AppsmithException;
|
||||
|
|
@ -45,6 +48,7 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
|
|||
private final UserService userService;
|
||||
private final SessionUserService sessionUserService;
|
||||
private final ApplicationService applicationService;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
private final PolicyGenerator policyGenerator;
|
||||
private final PolicyUtils policyUtils;
|
||||
|
|
@ -60,6 +64,7 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
|
|||
UserService userService,
|
||||
SessionUserService sessionUserService,
|
||||
ApplicationService applicationService,
|
||||
NotificationService notificationService,
|
||||
PolicyGenerator policyGenerator,
|
||||
PolicyUtils policyUtils
|
||||
) {
|
||||
|
|
@ -68,12 +73,17 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
|
|||
this.userService = userService;
|
||||
this.sessionUserService = sessionUserService;
|
||||
this.applicationService = applicationService;
|
||||
this.notificationService = notificationService;
|
||||
this.policyGenerator = policyGenerator;
|
||||
this.policyUtils = policyUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Comment> create(String threadId, Comment comment) {
|
||||
return create(threadId, comment, true);
|
||||
}
|
||||
|
||||
public Mono<Comment> create(String threadId, Comment comment, boolean shouldCreateNotification) {
|
||||
if (StringUtils.isWhitespace(comment.getAuthorName())) {
|
||||
// Error: User can't explicitly set the author name. It will be the currently logged in user.
|
||||
return Mono.empty();
|
||||
|
|
@ -112,7 +122,29 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
|
|||
|
||||
String authorName = user.getName() != null ? user.getName(): user.getUsername();
|
||||
comment.setAuthorName(authorName);
|
||||
return repository.save(comment);
|
||||
return Mono.zip(
|
||||
Mono.just(user),
|
||||
repository.save(comment)
|
||||
);
|
||||
})
|
||||
.flatMap(tuple -> {
|
||||
final User user = tuple.getT1();
|
||||
final Comment savedComment = tuple.getT2();
|
||||
|
||||
final Set<String> usernames = policyUtils.findUsernamesWithPermission(
|
||||
savedComment.getPolicies(), AclPermission.READ_COMMENT);
|
||||
|
||||
List<Mono<Notification>> monos = new ArrayList<>();
|
||||
for (String username : usernames) {
|
||||
if (!username.equals(user.getUsername())) {
|
||||
final CommentNotification notification = new CommentNotification();
|
||||
notification.setComment(savedComment);
|
||||
notification.setForUsername(username);
|
||||
monos.add(notificationService.create(notification));
|
||||
}
|
||||
}
|
||||
|
||||
return Flux.concat(monos).then(Mono.just(savedComment));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -170,9 +202,12 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
|
|||
List<Mono<Comment>> commentSaverMonos = new ArrayList<>();
|
||||
|
||||
if (!CollectionUtils.isEmpty(thread.getComments())) {
|
||||
thread.getComments().get(0).setLeading(true);
|
||||
boolean isFirst = true;
|
||||
for (final Comment comment : thread.getComments()) {
|
||||
comment.setId(null);
|
||||
commentSaverMonos.add(create(thread.getId(), comment));
|
||||
commentSaverMonos.add(create(thread.getId(), comment, !isFirst));
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,10 +216,28 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
|
|||
return Flux.concat(commentSaverMonos);
|
||||
})
|
||||
.collectList()
|
||||
.map(comments -> {
|
||||
.zipWith(sessionUserService.getCurrentUser())
|
||||
.flatMap(tuple -> {
|
||||
final List<Comment> comments = tuple.getT1();
|
||||
final User user = tuple.getT2();
|
||||
|
||||
commentThread.setComments(comments);
|
||||
commentThread.setIsViewed(true);
|
||||
return commentThread;
|
||||
|
||||
final Set<String> usernames = policyUtils.findUsernamesWithPermission(
|
||||
commentThread.getPolicies(), AclPermission.READ_THREAD);
|
||||
|
||||
List<Mono<Notification>> monos = new ArrayList<>();
|
||||
for (String username : usernames) {
|
||||
if (!username.equals(user.getUsername())) {
|
||||
final CommentThreadNotification notification = new CommentThreadNotification();
|
||||
notification.setCommentThread(commentThread);
|
||||
notification.setForUsername(username);
|
||||
monos.add(notificationService.create(notification));
|
||||
}
|
||||
}
|
||||
|
||||
return Flux.concat(monos).then(Mono.just(commentThread));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.appsmith.server.services;
|
||||
|
||||
import com.appsmith.server.domains.Notification;
|
||||
|
||||
public interface NotificationService extends CrudService<Notification, String> {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.appsmith.server.services;
|
||||
|
||||
import com.appsmith.server.domains.Notification;
|
||||
import com.appsmith.server.repositories.NotificationRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
|
||||
import org.springframework.data.mongodb.core.convert.MongoConverter;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
||||
import javax.validation.Validator;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class NotificationServiceImpl
|
||||
extends BaseService<NotificationRepository, Notification, String>
|
||||
implements NotificationService {
|
||||
|
||||
private final SessionUserService sessionUserService;
|
||||
|
||||
public NotificationServiceImpl(
|
||||
Scheduler scheduler,
|
||||
Validator validator,
|
||||
MongoConverter mongoConverter,
|
||||
ReactiveMongoTemplate reactiveMongoTemplate,
|
||||
NotificationRepository repository,
|
||||
AnalyticsService analyticsService,
|
||||
SessionUserService sessionUserService
|
||||
) {
|
||||
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
|
||||
this.sessionUserService = sessionUserService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Notification> create(Notification notification) {
|
||||
Mono<Notification> notificationWithUsernameMono;
|
||||
if (StringUtils.isEmpty(notification.getForUsername())) {
|
||||
notificationWithUsernameMono = sessionUserService.getCurrentUser()
|
||||
.map(user -> {
|
||||
notification.setForUsername(user.getUsername());
|
||||
return notification;
|
||||
});
|
||||
} else {
|
||||
notificationWithUsernameMono = Mono.just(notification);
|
||||
}
|
||||
|
||||
return notificationWithUsernameMono
|
||||
.flatMap(super::create);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Notification> get(MultiValueMap<String, String> params) {
|
||||
return sessionUserService.getCurrentUser()
|
||||
.flatMapMany(user -> repository.findByForUsername(user.getUsername()));
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user