feat: Add a restore point for Applications (#20933)
## Description This PR adds a way to take a snapshot of an application on the database. That snapshot can be restored to the application. Under the hood it uses the Export-Import feature. Fixes #19720
This commit is contained in:
parent
bc6c40a50c
commit
2116a2cc70
|
|
@ -4,6 +4,7 @@ import com.appsmith.server.constants.Url;
|
||||||
import com.appsmith.server.controllers.ce.ApplicationControllerCE;
|
import com.appsmith.server.controllers.ce.ApplicationControllerCE;
|
||||||
import com.appsmith.server.services.ApplicationPageService;
|
import com.appsmith.server.services.ApplicationPageService;
|
||||||
import com.appsmith.server.services.ApplicationService;
|
import com.appsmith.server.services.ApplicationService;
|
||||||
|
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||||
import com.appsmith.server.services.ThemeService;
|
import com.appsmith.server.services.ThemeService;
|
||||||
import com.appsmith.server.solutions.ApplicationFetcher;
|
import com.appsmith.server.solutions.ApplicationFetcher;
|
||||||
import com.appsmith.server.solutions.ApplicationForkingService;
|
import com.appsmith.server.solutions.ApplicationForkingService;
|
||||||
|
|
@ -20,10 +21,11 @@ public class ApplicationController extends ApplicationControllerCE {
|
||||||
ApplicationFetcher applicationFetcher,
|
ApplicationFetcher applicationFetcher,
|
||||||
ApplicationForkingService applicationForkingService,
|
ApplicationForkingService applicationForkingService,
|
||||||
ImportExportApplicationService importExportApplicationService,
|
ImportExportApplicationService importExportApplicationService,
|
||||||
ThemeService themeService) {
|
ThemeService themeService,
|
||||||
|
ApplicationSnapshotService applicationSnapshotService) {
|
||||||
|
|
||||||
super(service, applicationPageService, applicationFetcher, applicationForkingService,
|
super(service, applicationPageService, applicationFetcher, applicationForkingService,
|
||||||
importExportApplicationService, themeService);
|
importExportApplicationService, themeService, applicationSnapshotService);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,26 @@ import com.appsmith.external.models.Datasource;
|
||||||
import com.appsmith.server.constants.FieldName;
|
import com.appsmith.server.constants.FieldName;
|
||||||
import com.appsmith.server.constants.Url;
|
import com.appsmith.server.constants.Url;
|
||||||
import com.appsmith.server.domains.Application;
|
import com.appsmith.server.domains.Application;
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
import com.appsmith.server.domains.GitAuth;
|
import com.appsmith.server.domains.GitAuth;
|
||||||
import com.appsmith.server.domains.Theme;
|
import com.appsmith.server.domains.Theme;
|
||||||
import com.appsmith.server.dtos.ApplicationAccessDTO;
|
import com.appsmith.server.dtos.ApplicationAccessDTO;
|
||||||
import com.appsmith.server.dtos.ApplicationImportDTO;
|
import com.appsmith.server.dtos.ApplicationImportDTO;
|
||||||
import com.appsmith.server.dtos.ApplicationPagesDTO;
|
import com.appsmith.server.dtos.ApplicationPagesDTO;
|
||||||
import com.appsmith.server.dtos.GitAuthDTO;
|
import com.appsmith.server.dtos.GitAuthDTO;
|
||||||
|
import com.appsmith.server.dtos.ReleaseItemsDTO;
|
||||||
import com.appsmith.server.dtos.ResponseDTO;
|
import com.appsmith.server.dtos.ResponseDTO;
|
||||||
import com.appsmith.server.dtos.UserHomepageDTO;
|
import com.appsmith.server.dtos.UserHomepageDTO;
|
||||||
import com.appsmith.server.dtos.ReleaseItemsDTO;
|
|
||||||
import com.appsmith.server.exceptions.AppsmithError;
|
import com.appsmith.server.exceptions.AppsmithError;
|
||||||
import com.appsmith.server.exceptions.AppsmithException;
|
import com.appsmith.server.exceptions.AppsmithException;
|
||||||
import com.appsmith.server.services.ApplicationPageService;
|
import com.appsmith.server.services.ApplicationPageService;
|
||||||
import com.appsmith.server.services.ApplicationService;
|
import com.appsmith.server.services.ApplicationService;
|
||||||
|
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||||
import com.appsmith.server.services.ThemeService;
|
import com.appsmith.server.services.ThemeService;
|
||||||
import com.appsmith.server.solutions.ApplicationFetcher;
|
import com.appsmith.server.solutions.ApplicationFetcher;
|
||||||
import com.appsmith.server.solutions.ApplicationForkingService;
|
import com.appsmith.server.solutions.ApplicationForkingService;
|
||||||
import com.appsmith.server.solutions.ImportExportApplicationService;
|
import com.appsmith.server.solutions.ImportExportApplicationService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
|
@ -44,7 +47,6 @@ import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|
@ -56,6 +58,7 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
|
||||||
private final ApplicationForkingService applicationForkingService;
|
private final ApplicationForkingService applicationForkingService;
|
||||||
private final ImportExportApplicationService importExportApplicationService;
|
private final ImportExportApplicationService importExportApplicationService;
|
||||||
private final ThemeService themeService;
|
private final ThemeService themeService;
|
||||||
|
private final ApplicationSnapshotService applicationSnapshotService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ApplicationControllerCE(
|
public ApplicationControllerCE(
|
||||||
|
|
@ -63,13 +66,16 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
|
||||||
ApplicationPageService applicationPageService,
|
ApplicationPageService applicationPageService,
|
||||||
ApplicationFetcher applicationFetcher,
|
ApplicationFetcher applicationFetcher,
|
||||||
ApplicationForkingService applicationForkingService,
|
ApplicationForkingService applicationForkingService,
|
||||||
ImportExportApplicationService importExportApplicationService, ThemeService themeService) {
|
ImportExportApplicationService importExportApplicationService,
|
||||||
|
ThemeService themeService,
|
||||||
|
ApplicationSnapshotService applicationSnapshotService) {
|
||||||
super(service);
|
super(service);
|
||||||
this.applicationPageService = applicationPageService;
|
this.applicationPageService = applicationPageService;
|
||||||
this.applicationFetcher = applicationFetcher;
|
this.applicationFetcher = applicationFetcher;
|
||||||
this.applicationForkingService = applicationForkingService;
|
this.applicationForkingService = applicationForkingService;
|
||||||
this.importExportApplicationService = importExportApplicationService;
|
this.importExportApplicationService = importExportApplicationService;
|
||||||
this.themeService = themeService;
|
this.themeService = themeService;
|
||||||
|
this.applicationSnapshotService = applicationSnapshotService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
|
|
@ -178,6 +184,31 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/snapshot/{id}")
|
||||||
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
|
public Mono<ResponseDTO<Boolean>> createSnapshot(@PathVariable String id, @RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName) {
|
||||||
|
log.debug("Going to create snapshot with application id: {}, branch: {}", id, branchName);
|
||||||
|
|
||||||
|
return applicationSnapshotService.createApplicationSnapshot(id, branchName)
|
||||||
|
.map(result -> new ResponseDTO<>(HttpStatus.CREATED.value(), result, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/snapshot/{id}")
|
||||||
|
public Mono<ResponseDTO<ApplicationSnapshot>> getSnapshotWithoutApplicationJson(@PathVariable String id, @RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName) {
|
||||||
|
log.debug("Going to get snapshot with application id: {}, branch: {}", id, branchName);
|
||||||
|
|
||||||
|
return applicationSnapshotService.getWithoutDataByApplicationId(id, branchName)
|
||||||
|
.map(applicationSnapshot -> new ResponseDTO<>(HttpStatus.OK.value(), applicationSnapshot, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/snapshot/{id}/restore")
|
||||||
|
public Mono<ResponseDTO<Application>> restoreSnapshot(@PathVariable String id, @RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName) {
|
||||||
|
log.debug("Going to restore snapshot with application id: {}, branch: {}", id, branchName);
|
||||||
|
|
||||||
|
return applicationSnapshotService.restoreSnapshot(id, branchName)
|
||||||
|
.map(application -> new ResponseDTO<>(HttpStatus.OK.value(), application, null));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/import/{workspaceId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(value = "/import/{workspaceId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public Mono<ResponseDTO<ApplicationImportDTO>> importApplicationFromFile(@RequestPart("file") Mono<Part> fileMono,
|
public Mono<ResponseDTO<ApplicationImportDTO>> importApplicationFromFile(@RequestPart("file") Mono<Part> fileMono,
|
||||||
@PathVariable String workspaceId) {
|
@PathVariable String workspaceId) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.appsmith.server.domains;
|
||||||
|
|
||||||
|
import com.appsmith.external.models.BaseDomain;
|
||||||
|
import com.appsmith.server.helpers.DateUtils;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This stores a snapshot of an application. If a snapshot is more than 15 MB, we'll break it into smaller chunks.
|
||||||
|
* Both the root chunk and the child chunks will be stored in this collection.
|
||||||
|
* We'll use some attributes to create and maintain the sequence of the chunks.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Document
|
||||||
|
public class ApplicationSnapshot extends BaseDomain {
|
||||||
|
private String applicationId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* binary data, will be present always
|
||||||
|
*/
|
||||||
|
private byte [] data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chunkOrder: present only in child chunks. Used to maintain the order of the chunks.
|
||||||
|
* if a parent has 3 child chunks, the first one will have chunkOrder=1, second one 2 and third one 3
|
||||||
|
*/
|
||||||
|
private int chunkOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adding this method as updatedAt field in BaseDomain is annotated with @JsonIgnore
|
||||||
|
* @return Updated at timestamp in ISO format
|
||||||
|
*/
|
||||||
|
public String getUpdatedTime() {
|
||||||
|
return DateUtils.ISO_FORMATTER.format(this.getUpdatedAt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.appsmith.server.migrations.db.ce;
|
||||||
|
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
|
import com.appsmith.server.domains.QApplicationSnapshot;
|
||||||
|
import io.mongock.api.annotations.ChangeUnit;
|
||||||
|
import io.mongock.api.annotations.Execution;
|
||||||
|
import io.mongock.api.annotations.RollbackExecution;
|
||||||
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
|
import org.springframework.data.mongodb.core.index.Index;
|
||||||
|
|
||||||
|
import static com.appsmith.server.migrations.DatabaseChangelog1.ensureIndexes;
|
||||||
|
import static com.appsmith.server.migrations.DatabaseChangelog1.makeIndex;
|
||||||
|
import static com.appsmith.server.repositories.ce.BaseAppsmithRepositoryCEImpl.fieldName;
|
||||||
|
|
||||||
|
@ChangeUnit(order = "004", id="create-index-for-application-snapshot-collection")
|
||||||
|
public class Migration004CreateIndexForApplicationSnapshotMigration {
|
||||||
|
private final MongoTemplate mongoTemplate;
|
||||||
|
|
||||||
|
public Migration004CreateIndexForApplicationSnapshotMigration(MongoTemplate mongoTemplate) {
|
||||||
|
this.mongoTemplate = mongoTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RollbackExecution
|
||||||
|
public void demoRollbackExecution() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Execution
|
||||||
|
public void addIndexOnApplicationIdAndChunkOrder() {
|
||||||
|
Index applicationIdChunkOrderUniqueIndex = makeIndex(
|
||||||
|
fieldName(QApplicationSnapshot.applicationSnapshot.applicationId),
|
||||||
|
fieldName(QApplicationSnapshot.applicationSnapshot.chunkOrder)
|
||||||
|
).named("applicationId_chunkOrder_unique_index").unique();
|
||||||
|
|
||||||
|
ensureIndexes(mongoTemplate, ApplicationSnapshot.class, applicationIdChunkOrderUniqueIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.appsmith.server.repositories;
|
||||||
|
|
||||||
|
import com.appsmith.server.repositories.ce.ApplicationSnapshotRepositoryCE;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ApplicationSnapshotRepository extends ApplicationSnapshotRepositoryCE, CustomApplicationSnapshotRepository {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.appsmith.server.repositories;
|
||||||
|
|
||||||
|
import com.appsmith.server.repositories.ce.CustomApplicationSnapshotRepositoryCE;
|
||||||
|
|
||||||
|
public interface CustomApplicationSnapshotRepository extends CustomApplicationSnapshotRepositoryCE {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.appsmith.server.repositories;
|
||||||
|
|
||||||
|
import com.appsmith.server.repositories.ce.CustomApplicationSnapshotRepositoryCEImpl;
|
||||||
|
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
|
||||||
|
import org.springframework.data.mongodb.core.convert.MongoConverter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CustomApplicationSnapshotRepositoryImpl extends CustomApplicationSnapshotRepositoryCEImpl implements CustomApplicationSnapshotRepository {
|
||||||
|
public CustomApplicationSnapshotRepositoryImpl(ReactiveMongoOperations mongoOperations, MongoConverter mongoConverter, CacheableRepositoryHelper cacheableRepositoryHelper) {
|
||||||
|
super(mongoOperations, mongoConverter, cacheableRepositoryHelper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.appsmith.server.repositories.ce;
|
||||||
|
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
|
import com.appsmith.server.repositories.BaseRepository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public interface ApplicationSnapshotRepositoryCE extends CustomApplicationSnapshotRepositoryCE, BaseRepository<ApplicationSnapshot, String> {
|
||||||
|
Flux<ApplicationSnapshot> findByApplicationId(String applicationId);
|
||||||
|
Mono<Void> deleteAllByApplicationId(String applicationId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.appsmith.server.repositories.ce;
|
||||||
|
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
|
import com.appsmith.server.repositories.AppsmithRepository;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public interface CustomApplicationSnapshotRepositoryCE extends AppsmithRepository<ApplicationSnapshot> {
|
||||||
|
Mono<ApplicationSnapshot> findWithoutData(String applicationId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.appsmith.server.repositories.ce;
|
||||||
|
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
|
import com.appsmith.server.domains.QApplicationSnapshot;
|
||||||
|
import com.appsmith.server.repositories.BaseAppsmithRepositoryImpl;
|
||||||
|
import com.appsmith.server.repositories.CacheableRepositoryHelper;
|
||||||
|
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
|
||||||
|
import org.springframework.data.mongodb.core.convert.MongoConverter;
|
||||||
|
import org.springframework.data.mongodb.core.query.Criteria;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CustomApplicationSnapshotRepositoryCEImpl extends BaseAppsmithRepositoryImpl<ApplicationSnapshot>
|
||||||
|
implements CustomApplicationSnapshotRepositoryCE {
|
||||||
|
|
||||||
|
public CustomApplicationSnapshotRepositoryCEImpl(ReactiveMongoOperations mongoOperations, MongoConverter mongoConverter, CacheableRepositoryHelper cacheableRepositoryHelper) {
|
||||||
|
super(mongoOperations, mongoConverter, cacheableRepositoryHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ApplicationSnapshot> findWithoutData(String applicationId) {
|
||||||
|
List<Criteria> criteriaList = new ArrayList<>();
|
||||||
|
criteriaList.add(
|
||||||
|
Criteria.where(fieldName(QApplicationSnapshot.applicationSnapshot.applicationId)).is(applicationId)
|
||||||
|
);
|
||||||
|
criteriaList.add(
|
||||||
|
Criteria.where(fieldName(QApplicationSnapshot.applicationSnapshot.chunkOrder)).is(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
List<String> fieldNames = List.of(
|
||||||
|
fieldName(QApplicationSnapshot.applicationSnapshot.applicationId),
|
||||||
|
fieldName(QApplicationSnapshot.applicationSnapshot.chunkOrder),
|
||||||
|
fieldName(QApplicationSnapshot.applicationSnapshot.createdAt),
|
||||||
|
fieldName(QApplicationSnapshot.applicationSnapshot.updatedAt)
|
||||||
|
);
|
||||||
|
return queryOne(criteriaList, fieldNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.appsmith.server.services;
|
||||||
|
|
||||||
|
import com.appsmith.server.services.ce.ApplicationSnapshotServiceCE;
|
||||||
|
|
||||||
|
public interface ApplicationSnapshotService extends ApplicationSnapshotServiceCE {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.appsmith.server.services;
|
||||||
|
|
||||||
|
import com.appsmith.server.repositories.ApplicationSnapshotRepository;
|
||||||
|
import com.appsmith.server.services.ce.ApplicationSnapshotServiceCEImpl;
|
||||||
|
import com.appsmith.server.solutions.ApplicationPermission;
|
||||||
|
import com.appsmith.server.solutions.ImportExportApplicationService;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ApplicationSnapshotServiceImpl extends ApplicationSnapshotServiceCEImpl implements ApplicationSnapshotService {
|
||||||
|
|
||||||
|
public ApplicationSnapshotServiceImpl(ApplicationSnapshotRepository applicationSnapshotRepository, ApplicationService applicationService, ImportExportApplicationService importExportApplicationService, ApplicationPermission applicationPermission, Gson gson) {
|
||||||
|
super(applicationSnapshotRepository, applicationService, importExportApplicationService, applicationPermission, gson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.appsmith.server.services.ce;
|
||||||
|
|
||||||
|
import com.appsmith.server.domains.Application;
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public interface ApplicationSnapshotServiceCE {
|
||||||
|
/**
|
||||||
|
* This method will create a new snapshot of the provided applicationId and branch name and store in the
|
||||||
|
* ApplicationSnapshot collection.
|
||||||
|
* @param applicationId ID of the application, default application ID if application is connected to Git
|
||||||
|
* @param branchName name of the Git branch, null or empty if not connected to Git
|
||||||
|
* @return Created snapshot ID
|
||||||
|
*/
|
||||||
|
Mono<Boolean> createApplicationSnapshot(String applicationId, String branchName);
|
||||||
|
|
||||||
|
Mono<ApplicationSnapshot> getWithoutDataByApplicationId(String applicationId, String branchName);
|
||||||
|
|
||||||
|
Mono<Application> restoreSnapshot(String applicationId, String branchName);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
package com.appsmith.server.services.ce;
|
||||||
|
|
||||||
|
import com.appsmith.server.constants.FieldName;
|
||||||
|
import com.appsmith.server.constants.SerialiseApplicationObjective;
|
||||||
|
import com.appsmith.server.domains.Application;
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
|
import com.appsmith.server.dtos.ApplicationJson;
|
||||||
|
import com.appsmith.server.exceptions.AppsmithError;
|
||||||
|
import com.appsmith.server.exceptions.AppsmithException;
|
||||||
|
import com.appsmith.server.repositories.ApplicationSnapshotRepository;
|
||||||
|
import com.appsmith.server.services.ApplicationService;
|
||||||
|
import com.appsmith.server.solutions.ApplicationPermission;
|
||||||
|
import com.appsmith.server.solutions.ImportExportApplicationService;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ApplicationSnapshotServiceCEImpl implements ApplicationSnapshotServiceCE {
|
||||||
|
private final ApplicationSnapshotRepository applicationSnapshotRepository;
|
||||||
|
private final ApplicationService applicationService;
|
||||||
|
private final ImportExportApplicationService importExportApplicationService;
|
||||||
|
private final ApplicationPermission applicationPermission;
|
||||||
|
private final Gson gson;
|
||||||
|
|
||||||
|
private static final int MAX_SNAPSHOT_SIZE = 15*1024*1024; // 15 MB
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Boolean> createApplicationSnapshot(String applicationId, String branchName) {
|
||||||
|
return applicationService.findBranchedApplicationId(branchName, applicationId, applicationPermission.getEditPermission())
|
||||||
|
/* SerialiseApplicationObjective=VERSION_CONTROL because this API can be invoked from developers.
|
||||||
|
exportApplicationById method check for MANAGE_PERMISSION if SerialiseApplicationObjective=SHARE.
|
||||||
|
*/
|
||||||
|
.flatMap(branchedAppId ->
|
||||||
|
Mono.zip(
|
||||||
|
importExportApplicationService.exportApplicationById(branchedAppId, SerialiseApplicationObjective.VERSION_CONTROL),
|
||||||
|
Mono.just(branchedAppId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flatMapMany(objects -> {
|
||||||
|
String branchedAppId = objects.getT2();
|
||||||
|
ApplicationJson applicationJson = objects.getT1();
|
||||||
|
return applicationSnapshotRepository.deleteAllByApplicationId(branchedAppId)
|
||||||
|
.thenMany(createSnapshots(branchedAppId, applicationJson));
|
||||||
|
})
|
||||||
|
.then(Mono.just(Boolean.TRUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Flux<ApplicationSnapshot> createSnapshots(String applicationId, ApplicationJson applicationJson) {
|
||||||
|
String json = gson.toJson(applicationJson);
|
||||||
|
// check the size of the exported json before storing to avoid mongodb document size limit
|
||||||
|
byte[] utf8JsonString = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
List<ApplicationSnapshot> applicationSnapshots = createSnapshotsObjects(utf8JsonString, applicationId);
|
||||||
|
return applicationSnapshotRepository.saveAll(applicationSnapshots);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ApplicationSnapshot> getWithoutDataByApplicationId(String applicationId, String branchName) {
|
||||||
|
// get application first to check the permission and get child aka branched application ID
|
||||||
|
return applicationService.findBranchedApplicationId(branchName, applicationId, applicationPermission.getEditPermission())
|
||||||
|
.switchIfEmpty(Mono.error(
|
||||||
|
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId))
|
||||||
|
)
|
||||||
|
.flatMap(applicationSnapshotRepository::findWithoutData)
|
||||||
|
.switchIfEmpty(Mono.error(
|
||||||
|
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Application> restoreSnapshot(String applicationId, String branchName) {
|
||||||
|
return applicationService.findByBranchNameAndDefaultApplicationId(branchName, applicationId, applicationPermission.getEditPermission())
|
||||||
|
.switchIfEmpty(Mono.error(
|
||||||
|
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId))
|
||||||
|
)
|
||||||
|
.flatMap(
|
||||||
|
application -> getApplicationJsonStringFromSnapShot(application.getId())
|
||||||
|
.zipWith(Mono.just(application))
|
||||||
|
)
|
||||||
|
.flatMap(objects -> {
|
||||||
|
String applicationJsonString = objects.getT1();
|
||||||
|
Application application = objects.getT2();
|
||||||
|
ApplicationJson applicationJson = gson.fromJson(applicationJsonString, ApplicationJson.class);
|
||||||
|
return importExportApplicationService.importApplicationInWorkspace(
|
||||||
|
application.getWorkspaceId(), applicationJson, application.getId(), branchName
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<String> getApplicationJsonStringFromSnapShot(String applicationId) {
|
||||||
|
return applicationSnapshotRepository.findByApplicationId(applicationId)
|
||||||
|
.sort(Comparator.comparingInt(ApplicationSnapshot::getChunkOrder))
|
||||||
|
.map(ApplicationSnapshot::getData)
|
||||||
|
.collectList()
|
||||||
|
.map(bytes -> {
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
for(byte [] b: bytes) {
|
||||||
|
outputStream.writeBytes(b);
|
||||||
|
}
|
||||||
|
return outputStream.toString(StandardCharsets.UTF_8);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ApplicationSnapshot> createSnapshotsObjects(byte [] bytes, String applicationId) {
|
||||||
|
List<ApplicationSnapshot> applicationSnapshots = new ArrayList<>();
|
||||||
|
int total = bytes.length;
|
||||||
|
int copiedCount = 0;
|
||||||
|
int chunkOrder = 1;
|
||||||
|
|
||||||
|
while (copiedCount < total) {
|
||||||
|
int currentChunkSize = MAX_SNAPSHOT_SIZE;
|
||||||
|
if(copiedCount + currentChunkSize > total) {
|
||||||
|
currentChunkSize = total - copiedCount;
|
||||||
|
}
|
||||||
|
byte [] sub = new byte[currentChunkSize];
|
||||||
|
System.arraycopy(bytes, copiedCount, sub, 0, currentChunkSize);
|
||||||
|
copiedCount += currentChunkSize;
|
||||||
|
|
||||||
|
// create snapshot that'll contain the chunk of data
|
||||||
|
ApplicationSnapshot applicationSnapshot = new ApplicationSnapshot();
|
||||||
|
applicationSnapshot.setData(sub);
|
||||||
|
applicationSnapshot.setApplicationId(applicationId);
|
||||||
|
applicationSnapshot.setChunkOrder(chunkOrder);
|
||||||
|
applicationSnapshots.add(applicationSnapshot);
|
||||||
|
|
||||||
|
chunkOrder++;
|
||||||
|
}
|
||||||
|
return applicationSnapshots;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import com.appsmith.server.services.ActionCollectionService;
|
||||||
import com.appsmith.server.services.AnalyticsService;
|
import com.appsmith.server.services.AnalyticsService;
|
||||||
import com.appsmith.server.services.ApplicationPageService;
|
import com.appsmith.server.services.ApplicationPageService;
|
||||||
import com.appsmith.server.services.ApplicationService;
|
import com.appsmith.server.services.ApplicationService;
|
||||||
|
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||||
import com.appsmith.server.services.CustomJSLibService;
|
import com.appsmith.server.services.CustomJSLibService;
|
||||||
import com.appsmith.server.services.DatasourceService;
|
import com.appsmith.server.services.DatasourceService;
|
||||||
import com.appsmith.server.services.NewActionService;
|
import com.appsmith.server.services.NewActionService;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import com.appsmith.server.services.ActionCollectionService;
|
||||||
import com.appsmith.server.services.AnalyticsService;
|
import com.appsmith.server.services.AnalyticsService;
|
||||||
import com.appsmith.server.services.ApplicationPageService;
|
import com.appsmith.server.services.ApplicationPageService;
|
||||||
import com.appsmith.server.services.ApplicationService;
|
import com.appsmith.server.services.ApplicationService;
|
||||||
|
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||||
|
import com.appsmith.server.services.CustomJSLibService;
|
||||||
import com.appsmith.server.services.DatasourceService;
|
import com.appsmith.server.services.DatasourceService;
|
||||||
import com.appsmith.server.services.NewActionService;
|
import com.appsmith.server.services.NewActionService;
|
||||||
import com.appsmith.server.services.NewPageService;
|
import com.appsmith.server.services.NewPageService;
|
||||||
|
|
@ -16,13 +18,11 @@ import com.appsmith.server.services.SequenceService;
|
||||||
import com.appsmith.server.services.SessionUserService;
|
import com.appsmith.server.services.SessionUserService;
|
||||||
import com.appsmith.server.services.ThemeService;
|
import com.appsmith.server.services.ThemeService;
|
||||||
import com.appsmith.server.services.WorkspaceService;
|
import com.appsmith.server.services.WorkspaceService;
|
||||||
import com.appsmith.server.services.CustomJSLibService;
|
|
||||||
import com.appsmith.server.solutions.ce.ImportExportApplicationServiceCEImplV2;
|
import com.appsmith.server.solutions.ce.ImportExportApplicationServiceCEImplV2;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.transaction.reactive.TransactionalOperator;
|
import org.springframework.transaction.reactive.TransactionalOperator;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|
@ -54,12 +54,14 @@ public class ImportExportApplicationServiceImplV2 extends ImportExportApplicatio
|
||||||
PagePermission pagePermission,
|
PagePermission pagePermission,
|
||||||
ActionPermission actionPermission,
|
ActionPermission actionPermission,
|
||||||
Gson gson,
|
Gson gson,
|
||||||
TransactionalOperator transactionalOperator) {
|
TransactionalOperator transactionalOperator,
|
||||||
|
ApplicationSnapshotService applicationSnapshotService) {
|
||||||
|
|
||||||
super(datasourceService, sessionUserService, newActionRepository, datasourceRepository, pluginRepository,
|
super(datasourceService, sessionUserService, newActionRepository, datasourceRepository, pluginRepository,
|
||||||
workspaceService, applicationService, newPageService, applicationPageService, newPageRepository,
|
workspaceService, applicationService, newPageService, applicationPageService, newPageRepository,
|
||||||
newActionService, sequenceService, examplesWorkspaceCloner, actionCollectionRepository,
|
newActionService, sequenceService, examplesWorkspaceCloner, actionCollectionRepository,
|
||||||
actionCollectionService, themeService, analyticsService, customJSLibService, datasourcePermission,
|
actionCollectionService, themeService, analyticsService, customJSLibService, datasourcePermission,
|
||||||
workspacePermission, applicationPermission, pagePermission, actionPermission, gson, transactionalOperator);
|
workspacePermission, applicationPermission, pagePermission, actionPermission, gson, transactionalOperator,
|
||||||
|
applicationSnapshotService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import com.appsmith.server.domains.Application;
|
||||||
import com.appsmith.server.domains.ApplicationPage;
|
import com.appsmith.server.domains.ApplicationPage;
|
||||||
import com.appsmith.server.domains.CustomJSLib;
|
import com.appsmith.server.domains.CustomJSLib;
|
||||||
import com.appsmith.server.domains.GitApplicationMetadata;
|
import com.appsmith.server.domains.GitApplicationMetadata;
|
||||||
import com.appsmith.server.domains.GitApplicationMetadata;
|
|
||||||
import com.appsmith.server.domains.Layout;
|
import com.appsmith.server.domains.Layout;
|
||||||
import com.appsmith.server.domains.NewAction;
|
import com.appsmith.server.domains.NewAction;
|
||||||
import com.appsmith.server.domains.NewPage;
|
import com.appsmith.server.domains.NewPage;
|
||||||
|
|
@ -844,6 +843,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
||||||
// The isPublic flag has a default value as false and this would be confusing to user
|
// The isPublic flag has a default value as false and this would be confusing to user
|
||||||
// when it is reset to false during importing where the application already is present in DB
|
// when it is reset to false during importing where the application already is present in DB
|
||||||
importedApplication.setIsPublic(null);
|
importedApplication.setIsPublic(null);
|
||||||
|
importedApplication.setPolicies(null);
|
||||||
copyNestedNonNullProperties(importedApplication, existingApplication);
|
copyNestedNonNullProperties(importedApplication, existingApplication);
|
||||||
// We are expecting the changes present in DB are committed to git directory
|
// We are expecting the changes present in DB are committed to git directory
|
||||||
// so that these won't be lost when we are pulling changes from remote and
|
// so that these won't be lost when we are pulling changes from remote and
|
||||||
|
|
@ -851,7 +851,16 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
||||||
// the changes from remote
|
// the changes from remote
|
||||||
// We are using the save instead of update as we are using @Encrypted
|
// We are using the save instead of update as we are using @Encrypted
|
||||||
// for GitAuth
|
// for GitAuth
|
||||||
return applicationService.findById(existingApplication.getGitApplicationMetadata().getDefaultApplicationId())
|
Mono<Application> parentApplicationMono;
|
||||||
|
if (existingApplication.getGitApplicationMetadata() != null) {
|
||||||
|
parentApplicationMono = applicationService.findById(
|
||||||
|
existingApplication.getGitApplicationMetadata().getDefaultApplicationId()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
parentApplicationMono = Mono.just(existingApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentApplicationMono
|
||||||
.flatMap(application1 -> {
|
.flatMap(application1 -> {
|
||||||
// Set the policies from the defaultApplication
|
// Set the policies from the defaultApplication
|
||||||
existingApplication.setPolicies(application1.getPolicies());
|
existingApplication.setPolicies(application1.getPolicies());
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ import com.appsmith.server.services.ActionCollectionService;
|
||||||
import com.appsmith.server.services.AnalyticsService;
|
import com.appsmith.server.services.AnalyticsService;
|
||||||
import com.appsmith.server.services.ApplicationPageService;
|
import com.appsmith.server.services.ApplicationPageService;
|
||||||
import com.appsmith.server.services.ApplicationService;
|
import com.appsmith.server.services.ApplicationService;
|
||||||
|
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||||
import com.appsmith.server.services.CustomJSLibService;
|
import com.appsmith.server.services.CustomJSLibService;
|
||||||
import com.appsmith.server.services.DatasourceService;
|
import com.appsmith.server.services.DatasourceService;
|
||||||
import com.appsmith.server.services.NewActionService;
|
import com.appsmith.server.services.NewActionService;
|
||||||
|
|
@ -78,7 +79,6 @@ import org.springframework.http.ContentDisposition;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.multipart.Part;
|
import org.springframework.http.codec.multipart.Part;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.transaction.reactive.TransactionalOperator;
|
import org.springframework.transaction.reactive.TransactionalOperator;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
@ -135,6 +135,7 @@ public class ImportExportApplicationServiceCEImplV2 implements ImportExportAppli
|
||||||
private final ActionPermission actionPermission;
|
private final ActionPermission actionPermission;
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
private final TransactionalOperator transactionalOperator;
|
private final TransactionalOperator transactionalOperator;
|
||||||
|
private final ApplicationSnapshotService applicationSnapshotService;
|
||||||
|
|
||||||
private static final Set<MediaType> ALLOWED_CONTENT_TYPES = Set.of(MediaType.APPLICATION_JSON);
|
private static final Set<MediaType> ALLOWED_CONTENT_TYPES = Set.of(MediaType.APPLICATION_JSON);
|
||||||
private static final String INVALID_JSON_FILE = "invalid json file";
|
private static final String INVALID_JSON_FILE = "invalid json file";
|
||||||
|
|
@ -887,6 +888,7 @@ public class ImportExportApplicationServiceCEImplV2 implements ImportExportAppli
|
||||||
// The isPublic flag has a default value as false and this would be confusing to user
|
// The isPublic flag has a default value as false and this would be confusing to user
|
||||||
// when it is reset to false during importing where the application already is present in DB
|
// when it is reset to false during importing where the application already is present in DB
|
||||||
importedApplication.setIsPublic(null);
|
importedApplication.setIsPublic(null);
|
||||||
|
importedApplication.setPolicies(null);
|
||||||
copyNestedNonNullProperties(importedApplication, existingApplication);
|
copyNestedNonNullProperties(importedApplication, existingApplication);
|
||||||
// We are expecting the changes present in DB are committed to git directory
|
// We are expecting the changes present in DB are committed to git directory
|
||||||
// so that these won't be lost when we are pulling changes from remote and
|
// so that these won't be lost when we are pulling changes from remote and
|
||||||
|
|
@ -894,7 +896,16 @@ public class ImportExportApplicationServiceCEImplV2 implements ImportExportAppli
|
||||||
// the changes from remote
|
// the changes from remote
|
||||||
// We are using the save instead of update as we are using @Encrypted
|
// We are using the save instead of update as we are using @Encrypted
|
||||||
// for GitAuth
|
// for GitAuth
|
||||||
return applicationService.findById(existingApplication.getGitApplicationMetadata().getDefaultApplicationId())
|
Mono<Application> parentApplicationMono;
|
||||||
|
if (existingApplication.getGitApplicationMetadata() != null) {
|
||||||
|
parentApplicationMono = applicationService.findById(
|
||||||
|
existingApplication.getGitApplicationMetadata().getDefaultApplicationId()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
parentApplicationMono = Mono.just(existingApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentApplicationMono
|
||||||
.flatMap(application1 -> {
|
.flatMap(application1 -> {
|
||||||
// Set the policies from the defaultApplication
|
// Set the policies from the defaultApplication
|
||||||
existingApplication.setPolicies(application1.getPolicies());
|
existingApplication.setPolicies(application1.getPolicies());
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.appsmith.server.helpers.RedisUtils;
|
||||||
import com.appsmith.server.exceptions.AppsmithErrorCode;
|
import com.appsmith.server.exceptions.AppsmithErrorCode;
|
||||||
import com.appsmith.server.services.ApplicationPageService;
|
import com.appsmith.server.services.ApplicationPageService;
|
||||||
import com.appsmith.server.services.ApplicationService;
|
import com.appsmith.server.services.ApplicationService;
|
||||||
|
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||||
import com.appsmith.server.services.ThemeService;
|
import com.appsmith.server.services.ThemeService;
|
||||||
import com.appsmith.server.services.UserDataService;
|
import com.appsmith.server.services.UserDataService;
|
||||||
import com.appsmith.server.solutions.ApplicationFetcher;
|
import com.appsmith.server.solutions.ApplicationFetcher;
|
||||||
|
|
@ -53,6 +54,9 @@ public class ApplicationControllerTest {
|
||||||
@MockBean
|
@MockBean
|
||||||
ImportExportApplicationService importExportApplicationService;
|
ImportExportApplicationService importExportApplicationService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
ApplicationSnapshotService applicationSnapshotService;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
ThemeService themeService;
|
ThemeService themeService;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
package com.appsmith.server.repositories;
|
||||||
|
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.security.test.context.support.WithUserDetails;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
public class ApplicationSnapshotRepositoryTest {
|
||||||
|
@Autowired
|
||||||
|
private ApplicationSnapshotRepository applicationSnapshotRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Gson gson;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails("api_user")
|
||||||
|
public void findWithoutData_WhenMatched_ReturnsMatchedDocumentWithoutData() {
|
||||||
|
String testAppId1 = UUID.randomUUID().toString(),
|
||||||
|
testAppId2 = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
ApplicationSnapshot snapshot1 = new ApplicationSnapshot();
|
||||||
|
snapshot1.setApplicationId(testAppId1);
|
||||||
|
snapshot1.setChunkOrder(1);
|
||||||
|
|
||||||
|
ApplicationSnapshot snapshot2 = new ApplicationSnapshot();
|
||||||
|
snapshot2.setData(new byte[10]);
|
||||||
|
snapshot2.setApplicationId(testAppId2);
|
||||||
|
snapshot2.setChunkOrder(1);
|
||||||
|
|
||||||
|
Mono<ApplicationSnapshot> snapshotMono = applicationSnapshotRepository.saveAll(List.of(snapshot1, snapshot2))
|
||||||
|
.then(applicationSnapshotRepository.findWithoutData(testAppId2));
|
||||||
|
|
||||||
|
StepVerifier.create(snapshotMono).assertNext(applicationSnapshot -> {
|
||||||
|
assertThat(applicationSnapshot.getApplicationId()).isEqualTo(testAppId2);
|
||||||
|
assertThat(applicationSnapshot.getData()).isNull();
|
||||||
|
assertThat(applicationSnapshot.getChunkOrder()).isEqualTo(1);
|
||||||
|
}).verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails("api_user")
|
||||||
|
public void findWithoutData_WhenMultipleChunksArePresent_ReturnsSingleOne() {
|
||||||
|
String testAppId1 = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
// create two snapshots with same application id and another one with different application id
|
||||||
|
ApplicationSnapshot snapshot1 = new ApplicationSnapshot();
|
||||||
|
snapshot1.setApplicationId(testAppId1);
|
||||||
|
snapshot1.setChunkOrder(1);
|
||||||
|
|
||||||
|
ApplicationSnapshot snapshot2 = new ApplicationSnapshot();
|
||||||
|
snapshot2.setApplicationId(testAppId1);
|
||||||
|
snapshot2.setChunkOrder(2);
|
||||||
|
|
||||||
|
Mono<ApplicationSnapshot> snapshotMono = applicationSnapshotRepository.saveAll(List.of(snapshot1, snapshot2))
|
||||||
|
.then(applicationSnapshotRepository.findWithoutData(testAppId1));
|
||||||
|
|
||||||
|
StepVerifier.create(snapshotMono).assertNext(applicationSnapshot -> {
|
||||||
|
assertThat(applicationSnapshot.getApplicationId()).isEqualTo(testAppId1);
|
||||||
|
assertThat(applicationSnapshot.getChunkOrder()).isEqualTo(1);
|
||||||
|
}).verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails("api_user")
|
||||||
|
public void deleteAllByApplicationId_WhenMatched_ReturnsMatchedDocumentWithoutData() {
|
||||||
|
String testAppId1 = UUID.randomUUID().toString(),
|
||||||
|
testAppId2 = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
// create two snapshots with same application id and another one with different application id
|
||||||
|
ApplicationSnapshot snapshot1 = new ApplicationSnapshot();
|
||||||
|
snapshot1.setApplicationId(testAppId1);
|
||||||
|
snapshot1.setChunkOrder(1);
|
||||||
|
|
||||||
|
ApplicationSnapshot snapshot2 = new ApplicationSnapshot();
|
||||||
|
snapshot2.setApplicationId(testAppId1);
|
||||||
|
snapshot2.setChunkOrder(2);
|
||||||
|
|
||||||
|
ApplicationSnapshot snapshot3 = new ApplicationSnapshot();
|
||||||
|
snapshot3.setApplicationId(testAppId2);
|
||||||
|
snapshot3.setChunkOrder(1);
|
||||||
|
|
||||||
|
Flux<ApplicationSnapshot> applicationSnapshots = applicationSnapshotRepository.saveAll(List.of(snapshot1, snapshot2, snapshot3))
|
||||||
|
.then(applicationSnapshotRepository.deleteAllByApplicationId(testAppId1))
|
||||||
|
.thenMany(applicationSnapshotRepository.findByApplicationId(testAppId1));
|
||||||
|
|
||||||
|
StepVerifier.create(applicationSnapshots)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
StepVerifier.create(applicationSnapshotRepository.findByApplicationId(testAppId2))
|
||||||
|
.assertNext(applicationSnapshot -> {
|
||||||
|
assertThat(applicationSnapshot.getApplicationId()).isEqualTo(testAppId2);
|
||||||
|
assertThat(applicationSnapshot.getChunkOrder()).isEqualTo(1);
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails("api_user")
|
||||||
|
public void findByApplicationId_WhenMatched_ReturnsMatchedDocumentWithoutData() {
|
||||||
|
String testAppId1 = UUID.randomUUID().toString(),
|
||||||
|
testAppId2 = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
// create two snapshots with same application id and another one with different application id
|
||||||
|
ApplicationSnapshot snapshot1 = new ApplicationSnapshot();
|
||||||
|
snapshot1.setApplicationId(testAppId1);
|
||||||
|
snapshot1.setChunkOrder(1);
|
||||||
|
|
||||||
|
ApplicationSnapshot snapshot2 = new ApplicationSnapshot();
|
||||||
|
snapshot2.setApplicationId(testAppId1);
|
||||||
|
snapshot2.setChunkOrder(2);
|
||||||
|
|
||||||
|
ApplicationSnapshot snapshot3 = new ApplicationSnapshot();
|
||||||
|
snapshot3.setApplicationId(testAppId2);
|
||||||
|
snapshot3.setChunkOrder(1);
|
||||||
|
|
||||||
|
Flux<ApplicationSnapshot> applicationSnapshots = applicationSnapshotRepository.saveAll(List.of(snapshot1, snapshot2, snapshot3))
|
||||||
|
.thenMany(applicationSnapshotRepository.findByApplicationId(testAppId1));
|
||||||
|
|
||||||
|
StepVerifier.create(applicationSnapshots)
|
||||||
|
.assertNext(applicationSnapshot -> {
|
||||||
|
assertThat(applicationSnapshot.getApplicationId()).isEqualTo(testAppId1);
|
||||||
|
})
|
||||||
|
.assertNext(applicationSnapshot -> {
|
||||||
|
assertThat(applicationSnapshot.getApplicationId()).isEqualTo(testAppId1);
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
package com.appsmith.server.services;
|
||||||
|
|
||||||
|
import com.appsmith.server.domains.Application;
|
||||||
|
import com.appsmith.server.domains.ApplicationMode;
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
|
import com.appsmith.server.domains.GitApplicationMetadata;
|
||||||
|
import com.appsmith.server.domains.Workspace;
|
||||||
|
import com.appsmith.server.dtos.ApplicationPagesDTO;
|
||||||
|
import com.appsmith.server.dtos.PageDTO;
|
||||||
|
import com.appsmith.server.repositories.ApplicationSnapshotRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.security.test.context.support.WithUserDetails;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
import reactor.util.function.Tuple2;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
public class ApplicationSnapshotServiceTest {
|
||||||
|
@Autowired
|
||||||
|
private ApplicationPageService applicationPageService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NewPageService newPageService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationSnapshotService applicationSnapshotService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WorkspaceService workspaceService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationSnapshotRepository applicationSnapshotRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails("api_user")
|
||||||
|
public void createApplicationSnapshot_WhenNoPreviousSnapshotExists_NewCreated() {
|
||||||
|
// create a new workspace
|
||||||
|
Workspace workspace = new Workspace();
|
||||||
|
workspace.setName("Test workspace " + UUID.randomUUID());
|
||||||
|
|
||||||
|
Mono<ApplicationSnapshot> snapshotMono = workspaceService.create(workspace)
|
||||||
|
.flatMap(createdWorkspace -> {
|
||||||
|
Application testApplication = new Application();
|
||||||
|
testApplication.setName("Test app for snapshot");
|
||||||
|
testApplication.setWorkspaceId(createdWorkspace.getId());
|
||||||
|
return applicationPageService.createApplication(testApplication);
|
||||||
|
})
|
||||||
|
.flatMap(application -> {
|
||||||
|
assert application.getId() != null;
|
||||||
|
return applicationSnapshotService.createApplicationSnapshot(application.getId(), "")
|
||||||
|
.thenReturn(application.getId());
|
||||||
|
})
|
||||||
|
.flatMap(applicationId -> applicationSnapshotService.getWithoutDataByApplicationId(applicationId, null));
|
||||||
|
|
||||||
|
StepVerifier.create(snapshotMono)
|
||||||
|
.assertNext(snapshot -> {
|
||||||
|
assertThat(snapshot.getApplicationId()).isNotNull();
|
||||||
|
assertThat(snapshot.getData()).isNull();
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails("api_user")
|
||||||
|
public void createApplicationSnapshot_WhenSnapshotExists_ExistingSnapshotUpdated() {
|
||||||
|
// create a new workspace
|
||||||
|
Workspace workspace = new Workspace();
|
||||||
|
workspace.setName("Test workspace " + UUID.randomUUID());
|
||||||
|
|
||||||
|
Mono<ApplicationSnapshot> snapshotMono = workspaceService.create(workspace)
|
||||||
|
.flatMap(createdWorkspace -> {
|
||||||
|
Application testApplication = new Application();
|
||||||
|
testApplication.setName("Test app for snapshot");
|
||||||
|
testApplication.setWorkspaceId(createdWorkspace.getId());
|
||||||
|
return applicationPageService.createApplication(testApplication);
|
||||||
|
})
|
||||||
|
.flatMap(application -> {
|
||||||
|
assert application.getId() != null;
|
||||||
|
// create snapshot twice
|
||||||
|
return applicationSnapshotService.createApplicationSnapshot(application.getId(), "")
|
||||||
|
.then(applicationSnapshotService.createApplicationSnapshot(application.getId(), ""))
|
||||||
|
.thenReturn(application.getId());
|
||||||
|
})
|
||||||
|
.flatMap(applicationId -> applicationSnapshotService.getWithoutDataByApplicationId(applicationId, null));
|
||||||
|
|
||||||
|
StepVerifier.create(snapshotMono)
|
||||||
|
.assertNext(snapshot -> {
|
||||||
|
assertThat(snapshot.getApplicationId()).isNotNull();
|
||||||
|
assertThat(snapshot.getData()).isNull();
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails("api_user")
|
||||||
|
public void createApplicationSnapshot_WhenGitBranchExists_SnapshotCreatedWithBranchedAppId() {
|
||||||
|
String uniqueString = UUID.randomUUID().toString();
|
||||||
|
String testDefaultAppId = "default-app-" + uniqueString;
|
||||||
|
String testBranchName = "hello/world";
|
||||||
|
// create a new workspace
|
||||||
|
Workspace workspace = new Workspace();
|
||||||
|
workspace.setName("Test workspace " + uniqueString);
|
||||||
|
|
||||||
|
Mono<Tuple2<ApplicationSnapshot, Application>> tuple2Mono = workspaceService.create(workspace)
|
||||||
|
.flatMap(createdWorkspace -> {
|
||||||
|
Application testApplication = new Application();
|
||||||
|
testApplication.setName("Test app for snapshot");
|
||||||
|
testApplication.setWorkspaceId(createdWorkspace.getId());
|
||||||
|
|
||||||
|
// this app will have default app id=testDefaultAppId and branch name=test branch name
|
||||||
|
GitApplicationMetadata gitApplicationMetadata = new GitApplicationMetadata();
|
||||||
|
gitApplicationMetadata.setDefaultApplicationId(testDefaultAppId);
|
||||||
|
gitApplicationMetadata.setBranchName(testBranchName);
|
||||||
|
testApplication.setGitApplicationMetadata(gitApplicationMetadata);
|
||||||
|
|
||||||
|
return applicationPageService.createApplication(testApplication);
|
||||||
|
})
|
||||||
|
.flatMap(application -> applicationSnapshotService.createApplicationSnapshot(testDefaultAppId, testBranchName)
|
||||||
|
.then(applicationSnapshotService.getWithoutDataByApplicationId(testDefaultAppId, testBranchName))
|
||||||
|
.zipWith(Mono.just(application)));
|
||||||
|
|
||||||
|
StepVerifier.create(tuple2Mono)
|
||||||
|
.assertNext(objects -> {
|
||||||
|
ApplicationSnapshot applicationSnapshot = objects.getT1();
|
||||||
|
Application application = objects.getT2();
|
||||||
|
assertThat(applicationSnapshot.getData()).isNull();
|
||||||
|
assertThat(applicationSnapshot.getApplicationId()).isEqualTo(application.getId());
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails("api_user")
|
||||||
|
public void createApplicationSnapshot_OlderSnapshotExists_OlderSnapshotsRemoved() {
|
||||||
|
String uniqueString = UUID.randomUUID().toString();
|
||||||
|
String testDefaultAppId = "default-app-" + uniqueString;
|
||||||
|
String testBranchName = null;
|
||||||
|
// create a new workspace
|
||||||
|
Workspace workspace = new Workspace();
|
||||||
|
workspace.setName("Test workspace " + uniqueString);
|
||||||
|
|
||||||
|
Flux<ApplicationSnapshot> applicationSnapshotFlux = workspaceService.create(workspace)
|
||||||
|
.flatMap(createdWorkspace -> {
|
||||||
|
Application testApplication = new Application();
|
||||||
|
testApplication.setName("Test app for snapshot");
|
||||||
|
testApplication.setWorkspaceId(createdWorkspace.getId());
|
||||||
|
return applicationPageService.createApplication(testApplication);
|
||||||
|
})
|
||||||
|
.flatMap(application -> {
|
||||||
|
ApplicationSnapshot applicationSnapshot = new ApplicationSnapshot();
|
||||||
|
applicationSnapshot.setApplicationId(application.getId());
|
||||||
|
applicationSnapshot.setChunkOrder(5);
|
||||||
|
applicationSnapshot.setData("Hello".getBytes(StandardCharsets.UTF_8));
|
||||||
|
return applicationSnapshotRepository.save(applicationSnapshot).thenReturn(application);
|
||||||
|
})
|
||||||
|
.flatMapMany(application ->
|
||||||
|
applicationSnapshotService.createApplicationSnapshot(application.getId(), null)
|
||||||
|
.thenMany(applicationSnapshotRepository.findByApplicationId(application.getId()))
|
||||||
|
);
|
||||||
|
|
||||||
|
StepVerifier.create(applicationSnapshotFlux)
|
||||||
|
.assertNext(applicationSnapshot -> {
|
||||||
|
assertThat(applicationSnapshot.getChunkOrder()).isEqualTo(1);
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails("api_user")
|
||||||
|
public void restoreSnapshot_WhenNewPagesAddedAfterSnapshotTaken_NewPagesRemovedAfterSnapshotIsRestored() {
|
||||||
|
String uniqueString = UUID.randomUUID().toString();
|
||||||
|
String testDefaultAppId = "default-app-" + uniqueString;
|
||||||
|
String testBranchName = "hello/world";
|
||||||
|
|
||||||
|
// create a new workspace
|
||||||
|
Workspace workspace = new Workspace();
|
||||||
|
workspace.setName("Test workspace " + uniqueString);
|
||||||
|
|
||||||
|
Mono<Application> applicationMono = workspaceService.create(workspace)
|
||||||
|
.flatMap(createdWorkspace -> {
|
||||||
|
Application testApplication = new Application();
|
||||||
|
testApplication.setName("App before snapshot");
|
||||||
|
return applicationPageService.createApplication(testApplication, workspace.getId());
|
||||||
|
}).cache();
|
||||||
|
|
||||||
|
Mono<ApplicationPagesDTO> pagesBeforeSnapshot = applicationMono
|
||||||
|
.flatMap(createdApp -> {
|
||||||
|
// add a page to the application
|
||||||
|
PageDTO pageDTO = new PageDTO();
|
||||||
|
pageDTO.setName("Home");
|
||||||
|
pageDTO.setApplicationId(createdApp.getId());
|
||||||
|
return applicationPageService.createPage(pageDTO)
|
||||||
|
.then(newPageService.findApplicationPages(createdApp.getId(), null, null, ApplicationMode.EDIT));
|
||||||
|
});
|
||||||
|
|
||||||
|
Mono<ApplicationPagesDTO> pagesAfterSnapshot = applicationMono.flatMap(application -> { // create a snapshot
|
||||||
|
return applicationSnapshotService.createApplicationSnapshot(application.getId(), null)
|
||||||
|
.thenReturn(application);
|
||||||
|
}).flatMap(application -> { // add a new page to the application
|
||||||
|
PageDTO pageDTO = new PageDTO();
|
||||||
|
pageDTO.setName("About");
|
||||||
|
pageDTO.setApplicationId(application.getId());
|
||||||
|
return applicationPageService.createPage(pageDTO)
|
||||||
|
.then(applicationSnapshotService.restoreSnapshot(application.getId(), null))
|
||||||
|
.then(newPageService.findApplicationPages(application.getId(), null, null, ApplicationMode.EDIT));
|
||||||
|
});
|
||||||
|
|
||||||
|
// not using Mono.zip because we want pagesBeforeSnapshot to finish first
|
||||||
|
Mono<Tuple2<ApplicationPagesDTO, ApplicationPagesDTO>> tuple2Mono = pagesBeforeSnapshot
|
||||||
|
.flatMap(applicationPagesDTO -> pagesAfterSnapshot.zipWith(Mono.just(applicationPagesDTO)));
|
||||||
|
|
||||||
|
StepVerifier.create(tuple2Mono)
|
||||||
|
.assertNext(objects -> {
|
||||||
|
ApplicationPagesDTO beforePages = objects.getT2();
|
||||||
|
ApplicationPagesDTO afterPages = objects.getT1();
|
||||||
|
assertThat(beforePages.getPages().size()).isEqualTo(afterPages.getPages().size());
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
package com.appsmith.server.services.ce;
|
||||||
|
|
||||||
|
import com.appsmith.server.acl.AclPermission;
|
||||||
|
import com.appsmith.server.constants.SerialiseApplicationObjective;
|
||||||
|
import com.appsmith.server.domains.Application;
|
||||||
|
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||||
|
import com.appsmith.server.domains.Layout;
|
||||||
|
import com.appsmith.server.domains.NewPage;
|
||||||
|
import com.appsmith.server.dtos.ApplicationJson;
|
||||||
|
import com.appsmith.server.dtos.PageDTO;
|
||||||
|
import com.appsmith.server.repositories.ApplicationSnapshotRepository;
|
||||||
|
import com.appsmith.server.services.ApplicationService;
|
||||||
|
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||||
|
import com.appsmith.server.solutions.ImportExportApplicationService;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import net.minidev.json.JSONObject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentMatcher;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static java.util.Arrays.copyOfRange;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
public class ApplicationSnapshotServiceUnitTest {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
ApplicationService applicationService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
ImportExportApplicationService importExportApplicationService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
ApplicationSnapshotRepository applicationSnapshotRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ApplicationSnapshotService applicationSnapshotService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Gson gson;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createApplicationSnapshot_WhenApplicationTooLarge_SnapshotCreatedSuccessfully() {
|
||||||
|
String defaultAppId = "default-app-id",
|
||||||
|
branchName = "develop",
|
||||||
|
branchedAppId = "branched-app-id";
|
||||||
|
|
||||||
|
// Create a large ApplicationJson object that exceeds the 15 MB size
|
||||||
|
JSONObject jsonObject = new JSONObject();
|
||||||
|
jsonObject.put("key", generateRandomString(16));
|
||||||
|
|
||||||
|
Layout layout = new Layout();
|
||||||
|
layout.setDsl(jsonObject);
|
||||||
|
|
||||||
|
PageDTO pageDTO = new PageDTO();
|
||||||
|
pageDTO.setLayouts(new ArrayList<>());
|
||||||
|
pageDTO.getLayouts().add(layout);
|
||||||
|
NewPage newPage = new NewPage();
|
||||||
|
newPage.setUnpublishedPage(pageDTO);
|
||||||
|
|
||||||
|
ApplicationJson applicationJson = new ApplicationJson();
|
||||||
|
applicationJson.setPageList(List.of(newPage));
|
||||||
|
|
||||||
|
Mockito.when(applicationService.findBranchedApplicationId(branchName, defaultAppId, AclPermission.MANAGE_APPLICATIONS))
|
||||||
|
.thenReturn(Mono.just(branchedAppId));
|
||||||
|
|
||||||
|
Mockito.when(importExportApplicationService.exportApplicationById(branchedAppId, SerialiseApplicationObjective.VERSION_CONTROL))
|
||||||
|
.thenReturn(Mono.just(applicationJson));
|
||||||
|
|
||||||
|
Mockito.when(applicationSnapshotRepository.deleteAllByApplicationId(branchedAppId))
|
||||||
|
.thenReturn(Mono.just("").then());
|
||||||
|
|
||||||
|
// we're expecting to receive two application snapshots, create a matcher to check the size
|
||||||
|
ArgumentMatcher<List<ApplicationSnapshot>> snapshotListHasTwoSnapshot = snapshotList -> snapshotList.size() == 2;
|
||||||
|
|
||||||
|
Mockito.when(applicationSnapshotRepository.saveAll(argThat(snapshotListHasTwoSnapshot)))
|
||||||
|
.thenReturn(Flux.just(new ApplicationSnapshot(), new ApplicationSnapshot()));
|
||||||
|
|
||||||
|
StepVerifier.create(applicationSnapshotService.createApplicationSnapshot(defaultAppId, branchName))
|
||||||
|
.assertNext(aBoolean -> {
|
||||||
|
assertThat(aBoolean).isTrue();
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void restoreSnapshot_WhenSnapshotHasMultipleChunks_RestoredSuccessfully() {
|
||||||
|
String defaultAppId = "default-app-id",
|
||||||
|
branchedAppId = "branched-app-id",
|
||||||
|
workspaceId = "workspace-id",
|
||||||
|
branch = "development";
|
||||||
|
|
||||||
|
Application application = new Application();
|
||||||
|
application.setName("Snapshot test");
|
||||||
|
application.setWorkspaceId(workspaceId);
|
||||||
|
application.setId(branchedAppId);
|
||||||
|
|
||||||
|
Mockito.when(applicationService.findByBranchNameAndDefaultApplicationId(branch, defaultAppId, AclPermission.MANAGE_APPLICATIONS))
|
||||||
|
.thenReturn(Mono.just(application));
|
||||||
|
|
||||||
|
ApplicationJson applicationJson = new ApplicationJson();
|
||||||
|
applicationJson.setExportedApplication(application);
|
||||||
|
|
||||||
|
String jsonString = gson.toJson(applicationJson);
|
||||||
|
byte[] jsonStringBytes = jsonString.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
int chunkSize = jsonStringBytes.length / 3;
|
||||||
|
|
||||||
|
List<ApplicationSnapshot> snapshots = List.of(
|
||||||
|
createSnapshot(branchedAppId, copyOfRange(jsonStringBytes, chunkSize*2, jsonStringBytes.length), 3),
|
||||||
|
createSnapshot(branchedAppId, copyOfRange(jsonStringBytes, 0, chunkSize), 1),
|
||||||
|
createSnapshot(branchedAppId, copyOfRange(jsonStringBytes, chunkSize, chunkSize*2), 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
Mockito.when(applicationSnapshotRepository.findByApplicationId(branchedAppId))
|
||||||
|
.thenReturn(Flux.fromIterable(snapshots));
|
||||||
|
|
||||||
|
// matcher to check that ApplicationJson created from chunks matches the original one
|
||||||
|
ArgumentMatcher<ApplicationJson> matchApplicationJson;
|
||||||
|
matchApplicationJson = applicationJson1 -> applicationJson1.getExportedApplication().getName().equals(application.getName());
|
||||||
|
Mockito.when(importExportApplicationService.importApplicationInWorkspace(eq(application.getWorkspaceId()), argThat(matchApplicationJson), eq(branchedAppId), eq(branch)))
|
||||||
|
.thenReturn(Mono.just(application));
|
||||||
|
|
||||||
|
StepVerifier.create(applicationSnapshotService.restoreSnapshot(defaultAppId, branch))
|
||||||
|
.assertNext(application1 -> {
|
||||||
|
assertThat(application1.getName()).isEqualTo(application.getName());
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApplicationSnapshot createSnapshot(String applicationId, byte [] data, int chunkOrder) {
|
||||||
|
ApplicationSnapshot applicationSnapshot = new ApplicationSnapshot();
|
||||||
|
applicationSnapshot.setApplicationId(applicationId);
|
||||||
|
applicationSnapshot.setData(data);
|
||||||
|
applicationSnapshot.setChunkOrder(chunkOrder);
|
||||||
|
return applicationSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateRandomString(int targetStringSizeInMB) {
|
||||||
|
int targetSizeInBytes = targetStringSizeInMB * 1024 * 1024;
|
||||||
|
int leftLimit = 48; // numeral '0'
|
||||||
|
int rightLimit = 122; // letter 'z'
|
||||||
|
Random random = new Random();
|
||||||
|
|
||||||
|
String generatedString = random.ints(leftLimit, rightLimit + 1)
|
||||||
|
.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
|
||||||
|
.limit(targetSizeInBytes)
|
||||||
|
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
return generatedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3642,4 +3642,72 @@ public class ImportExportApplicationServiceTests {
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails(value = "api_user")
|
||||||
|
public void importApplication_existingApplication_ApplicationReplacedWithImportedOne() {
|
||||||
|
String randomUUID = UUID.randomUUID().toString();
|
||||||
|
Mono<ApplicationJson> applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json");
|
||||||
|
|
||||||
|
// Create the initial application
|
||||||
|
Application application = new Application();
|
||||||
|
application.setName("Application_" + randomUUID);
|
||||||
|
application.setWorkspaceId(workspaceId);
|
||||||
|
|
||||||
|
Mono<Tuple4<Application, List<NewPage>, List<NewAction>, List<ActionCollection>>> importedApplication = applicationPageService.createApplication(application).flatMap(createdApp -> {
|
||||||
|
PageDTO pageDTO = new PageDTO();
|
||||||
|
pageDTO.setApplicationId(application.getId());
|
||||||
|
pageDTO.setName("Home Page");
|
||||||
|
return applicationPageService.createPage(pageDTO).thenReturn(createdApp);
|
||||||
|
})
|
||||||
|
.zipWith(applicationJson)
|
||||||
|
.flatMap(objects -> {
|
||||||
|
return importExportApplicationService.importApplicationInWorkspace(workspaceId, objects.getT2(), objects.getT1().getId(), null)
|
||||||
|
.zipWith(Mono.just(objects.getT1()));
|
||||||
|
}).flatMap(objects -> {
|
||||||
|
Application newApp = objects.getT1();
|
||||||
|
Application oldApp = objects.getT2();
|
||||||
|
// after import, application id should not change
|
||||||
|
assert Objects.equals(newApp.getId(), oldApp.getId());
|
||||||
|
|
||||||
|
Mono<List<NewPage>> pageList = newPageService.findNewPagesByApplicationId(newApp.getId(), MANAGE_PAGES).collectList();
|
||||||
|
Mono<List<NewAction>> actionList = newActionService.findAllByApplicationIdAndViewMode(newApp.getId(), false, MANAGE_ACTIONS, null).collectList();
|
||||||
|
Mono<List<ActionCollection>> actionCollectionList = actionCollectionService.findAllByApplicationIdAndViewMode(newApp.getId(), false, MANAGE_ACTIONS, null).collectList();
|
||||||
|
return Mono.zip(Mono.just(newApp), pageList, actionList, actionCollectionList);
|
||||||
|
});
|
||||||
|
|
||||||
|
StepVerifier
|
||||||
|
.create(importedApplication)
|
||||||
|
.assertNext(tuple -> {
|
||||||
|
List<NewPage> pageList = tuple.getT2();
|
||||||
|
List<NewAction> actionList = tuple.getT3();
|
||||||
|
List<ActionCollection> actionCollectionList = tuple.getT4();
|
||||||
|
|
||||||
|
assertThat(pageList.size()).isEqualTo(2);
|
||||||
|
assertThat(actionList.size()).isEqualTo(3);
|
||||||
|
|
||||||
|
List<String> pageNames = pageList.stream()
|
||||||
|
.map(p -> p.getUnpublishedPage().getName())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<String> actionNames = actionList.stream()
|
||||||
|
.map(p -> p.getUnpublishedAction().getName())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<String> actionCollectionNames = actionCollectionList.stream()
|
||||||
|
.map(p -> p.getUnpublishedCollection().getName())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Verify the pages after importing the application
|
||||||
|
assertThat(pageNames).contains("Page1", "Page2");
|
||||||
|
|
||||||
|
// Verify the actions after importing the application
|
||||||
|
assertThat(actionNames).contains("api_wo_auth", "get_users", "run");
|
||||||
|
|
||||||
|
// Verify the actionCollections after importing the application
|
||||||
|
assertThat(actionCollectionNames).contains("JSObject1", "JSObject2");
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3732,4 +3732,72 @@ public class ImportExportApplicationServiceV2Tests {
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithUserDetails(value = "api_user")
|
||||||
|
public void importApplication_existingApplication_ApplicationReplacedWithImportedOne() {
|
||||||
|
String randomUUID = UUID.randomUUID().toString();
|
||||||
|
Mono<ApplicationJson> applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json");
|
||||||
|
|
||||||
|
// Create the initial application
|
||||||
|
Application application = new Application();
|
||||||
|
application.setName("Application_" + randomUUID);
|
||||||
|
application.setWorkspaceId(workspaceId);
|
||||||
|
|
||||||
|
Mono<Tuple4<Application, List<NewPage>, List<NewAction>, List<ActionCollection>>> importedApplication = applicationPageService.createApplication(application).flatMap(createdApp -> {
|
||||||
|
PageDTO pageDTO = new PageDTO();
|
||||||
|
pageDTO.setApplicationId(application.getId());
|
||||||
|
pageDTO.setName("Home Page");
|
||||||
|
return applicationPageService.createPage(pageDTO).thenReturn(createdApp);
|
||||||
|
})
|
||||||
|
.zipWith(applicationJson)
|
||||||
|
.flatMap(objects -> {
|
||||||
|
return importExportApplicationService.importApplicationInWorkspace(workspaceId, objects.getT2(), objects.getT1().getId(), null)
|
||||||
|
.zipWith(Mono.just(objects.getT1()));
|
||||||
|
}).flatMap(objects -> {
|
||||||
|
Application newApp = objects.getT1();
|
||||||
|
Application oldApp = objects.getT2();
|
||||||
|
// after import, application id should not change
|
||||||
|
assert Objects.equals(newApp.getId(), oldApp.getId());
|
||||||
|
|
||||||
|
Mono<List<NewPage>> pageList = newPageService.findNewPagesByApplicationId(newApp.getId(), MANAGE_PAGES).collectList();
|
||||||
|
Mono<List<NewAction>> actionList = newActionService.findAllByApplicationIdAndViewMode(newApp.getId(), false, MANAGE_ACTIONS, null).collectList();
|
||||||
|
Mono<List<ActionCollection>> actionCollectionList = actionCollectionService.findAllByApplicationIdAndViewMode(newApp.getId(), false, MANAGE_ACTIONS, null).collectList();
|
||||||
|
return Mono.zip(Mono.just(newApp), pageList, actionList, actionCollectionList);
|
||||||
|
});
|
||||||
|
|
||||||
|
StepVerifier
|
||||||
|
.create(importedApplication)
|
||||||
|
.assertNext(tuple -> {
|
||||||
|
List<NewPage> pageList = tuple.getT2();
|
||||||
|
List<NewAction> actionList = tuple.getT3();
|
||||||
|
List<ActionCollection> actionCollectionList = tuple.getT4();
|
||||||
|
|
||||||
|
assertThat(pageList.size()).isEqualTo(2);
|
||||||
|
assertThat(actionList.size()).isEqualTo(3);
|
||||||
|
|
||||||
|
List<String> pageNames = pageList.stream()
|
||||||
|
.map(p -> p.getUnpublishedPage().getName())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<String> actionNames = actionList.stream()
|
||||||
|
.map(p -> p.getUnpublishedAction().getName())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<String> actionCollectionNames = actionCollectionList.stream()
|
||||||
|
.map(p -> p.getUnpublishedCollection().getName())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Verify the pages after importing the application
|
||||||
|
assertThat(pageNames).contains("Page1", "Page2");
|
||||||
|
|
||||||
|
// Verify the actions after importing the application
|
||||||
|
assertThat(actionNames).contains("api_wo_auth", "get_users", "run");
|
||||||
|
|
||||||
|
// Verify the actionCollections after importing the application
|
||||||
|
assertThat(actionCollectionNames).contains("JSObject1", "JSObject2");
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user