Adding default implementation in BaseRepositoryImpl for default JPA queries defined by Spring Data.

We override the SimpleReactiveMongoRepository with our custom implementation to add criteria for filtering soft deleted records.
Also, adding a new function to archive record instead of a hard delete.
This commit is contained in:
Arpit Mohan 2020-02-04 12:02:51 +00:00
parent 05cfa3f72f
commit fbada3051d
9 changed files with 196 additions and 58 deletions

View File

@ -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";

View File

@ -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 {
}

View File

@ -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";

View File

@ -15,10 +15,7 @@ public interface ActionRepository extends BaseRepository<Action, String> {
Mono<Action> findByNameAndPageId(String name, String pageId);
Flux<Action> findDistinctActionsByNameInAndPageId(Set<String> names, String pageId);
Flux<Action> findByPageId(String pageId);
Flux<Action> findDistinctActionsByNameInAndPageIdAndActionConfiguration_HttpMethod(Set<String> names, String pageId, String httpMethod);
Flux<Action> saveAll(List<Action> actions);
}

View File

@ -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<T, ID extends Serializable> extends ReactiveMongoRepository<T, ID> {
/**
* 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<T>
*/
Mono<T> 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<Boolean> 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<Boolean> archiveAllById(List<ID> ids);
}

View File

@ -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).
* <p>
* 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 <T> The domain class that extends {@link BaseDomain}. This is required because we use default fields in
* {@link BaseDomain} such as `deleted`
* @param <ID> The ID field that extends Serializable interface
*/
@Slf4j
public class BaseRepositoryImpl<T extends BaseDomain, ID extends Serializable> extends SimpleReactiveMongoRepository<T, ID>
implements BaseRepository<T, ID> {
private final MongoEntityInformation<T, ID> entityInformation;
private final ReactiveMongoOperations mongoOperations;
public BaseRepositoryImpl(@NonNull MongoEntityInformation<T, ID> 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<T> 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<T> findAll() {
Query query = new Query(notDeleted());
return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName());
}
@Override
public Mono<T> 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<Boolean> 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<Boolean> archiveAllById(List<ID> 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);
}
}

View File

@ -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<Action, String> {
@ -17,11 +16,7 @@ public interface ActionService extends CrudService<Action, String> {
Mono<Action> findByNameAndPageId(String name, String pageId);
Flux<Action> findDistinctActionsByNameInAndPageId(Set<String> names, String pageId);
Flux<Action> findDistinctRestApiActionsByNameInAndPageIdAndHttpMethod(Set<String> names, String pageId, String httpMethod);
Flux<Action> saveAll(List<Action> actions);
public Action extractAndSetJsonPathKeys(Action action);
Action extractAndSetJsonPathKeys(Action action);
}

View File

@ -451,21 +451,11 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
return repository.findByNameAndPageId(name, pageId);
}
@Override
public Flux<Action> findDistinctActionsByNameInAndPageId(Set<String> names, String pageId) {
return repository.findDistinctActionsByNameInAndPageId(names, pageId);
}
@Override
public Flux<Action> findDistinctRestApiActionsByNameInAndPageIdAndHttpMethod(Set<String> names, String pageId, String httpMethod) {
return repository.findDistinctActionsByNameInAndPageIdAndActionConfiguration_HttpMethod(names, pageId, httpMethod);
}
@Override
public Flux<Action> saveAll(List<Action> actions) {
return repository.saveAll(actions);
}
/**
* This function replaces the variables in the Object with the actual params
*/

View File

@ -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<ApplicationRepository, A
//Using PageRepository instead of PageService is because a cyclic dependency is introduced if PageService is used here.
//TODO : Solve for this across LayoutService, PageService and ApplicationService.
private final PageRepository pageRepository;
private final ActionRepository actionRepository;
@Autowired
public ApplicationServiceImpl(Scheduler scheduler,
@ -44,10 +48,12 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
ApplicationRepository repository,
AnalyticsService analyticsService,
SessionUserService sessionUserService,
PageRepository pageRepository) {
PageRepository pageRepository,
ActionRepository actionRepository) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.sessionUserService = sessionUserService;
this.pageRepository = pageRepository;
this.actionRepository = actionRepository;
}
@Override
@ -139,17 +145,40 @@ public class ApplicationServiceImpl extends BaseService<ApplicationRepository, A
.map(pages -> 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<Application> delete(String id) {
log.debug("Archiving application with id: {}", id);
Mono<Application> 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<Page> archivedPageMono = pageRepository.archiveById(page);
Mono<List<Action>> 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<Action> 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;