diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/CommonConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/CommonConfig.java index 8fbdbd94c0..9e5c99544a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/CommonConfig.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/CommonConfig.java @@ -1,6 +1,5 @@ package com.appsmith.server.configurations; -import com.appsmith.server.configurations.mongo.SoftDeleteMongoRepositoryFactoryBean; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -9,8 +8,6 @@ import lombok.Setter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.mongodb.config.EnableMongoAuditing; -import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; @@ -21,9 +18,6 @@ import java.util.List; @Getter @Setter @Configuration -@EnableMongoAuditing -@EnableReactiveMongoRepositories(repositoryFactoryBeanClass = SoftDeleteMongoRepositoryFactoryBean.class, - basePackages = "com.appsmith.server.repositories") public class CommonConfig { private String ELASTIC_THREAD_POOL_NAME = "appsmith-elastic-pool"; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java index f7fccfc72f..d696bb5322 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java @@ -1,31 +1,23 @@ package com.appsmith.server.configurations; -import com.mongodb.reactivestreams.client.MongoClient; -import com.mongodb.reactivestreams.client.MongoClients; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration; -import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import com.appsmith.server.configurations.mongo.SoftDeleteMongoRepositoryFactoryBean; +import com.appsmith.server.repositories.BaseRepositoryImpl; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.EnableMongoAuditing; import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories; -@EnableReactiveMongoRepositories -public class MongoConfig extends AbstractReactiveMongoConfiguration { - - @Value("${spring.data.mongodb.database}") - private String dbName; - - @Override - public MongoClient reactiveMongoClient() { - return MongoClients.create(); - } - - @Bean - public ReactiveMongoTemplate reactiveMongoTemplate() throws Exception { - return new ReactiveMongoTemplate(reactiveMongoClient(), dbName); - } - - @Override - protected String getDatabaseName() { - return dbName; - } +/** + * This configures the JPA Mongo repositories. The default base implementation is defined in {@link BaseRepositoryImpl}. + * This is required to add default clauses for default JPA queries defined by Spring Data. + * + * The factoryBean class is also custom defined in order to add default clauses for soft delete for all custom JPA queries. + * {@link SoftDeleteMongoRepositoryFactoryBean} for details. + */ +@Configuration +@EnableMongoAuditing +@EnableReactiveMongoRepositories(repositoryFactoryBeanClass = SoftDeleteMongoRepositoryFactoryBean.class, + basePackages = "com.appsmith.server.repositories", + repositoryBaseClass = BaseRepositoryImpl.class +) +public class MongoConfig { } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java index 26b0c3f2f9..a294079482 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java @@ -3,6 +3,7 @@ package com.appsmith.server.constants; public class FieldName { public static final String EMAIL = "email"; public static final String ORGANIZATION_ID = "organizationId"; + public static final String DELETED = "deleted"; public static String ORGANIZATION = "organization"; public static String ID = "id"; public static String NAME = "name"; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ActionRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ActionRepository.java index 7723421e4d..6d62775cae 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ActionRepository.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ActionRepository.java @@ -15,10 +15,7 @@ public interface ActionRepository extends BaseRepository { Mono findByNameAndPageId(String name, String pageId); - Flux findDistinctActionsByNameInAndPageId(Set names, String pageId); + Flux findByPageId(String pageId); Flux findDistinctActionsByNameInAndPageIdAndActionConfiguration_HttpMethod(Set names, String pageId, String httpMethod); - - Flux saveAll(List actions); - } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseRepository.java index cf4cb1b4a3..eef3852672 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseRepository.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseRepository.java @@ -2,10 +2,36 @@ package com.appsmith.server.repositories; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.repository.NoRepositoryBean; +import reactor.core.publisher.Mono; import java.io.Serializable; +import java.util.List; @NoRepositoryBean public interface BaseRepository extends ReactiveMongoRepository { + /** + * This function sets the deleted flag to true and then saves the modified document. + * + * @param T The entity which needs to be archived + * @return Mono + */ + Mono archiveById(T entity); + + /** + * This function directly updates the document by setting the deleted flag to true for the entity with the given id + * + * @param id The id of the document that needs to be archived + * @return + */ + Mono archiveById(ID id); + + /** + * This function directly updates the DB by setting the deleted flag to true for all the documents in the collection + * with the given list of ids. + * + * @param ids The list of ids of the document that needs to be archived. + * @return + */ + Mono archiveAllById(List ids); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseRepositoryImpl.java new file mode 100644 index 0000000000..16336d0654 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/BaseRepositoryImpl.java @@ -0,0 +1,114 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.BaseDomain; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.Serializable; +import java.util.List; + +import static org.springframework.data.mongodb.core.query.Criteria.where; + +/** + * This repository implementation is the base class that will be used by Spring Data running all the default JPA queries. + * We override the default implementation {@link SimpleReactiveMongoRepository} to filter out records marked with + * deleted=true. + * To enable this base implementation, it MUST be set in the annotation @EnableReactiveMongoRepositories.repositoryBaseClass. + * This is currently defined in {@link com.appsmith.server.configurations.MongoConfig} (liable to change in the future). + *

+ * An implementation like this can also be used to set default query parameters based on the user's role and permissions + * to filter out data that they are allowed to see. This is will be implemented with ACL. + * + * @param The domain class that extends {@link BaseDomain}. This is required because we use default fields in + * {@link BaseDomain} such as `deleted` + * @param The ID field that extends Serializable interface + */ +@Slf4j +public class BaseRepositoryImpl extends SimpleReactiveMongoRepository + implements BaseRepository { + + private final MongoEntityInformation entityInformation; + private final ReactiveMongoOperations mongoOperations; + + public BaseRepositoryImpl(@NonNull MongoEntityInformation entityInformation, + @NonNull ReactiveMongoOperations mongoOperations) { + super(entityInformation, mongoOperations); + this.entityInformation = entityInformation; + this.mongoOperations = mongoOperations; + } + + private Criteria notDeleted() { + return new Criteria().orOperator( + where("deleted").exists(false), + where("deleted").is(false) + ); + } + + private Criteria getIdCriteria(Object id) { + return where(entityInformation.getIdAttribute()).is(id); + } + + @Override + public Mono findById(ID id) { + Assert.notNull(id, "The given id must not be null!"); + Query query = new Query(getIdCriteria(id)); + query.addCriteria(notDeleted()); + return mongoOperations.query(entityInformation.getJavaType()) + .inCollection(entityInformation.getCollectionName()) + .matching(query) + .one(); + } + + @Override + public Flux findAll() { + Query query = new Query(notDeleted()); + return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); + } + + @Override + public Mono archiveById(T entity) { + Assert.notNull(entity, "The given entity must not be null!"); + Assert.notNull(entity.getId(), "The given entity's id must not be null!"); + Assert.isTrue(!entity.getDeleted(), "The given entity is already deleted"); + + entity.setDeleted(true); + return mongoOperations.save(entity, entityInformation.getCollectionName()); + } + + @Override + public Mono archiveById(ID id) { + Assert.notNull(id, "The given id must not be null!"); + Query query = new Query(getIdCriteria(id)); + query.addCriteria(notDeleted()); + + Update update = new Update(); + update.set(FieldName.DELETED, true); + return mongoOperations.updateFirst(query, update, entityInformation.getJavaType()) + .map(result -> result.getModifiedCount() > 0 ? true : false); + } + + @Override + public Mono archiveAllById(List ids) { + Assert.notNull(ids, "The given ids must not be null!"); + Assert.notEmpty(ids, "The given list of ids must not be empty!"); + + Query query = new Query(); + query.addCriteria(new Criteria().where(FieldName.ID).in(ids)); + query.addCriteria(notDeleted()); + + Update update = new Update(); + update.set(FieldName.DELETED, true); + return mongoOperations.updateMulti(query, update, entityInformation.getJavaType()) + .map(result -> result.getModifiedCount() > 0 ? true : false); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java index 4fd9c6915e..981716ba4b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java @@ -6,7 +6,6 @@ import com.appsmith.server.dtos.ExecuteActionDTO; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.List; import java.util.Set; public interface ActionService extends CrudService { @@ -17,11 +16,7 @@ public interface ActionService extends CrudService { Mono findByNameAndPageId(String name, String pageId); - Flux findDistinctActionsByNameInAndPageId(Set names, String pageId); - Flux findDistinctRestApiActionsByNameInAndPageIdAndHttpMethod(Set names, String pageId, String httpMethod); - Flux saveAll(List actions); - - public Action extractAndSetJsonPathKeys(Action action); + Action extractAndSetJsonPathKeys(Action action); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java index 05a8227514..eb7cf2a044 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java @@ -451,21 +451,11 @@ public class ActionServiceImpl extends BaseService findDistinctActionsByNameInAndPageId(Set names, String pageId) { - return repository.findDistinctActionsByNameInAndPageId(names, pageId); - } - @Override public Flux findDistinctRestApiActionsByNameInAndPageIdAndHttpMethod(Set names, String pageId, String httpMethod) { return repository.findDistinctActionsByNameInAndPageIdAndActionConfiguration_HttpMethod(names, pageId, httpMethod); } - @Override - public Flux saveAll(List actions) { - return repository.saveAll(actions); - } - /** * This function replaces the variables in the Object with the actual params */ diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java index 9b524b0d11..e0ac89801b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java @@ -2,12 +2,15 @@ package com.appsmith.server.services; import com.appsmith.server.constants.AnalyticsEvents; import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.repositories.ActionRepository; import com.appsmith.server.repositories.ApplicationRepository; import com.appsmith.server.repositories.PageRepository; import lombok.extern.slf4j.Slf4j; @@ -35,6 +38,7 @@ public class ApplicationServiceImpl extends BaseService true); } + /** + * This function performs a soft delete for the application along with it's associated pages and actions. + * + * @param id The application id to delete + * @return The modified application object with the deleted flag set + */ @Override public Mono delete(String id) { log.debug("Archiving application with id: {}", id); + Mono applicationMono = repository.findById(id) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "application", id))); + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "application", id))) + .flatMap(application -> pageRepository.findByApplicationId(id) + .flatMap(page -> { + log.debug("Going to archive pageId: {} for applicationId: {}", page.getId(), id); + Mono archivedPageMono = pageRepository.archiveById(page); + Mono> archivedActionsMono = actionRepository.findByPageId(page.getId()) + .flatMap(action -> { + log.debug("Going to archive actionId: {} for applicationId: {}", action.getId(), id); + return actionRepository.archiveById(action); + }) + .collectList(); + return Mono.zip(archivedPageMono, archivedActionsMono) + .map(tuple -> { + Page page1 = tuple.getT1(); + List actions = tuple.getT2(); + log.debug("Archived pageId: {} and {} actions for applicationId: {}", page1.getId(), actions.size(), id); + return page1; + }); + }) + .collectList() + .flatMap(pages -> repository.archiveById(application))); return applicationMono - .flatMap(deletedObj -> { - deletedObj.setDeleted(true); - return repository.save(deletedObj); - }) .map(deletedObj -> { analyticsService.sendEvent(AnalyticsEvents.DELETE + "_" + deletedObj.getClass().getSimpleName().toUpperCase(), (Application) deletedObj); return (Application) deletedObj;