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.services.ApplicationPageService;
|
||||
import com.appsmith.server.services.ApplicationService;
|
||||
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||
import com.appsmith.server.services.ThemeService;
|
||||
import com.appsmith.server.solutions.ApplicationFetcher;
|
||||
import com.appsmith.server.solutions.ApplicationForkingService;
|
||||
|
|
@ -20,10 +21,11 @@ public class ApplicationController extends ApplicationControllerCE {
|
|||
ApplicationFetcher applicationFetcher,
|
||||
ApplicationForkingService applicationForkingService,
|
||||
ImportExportApplicationService importExportApplicationService,
|
||||
ThemeService themeService) {
|
||||
ThemeService themeService,
|
||||
ApplicationSnapshotService applicationSnapshotService) {
|
||||
|
||||
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.Url;
|
||||
import com.appsmith.server.domains.Application;
|
||||
import com.appsmith.server.domains.ApplicationSnapshot;
|
||||
import com.appsmith.server.domains.GitAuth;
|
||||
import com.appsmith.server.domains.Theme;
|
||||
import com.appsmith.server.dtos.ApplicationAccessDTO;
|
||||
import com.appsmith.server.dtos.ApplicationImportDTO;
|
||||
import com.appsmith.server.dtos.ApplicationPagesDTO;
|
||||
import com.appsmith.server.dtos.GitAuthDTO;
|
||||
import com.appsmith.server.dtos.ReleaseItemsDTO;
|
||||
import com.appsmith.server.dtos.ResponseDTO;
|
||||
import com.appsmith.server.dtos.UserHomepageDTO;
|
||||
import com.appsmith.server.dtos.ReleaseItemsDTO;
|
||||
import com.appsmith.server.exceptions.AppsmithError;
|
||||
import com.appsmith.server.exceptions.AppsmithException;
|
||||
import com.appsmith.server.services.ApplicationPageService;
|
||||
import com.appsmith.server.services.ApplicationService;
|
||||
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||
import com.appsmith.server.services.ThemeService;
|
||||
import com.appsmith.server.solutions.ApplicationFetcher;
|
||||
import com.appsmith.server.solutions.ApplicationForkingService;
|
||||
import com.appsmith.server.solutions.ImportExportApplicationService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
|
@ -44,7 +47,6 @@ import org.springframework.web.bind.annotation.ResponseStatus;
|
|||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
|
|
@ -56,6 +58,7 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
|
|||
private final ApplicationForkingService applicationForkingService;
|
||||
private final ImportExportApplicationService importExportApplicationService;
|
||||
private final ThemeService themeService;
|
||||
private final ApplicationSnapshotService applicationSnapshotService;
|
||||
|
||||
@Autowired
|
||||
public ApplicationControllerCE(
|
||||
|
|
@ -63,13 +66,16 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
|
|||
ApplicationPageService applicationPageService,
|
||||
ApplicationFetcher applicationFetcher,
|
||||
ApplicationForkingService applicationForkingService,
|
||||
ImportExportApplicationService importExportApplicationService, ThemeService themeService) {
|
||||
ImportExportApplicationService importExportApplicationService,
|
||||
ThemeService themeService,
|
||||
ApplicationSnapshotService applicationSnapshotService) {
|
||||
super(service);
|
||||
this.applicationPageService = applicationPageService;
|
||||
this.applicationFetcher = applicationFetcher;
|
||||
this.applicationForkingService = applicationForkingService;
|
||||
this.importExportApplicationService = importExportApplicationService;
|
||||
this.themeService = themeService;
|
||||
this.applicationSnapshotService = applicationSnapshotService;
|
||||
}
|
||||
|
||||
@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)
|
||||
public Mono<ResponseDTO<ApplicationImportDTO>> importApplicationFromFile(@RequestPart("file") Mono<Part> fileMono,
|
||||
@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.ApplicationPageService;
|
||||
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.NewActionService;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import com.appsmith.server.services.ActionCollectionService;
|
|||
import com.appsmith.server.services.AnalyticsService;
|
||||
import com.appsmith.server.services.ApplicationPageService;
|
||||
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.NewActionService;
|
||||
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.ThemeService;
|
||||
import com.appsmith.server.services.WorkspaceService;
|
||||
import com.appsmith.server.services.CustomJSLibService;
|
||||
import com.appsmith.server.solutions.ce.ImportExportApplicationServiceCEImplV2;
|
||||
import com.google.gson.Gson;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.reactive.TransactionalOperator;
|
||||
|
||||
@Slf4j
|
||||
|
|
@ -54,12 +54,14 @@ public class ImportExportApplicationServiceImplV2 extends ImportExportApplicatio
|
|||
PagePermission pagePermission,
|
||||
ActionPermission actionPermission,
|
||||
Gson gson,
|
||||
TransactionalOperator transactionalOperator) {
|
||||
TransactionalOperator transactionalOperator,
|
||||
ApplicationSnapshotService applicationSnapshotService) {
|
||||
|
||||
super(datasourceService, sessionUserService, newActionRepository, datasourceRepository, pluginRepository,
|
||||
workspaceService, applicationService, newPageService, applicationPageService, newPageRepository,
|
||||
newActionService, sequenceService, examplesWorkspaceCloner, actionCollectionRepository,
|
||||
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.CustomJSLib;
|
||||
import com.appsmith.server.domains.GitApplicationMetadata;
|
||||
import com.appsmith.server.domains.GitApplicationMetadata;
|
||||
import com.appsmith.server.domains.Layout;
|
||||
import com.appsmith.server.domains.NewAction;
|
||||
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
|
||||
// when it is reset to false during importing where the application already is present in DB
|
||||
importedApplication.setIsPublic(null);
|
||||
importedApplication.setPolicies(null);
|
||||
copyNestedNonNullProperties(importedApplication, existingApplication);
|
||||
// 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
|
||||
|
|
@ -851,7 +851,16 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
// the changes from remote
|
||||
// We are using the save instead of update as we are using @Encrypted
|
||||
// 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 -> {
|
||||
// Set the policies from the defaultApplication
|
||||
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.ApplicationPageService;
|
||||
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.NewActionService;
|
||||
|
|
@ -78,7 +79,6 @@ import org.springframework.http.ContentDisposition;
|
|||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.reactive.TransactionalOperator;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
|
@ -135,6 +135,7 @@ public class ImportExportApplicationServiceCEImplV2 implements ImportExportAppli
|
|||
private final ActionPermission actionPermission;
|
||||
private final Gson gson;
|
||||
private final TransactionalOperator transactionalOperator;
|
||||
private final ApplicationSnapshotService applicationSnapshotService;
|
||||
|
||||
private static final Set<MediaType> ALLOWED_CONTENT_TYPES = Set.of(MediaType.APPLICATION_JSON);
|
||||
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
|
||||
// when it is reset to false during importing where the application already is present in DB
|
||||
importedApplication.setIsPublic(null);
|
||||
importedApplication.setPolicies(null);
|
||||
copyNestedNonNullProperties(importedApplication, existingApplication);
|
||||
// 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
|
||||
|
|
@ -894,7 +896,16 @@ public class ImportExportApplicationServiceCEImplV2 implements ImportExportAppli
|
|||
// the changes from remote
|
||||
// We are using the save instead of update as we are using @Encrypted
|
||||
// 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 -> {
|
||||
// Set the policies from the defaultApplication
|
||||
existingApplication.setPolicies(application1.getPolicies());
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.appsmith.server.helpers.RedisUtils;
|
|||
import com.appsmith.server.exceptions.AppsmithErrorCode;
|
||||
import com.appsmith.server.services.ApplicationPageService;
|
||||
import com.appsmith.server.services.ApplicationService;
|
||||
import com.appsmith.server.services.ApplicationSnapshotService;
|
||||
import com.appsmith.server.services.ThemeService;
|
||||
import com.appsmith.server.services.UserDataService;
|
||||
import com.appsmith.server.solutions.ApplicationFetcher;
|
||||
|
|
@ -53,6 +54,9 @@ public class ApplicationControllerTest {
|
|||
@MockBean
|
||||
ImportExportApplicationService importExportApplicationService;
|
||||
|
||||
@MockBean
|
||||
ApplicationSnapshotService applicationSnapshotService;
|
||||
|
||||
@MockBean
|
||||
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();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@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