diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DecryptedSensitiveFields.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DecryptedSensitiveFields.java new file mode 100644 index 0000000000..6b6a64bb3f --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DecryptedSensitiveFields.java @@ -0,0 +1,39 @@ +package com.appsmith.external.models; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** + * This class hold sensitive information, and fields that have a `@JsonIgnore` on them, so that such information + * can be serialized when an application is exported. + */ +@ToString +@Getter +@Setter +@NoArgsConstructor +public class DecryptedSensitiveFields { + + String password; + + String token; + + String refreshToken; + + Object tokenResponse; + + String authType; + + DBAuth dbAuth; + + BasicAuth basicAuth; + + OAuth2 openAuth2; + + public DecryptedSensitiveFields(AuthenticationResponse authResponse) { + this.token = authResponse.getToken(); + this.refreshToken = authResponse.getRefreshToken(); + this.tokenResponse = authResponse.getTokenResponse(); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java index 73484fd42c..3b044a72c4 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java @@ -58,6 +58,7 @@ public class FieldName { public static String ANONYMOUS_USER = "anonymousUser"; public static String USERNAMES = "usernames"; public static String ACTION = "action"; + public static String ACTIONS = "actions"; public static String ASSET = "asset"; public static String APPLICATION = "application"; public static String COMMENT = "comment"; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java index b036eed859..c8e035b655 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java @@ -2,6 +2,7 @@ package com.appsmith.server.controllers; import com.appsmith.server.constants.Url; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationJson; import com.appsmith.server.dtos.ApplicationAccessDTO; import com.appsmith.server.dtos.ResponseDTO; import com.appsmith.server.dtos.UserHomepageDTO; @@ -11,9 +12,15 @@ import com.appsmith.server.services.ApplicationPageService; import com.appsmith.server.services.ApplicationService; import com.appsmith.server.solutions.ApplicationFetcher; import com.appsmith.server.solutions.ApplicationForkingService; +import com.appsmith.server.solutions.ImportExportApplicationService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.codec.multipart.Part; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -22,12 +29,14 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.validation.Valid; +import java.nio.charset.StandardCharsets; @RestController @RequestMapping(Url.APPLICATION_URL) @@ -37,17 +46,20 @@ public class ApplicationController extends BaseController new ResponseDTO<>(HttpStatus.OK.value(), application, null)); } + @GetMapping("/export/{id}") + public Mono> getApplicationFile(@PathVariable String id) { + log.debug("Going to export application with id: {}", id); + + return importExportApplicationService.exportApplicationById(id) + .map(fetchedResource -> { + + HttpHeaders responseHeaders = new HttpHeaders(); + ContentDisposition contentDisposition = ContentDisposition + .builder("attachment") + .filename("application-file.json", StandardCharsets.UTF_8) + .build(); + responseHeaders.setContentDisposition(contentDisposition); + responseHeaders.setContentType(MediaType.APPLICATION_JSON); + + return new ResponseEntity(fetchedResource, responseHeaders, HttpStatus.OK); + }); + } + + @PostMapping(value = "/import/{orgId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public Mono> importApplicationFromFile( + @RequestPart("file") Mono fileMono, @PathVariable String orgId) { + log.debug("Going to import application in organization with id: {}", orgId); + return fileMono + .flatMap(file -> importExportApplicationService.extractFileAndSaveApplication(orgId, file)) + .map(fetchedResource -> new ResponseDTO<>(HttpStatus.OK.value(), fetchedResource, null)); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationJson.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationJson.java new file mode 100644 index 0000000000..4c7942a1ec --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationJson.java @@ -0,0 +1,38 @@ +package com.appsmith.server.domains; + +import com.appsmith.external.models.DecryptedSensitiveFields; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A DTO class to hold complete information about an application, which will then be serialized to a file so as to + * export that application into a file. + */ +@Getter +@Setter +public class ApplicationJson { + + Application exportedApplication; + + List datasourceList; + + List pageList; + + String publishedDefaultPageName; + + String unpublishedDefaultPageName; + + List actionList; + + Map decryptedFields; + + /** + * Mapping mongoEscapedWidgets with layoutId + */ + Map> publishedLayoutmongoEscapedWidgets; + Map> unpublishedLayoutmongoEscapedWidgets; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepository.java index efe7fa06c9..c1e46d8b85 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepository.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepository.java @@ -12,7 +12,7 @@ import java.util.Set; public interface CustomDatasourceRepository extends AppsmithRepository { Flux findAllByOrganizationId(String organizationId, AclPermission permission); - Mono findByName(String name, AclPermission aclPermission); + Mono findByNameAndOrganizationId(String name, String organizationId, AclPermission aclPermission); Mono findById(String id, AclPermission aclPermission); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepositoryImpl.java index a7774860d2..e6b416649e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepositoryImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepositoryImpl.java @@ -33,9 +33,10 @@ public class CustomDatasourceRepositoryImpl extends BaseAppsmithRepositoryImpl findByName(String name, AclPermission aclPermission) { + public Mono findByNameAndOrganizationId(String name, String organizationId, AclPermission aclPermission) { Criteria nameCriteria = where(fieldName(QDatasource.datasource.name)).is(name); - return queryOne(List.of(nameCriteria), aclPermission); + Criteria orgIdCriteria = where(fieldName(QDatasource.datasource.organizationId)).is(organizationId); + return queryOne(List.of(nameCriteria, orgIdCriteria), aclPermission); } @Override diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java index 5c66bc4bf2..2b353ccd0f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java @@ -34,4 +34,6 @@ public interface ApplicationPageService { Mono deleteUnpublishedPage(String id); Mono publish(String applicationId); + + void generateAndSetPagePolicies(Application application, PageDTO page); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java index 11e074e079..0cfa0195da 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java @@ -277,7 +277,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { }); } - private void generateAndSetPagePolicies(Application application, PageDTO page) { + public void generateAndSetPagePolicies(Application application, PageDTO page) { Set documentPolicies = policyGenerator.getAllChildPolicies(application.getPolicies(), Application.class, Page.class); page.setPolicies(documentPolicies); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java index a497b0b9ed..f0d7369c31 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java @@ -13,7 +13,7 @@ public interface DatasourceService extends CrudService { Mono testDatasource(Datasource datasource); - Mono findByName(String name, AclPermission permission); + Mono findByNameAndOrganizationId(String name, String organizationId, AclPermission permission); Mono findById(String id, AclPermission aclPermission); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java index b6d0e02e4e..9d4cd4a44e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java @@ -343,8 +343,8 @@ public class DatasourceServiceImpl extends BaseService findByName(String name, AclPermission permission) { - return repository.findByName(name, permission); + public Mono findByNameAndOrganizationId(String name, String organizationId, AclPermission permission) { + return repository.findByNameAndOrganizationId(name, organizationId, permission); } @Override diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java index 221541a3ff..aaaa2f72db 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java @@ -459,7 +459,7 @@ public class ExamplesOrganizationCloner { }); } - private void makePristine(BaseDomain domain) { + public void makePristine(BaseDomain domain) { // Set the ID to null for this domain object so that it is saved a new document in the database (as opposed to // updating an existing document). If it contains any policies, they are also reset. domain.setId(null); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationService.java new file mode 100644 index 0000000000..4a6b7e9dac --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationService.java @@ -0,0 +1,640 @@ +package com.appsmith.server.solutions; + +import com.appsmith.external.helpers.BeanCopyUtils; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.AuthenticationResponse; +import com.appsmith.external.models.BaseDomain; +import com.appsmith.external.models.BasicAuth; +import com.appsmith.external.models.DBAuth; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DecryptedSensitiveFields; +import com.appsmith.external.models.OAuth2; +import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationJson; +import com.appsmith.server.domains.ApplicationPage; +import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.domains.PluginType; +import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; +import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.repositories.DatasourceRepository; +import com.appsmith.server.repositories.NewActionRepository; +import com.appsmith.server.repositories.NewPageRepository; +import com.appsmith.server.repositories.PluginRepository; +import com.appsmith.server.services.ApplicationPageService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.NewActionService; +import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.OrganizationService; +import com.appsmith.server.services.SequenceService; +import com.appsmith.server.services.SessionUserService; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.bson.types.ObjectId; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.MediaType; +import org.springframework.http.codec.multipart.Part; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.lang.reflect.Type; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ImportExportApplicationService { + private final DatasourceService datasourceService; + private final SessionUserService sessionUserService; + private final NewActionRepository newActionRepository; + private final DatasourceRepository datasourceRepository; + private final PluginRepository pluginRepository; + private final OrganizationService organizationService; + private final ApplicationService applicationService; + private final NewPageService newPageService; + private final ApplicationPageService applicationPageService; + private final NewPageRepository newPageRepository; + private final NewActionService newActionService; + private final SequenceService sequenceService; + private final ExamplesOrganizationCloner examplesOrganizationCloner; + + private static final Set ALLOWED_CONTENT_TYPES = Set.of(MediaType.APPLICATION_JSON); + public final String INVALID_JSON_FILE = "invalid json file"; + private enum PublishType { + UNPUBLISH, PUBLISH + } + + public Mono exportApplicationById(String applicationId) { + ApplicationJson applicationJson = new ApplicationJson(); + Map pluginMap = new HashMap<>(); + Map datasourceIdToNameMap = new HashMap<>(); + Map pageIdToNameMap = new HashMap<>(); + + if (applicationId == null || applicationId.isEmpty()) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.APPLICATION_ID)); + } + + return pluginRepository + .findAll() + .map(plugin -> { + pluginMap.put(plugin.getId(), plugin.getPackageName()); + return plugin; + }) + .then(applicationService.findById(applicationId, AclPermission.MANAGE_APPLICATIONS)) + .switchIfEmpty(Mono.error(new AppsmithException( + AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId)) + ) + .flatMap(application -> { + ApplicationPage unpublishedDefaultPage = application.getPages() + .stream() + .filter(ApplicationPage::getIsDefault) + .findFirst() + .orElse(null); + + if (unpublishedDefaultPage == null) { + return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.DEFAULT_PAGE_NAME)); + } else { + applicationJson.setUnpublishedDefaultPageName(unpublishedDefaultPage.getId()); + } + + if (application.getPublishedPages() != null) { + ApplicationPage publishedDefaultPage = application.getPublishedPages() + .stream() + .filter(ApplicationPage::getIsDefault) + .findFirst() + .orElse(null); + + if(publishedDefaultPage != null) { + applicationJson.setPublishedDefaultPageName(publishedDefaultPage.getId()); + } + } + + final String organizationId = application.getOrganizationId(); + application.setOrganizationId(null); + application.setPages(null); + examplesOrganizationCloner.makePristine(application); + applicationJson.setExportedApplication(application); + return newPageRepository.findByApplicationId(applicationId, AclPermission.MANAGE_PAGES) + .collectList() + .flatMap(newPageList -> { + Map> publishedMongoEscapedWidgetsNames = new HashMap<>(); + Map> unpublishedMongoEscapedWidgetsNames = new HashMap<>(); + newPageList.forEach(newPage -> { + + if (newPage.getUnpublishedPage() != null) { + pageIdToNameMap.put( + newPage.getId() + PublishType.UNPUBLISH, newPage.getUnpublishedPage().getName() + ); + PageDTO unpublishedPageDTO = newPage.getUnpublishedPage(); + if (StringUtils.equals( + applicationJson.getUnpublishedDefaultPageName(), newPage.getId()) + ) { + applicationJson.setUnpublishedDefaultPageName(unpublishedPageDTO.getName()); + } + if (unpublishedPageDTO.getLayouts() != null) { + + unpublishedPageDTO.getLayouts().forEach(layout -> + unpublishedMongoEscapedWidgetsNames + .put(layout.getId(), layout.getMongoEscapedWidgetNames()) + ); + } + } + + if (newPage.getPublishedPage() != null) { + pageIdToNameMap.put( + newPage.getId() + PublishType.PUBLISH, newPage.getPublishedPage().getName() + ); + PageDTO publishedPageDTO = newPage.getPublishedPage(); + if (applicationJson.getPublishedDefaultPageName() != null && + StringUtils.equals( + applicationJson.getPublishedDefaultPageName(), newPage.getId() + ) + ) { + applicationJson.setPublishedDefaultPageName(publishedPageDTO.getName()); + } + + if (publishedPageDTO.getLayouts() != null) { + newPage.getPublishedPage().getLayouts().forEach(layout -> + publishedMongoEscapedWidgetsNames + .put(layout.getId(), layout.getMongoEscapedWidgetNames()) + ); + } + } + newPage.setApplicationId(null); + examplesOrganizationCloner.makePristine(newPage); + }); + applicationJson.setPageList(newPageList); + applicationJson.setPublishedLayoutmongoEscapedWidgets(publishedMongoEscapedWidgetsNames); + applicationJson.setUnpublishedLayoutmongoEscapedWidgets(unpublishedMongoEscapedWidgetsNames); + return datasourceRepository + .findAllByOrganizationId(organizationId, AclPermission.MANAGE_DATASOURCES) + .collectList(); + }) + .flatMapMany(datasourceList -> { + datasourceList.forEach(datasource -> + datasourceIdToNameMap.put(datasource.getId(), datasource.getName())); + + applicationJson.setDatasourceList(datasourceList); + return newActionRepository + .findByApplicationId(applicationId, AclPermission.MANAGE_ACTIONS, null); + }) + .collectList() + .map(newActionList -> { + Set concernedDBNames = new HashSet<>(); + newActionList.forEach(newAction -> { + newAction.setPluginId(pluginMap.get(newAction.getPluginId())); + newAction.setOrganizationId(null); + newAction.setPolicies(null); + newAction.setApplicationId(null); + //Collect Datasource names to filter only required datasources + if (newAction.getPluginType() == PluginType.DB || newAction.getPluginType() == PluginType.API) { + concernedDBNames.add( + mapDatasourceIdToNewAction(newAction.getPublishedAction(), datasourceIdToNameMap) + ); + concernedDBNames.add( + mapDatasourceIdToNewAction(newAction.getUnpublishedAction(), datasourceIdToNameMap) + ); + } + if (newAction.getUnpublishedAction() != null) { + ActionDTO actionDTO = newAction.getUnpublishedAction(); + actionDTO.setPageId(pageIdToNameMap.get(actionDTO.getPageId() + PublishType.UNPUBLISH)); + } + if (newAction.getPublishedAction() != null) { + ActionDTO actionDTO = newAction.getPublishedAction(); + actionDTO.setPageId(pageIdToNameMap.get(actionDTO.getPageId() + PublishType.PUBLISH)); + } + }); + applicationJson + .getDatasourceList() + .removeIf(datasource -> !concernedDBNames.contains(datasource.getName())); + + applicationJson.setActionList(newActionList); + + //Only export those datasources which are used in the app instead of org level + Map decryptedFields = new HashMap<>(); + applicationJson.getDatasourceList().forEach(datasource -> { + decryptedFields.put(datasource.getName(), getDecryptedFields(datasource)); + datasource.setId(null); + datasource.setOrganizationId(null); + datasource.setPluginId(pluginMap.get(datasource.getPluginId())); + if (datasource.getDatasourceConfiguration() != null) { + datasource.getDatasourceConfiguration().setAuthentication(null); + } + }); + applicationJson.setDecryptedFields(decryptedFields); + return applicationJson; + }); + }) + .then() + .thenReturn(applicationJson); + } + + public Mono extractFileAndSaveApplication(String orgId, Part filePart) { + + final MediaType contentType = filePart.headers().getContentType(); + + if (orgId == null || orgId.isEmpty()) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORGANIZATION_ID)); + } + + if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) { + return Mono.error(new AppsmithException(AppsmithError.VALIDATION_FAILURE, INVALID_JSON_FILE)); + } + + final Flux contentCache = filePart.content().cache(); + Mono stringifiedFile = DataBufferUtils.join(contentCache) + .map(dataBuffer -> { + byte[] data = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(data); + DataBufferUtils.release(dataBuffer); + return new String(data); + }); + + return stringifiedFile + .flatMap(data -> { + Gson gson = new Gson(); + Type fileType = new TypeToken() {}.getType(); + ApplicationJson jsonFile = gson.fromJson(data, fileType); + return importApplicationInOrganization(orgId, jsonFile); + }); + } + + public Mono importApplicationInOrganization(String orgId, ApplicationJson importedDoc) { + Map pluginMap = new HashMap<>(); + Map datasourceMap = new HashMap<>(); + Map pageNameMap = new HashMap<>(); + Map actionIdMap = new HashMap<>(); + + Application importedApplication = importedDoc.getExportedApplication(); + List importedDatasourceList = importedDoc.getDatasourceList(); + List importedNewPageList = importedDoc.getPageList(); + List importedNewActionList = importedDoc.getActionList(); + + Mono currUserMono = sessionUserService.getCurrentUser(); + final Flux existingDatasourceFlux = datasourceRepository.findAllByOrganizationId(orgId).cache(); + + String errorField = ""; + if (importedNewPageList == null || importedNewPageList.isEmpty()) { + errorField = FieldName.PAGES; + } else if (importedApplication == null) { + errorField = FieldName.APPLICATION; + } else if (importedNewActionList == null) { + errorField = FieldName.ACTIONS; + } else if (importedDatasourceList == null) { + errorField = FieldName.DATASOURCE; + } + + if(!errorField.isEmpty()) { + return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, errorField, INVALID_JSON_FILE)); + } + + return pluginRepository.findAll() + .map(plugin -> { + pluginMap.put(plugin.getPackageName(), plugin.getId()); + return plugin; + }) + .then(organizationService.findById(orgId, AclPermission.ORGANIZATION_MANAGE_APPLICATIONS)) + .switchIfEmpty(Mono.error( + new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.ORGANIZATION, orgId)) + ) + .flatMap(organization -> Flux.fromIterable(importedDatasourceList) + //Check for duplicate datasources to avoid duplicates in target organization + .flatMap(datasource -> { + datasource.setPluginId(pluginMap.get(datasource.getPluginId())); + datasource.setOrganizationId(organization.getId()); + + //Check if any decrypted fields are present for datasource + if (importedDoc.getDecryptedFields().get(datasource.getName()) != null) { + + DecryptedSensitiveFields decryptedFields = + importedDoc.getDecryptedFields().get(datasource.getName()); + + updateAuthenticationDTO(datasource, decryptedFields); + } + return createUniqueDatasourceIfNotPresent(existingDatasourceFlux, datasource, orgId); + }) + .map(datasource -> { + datasourceMap.put(datasource.getName(), datasource.getId()); + return datasource; + }) + .collectList() + ) + .then( + + applicationPageService.setApplicationPolicies(currUserMono, orgId, importedApplication) + .flatMap(application -> applicationService + .findByOrganizationId(orgId, AclPermission.MANAGE_APPLICATIONS) + .collectList() + .flatMap(applicationList -> { + + Application duplicateNameApp = applicationList + .stream() + .filter(application1 -> StringUtils.equals(application1.getName(), application.getName())) + .findAny() + .orElse(null); + + return getUniqueSuffixForDuplicateNameEntity(duplicateNameApp, orgId) + .map(suffix -> { + importedApplication.setName(importedApplication.getName() + suffix); + return importedApplication; + }); + }) + .then(applicationService.save(importedApplication)) + ) + ) + .flatMap(savedApp -> { + importedApplication.setId(savedApp.getId()); + importedNewPageList.forEach(newPage -> newPage.setApplicationId(savedApp.getId())); + Map> applicationPages = Map.of( + PublishType.UNPUBLISH, new ArrayList<>(), + PublishType.PUBLISH, new ArrayList<>() + ); + + return importAndSavePages( + importedNewPageList, + importedApplication, + importedDoc.getPublishedLayoutmongoEscapedWidgets(), + importedDoc.getUnpublishedLayoutmongoEscapedWidgets() + ) + .map(newPage -> { + ApplicationPage unpublishedAppPage = new ApplicationPage(); + ApplicationPage publishedAppPage = new ApplicationPage(); + + if (newPage.getUnpublishedPage() != null && newPage.getUnpublishedPage().getName() != null) { + unpublishedAppPage.setIsDefault( + StringUtils.equals( + newPage.getUnpublishedPage().getName(), importedDoc.getUnpublishedDefaultPageName() + ) + ); + unpublishedAppPage.setId(newPage.getId()); + pageNameMap.put(newPage.getUnpublishedPage().getName(), newPage); + } + + if (newPage.getPublishedPage() != null && newPage.getPublishedPage().getName() != null) { + publishedAppPage.setIsDefault( + StringUtils.equals( + newPage.getPublishedPage().getName(), importedDoc.getPublishedDefaultPageName() + ) + ); + publishedAppPage.setId(newPage.getId()); + pageNameMap.put(newPage.getPublishedPage().getName(), newPage); + } + applicationPages.get(PublishType.UNPUBLISH).add(unpublishedAppPage); + applicationPages.get(PublishType.PUBLISH).add(publishedAppPage); + return applicationPages; + }) + .then() + .thenReturn(applicationPages); + }) + .flatMap(applicationPageMap -> { + importedApplication.setPages(applicationPageMap.get(PublishType.UNPUBLISH)); + importedApplication.setPublishedPages(applicationPageMap.get(PublishType.PUBLISH)); + + importedNewActionList.forEach(newAction -> { + NewPage parentPage = new NewPage(); + if (newAction.getUnpublishedAction() != null && newAction.getUnpublishedAction().getName() != null) { + parentPage = pageNameMap.get(newAction.getUnpublishedAction().getPageId()); + actionIdMap.put(newAction.getUnpublishedAction().getName(), newAction.getId()); + newAction.getUnpublishedAction().setPageId(parentPage.getId()); + mapDatasourceIdToNewAction(newAction.getUnpublishedAction(), datasourceMap); + } + + if (newAction.getPublishedAction() != null && newAction.getPublishedAction().getName() != null) { + parentPage = pageNameMap.get(newAction.getPublishedAction().getPageId()); + actionIdMap.put(newAction.getPublishedAction().getName(), newAction.getId()); + newAction.getPublishedAction().setPageId(parentPage.getId()); + mapDatasourceIdToNewAction(newAction.getPublishedAction(), datasourceMap); + } + + examplesOrganizationCloner.makePristine(newAction); + newAction.setOrganizationId(orgId); + newAction.setApplicationId(importedApplication.getId()); + newAction.setPluginId(pluginMap.get(newAction.getPluginId())); + newActionService.generateAndSetActionPolicies(parentPage, newAction); + }); + return newActionService.saveAll(importedNewActionList) + .map(newAction -> { + + if (newAction.getUnpublishedAction() != null) { + actionIdMap.put( + actionIdMap.get(newAction.getUnpublishedAction().getName()), newAction.getId() + ); + } + + if (newAction.getPublishedAction() != null) { + actionIdMap.put( + actionIdMap.get(newAction.getPublishedAction().getName()), newAction.getId() + ); + } + + return newAction; + }) + .then(Mono.just(importedApplication)); + }) + .flatMap(ignored -> { + //Map layoutOnLoadActions ids with relevant actions + importedNewPageList.forEach(page -> mapActionIdWithPageLayout(page, actionIdMap)); + return Flux.fromIterable(importedNewPageList) + .flatMap(newPageService::save) + .then(applicationService.update(importedApplication.getId(), importedApplication)); + }); + } + + private Mono getUniqueSuffixForDuplicateNameEntity(BaseDomain sourceEntity, String orgId) { + if (sourceEntity != null) { + return sequenceService + .getNextAsSuffix(sourceEntity.getClass(), " for organization with _id : " + orgId) + .flatMap(sequenceNumber -> Mono.just(" #" + sequenceNumber.trim())); + } + return Mono.just(""); + } + + private Flux importAndSavePages(List pages, + Application application, + Map> publishedMongoEscapedWidget, + Map> unpublishedMongoEscapedWidget + ) { + + pages.forEach(newPage -> { + String layoutId = new ObjectId().toString(); + newPage.setApplicationId(application.getId()); + if (newPage.getUnpublishedPage() != null) { + applicationPageService.generateAndSetPagePolicies(application, newPage.getUnpublishedPage()); + newPage.setPolicies(newPage.getUnpublishedPage().getPolicies()); + if (unpublishedMongoEscapedWidget != null) { + newPage.getUnpublishedPage().getLayouts().forEach(layout -> { + layout.setMongoEscapedWidgetNames(unpublishedMongoEscapedWidget.get(layout.getId())); + layout.setId(layoutId); + }); + } + } + + if (newPage.getPublishedPage() != null) { + applicationPageService.generateAndSetPagePolicies(application, newPage.getPublishedPage()); + if (publishedMongoEscapedWidget != null) { + newPage.getPublishedPage().getLayouts().forEach(layout -> { + layout.setMongoEscapedWidgetNames(publishedMongoEscapedWidget.get(layout.getId())); + layout.setId(layoutId); + }); + } + } + }); + + return Flux.fromIterable(pages) + .flatMap(newPageService::save); + } + + private String mapDatasourceIdToNewAction(ActionDTO actionDTO, Map datasourceMap) { + + if (actionDTO != null && actionDTO.getDatasource() != null && actionDTO.getDatasource().getId() != null) { + + Datasource ds = actionDTO.getDatasource(); + //Mapping ds name in id field + ds.setId(datasourceMap.get(ds.getId())); + ds.setOrganizationId(null); + ds.setPluginId(null); + return ds.getId(); + } + return ""; + } + + private void mapActionIdWithPageLayout(NewPage page, Map actionIdMap) { + if (page.getUnpublishedPage().getLayouts() != null) { + + page.getUnpublishedPage().getLayouts().forEach(layout -> { + if (layout.getLayoutOnLoadActions() != null) { + layout.getLayoutOnLoadActions().forEach(onLoadAction -> onLoadAction + .forEach(actionDTO -> actionDTO.setId(actionIdMap.get(actionDTO.getId())))); + } + }); + } + + if (page.getPublishedPage() != null && page.getPublishedPage().getLayouts() != null) { + + page.getPublishedPage().getLayouts().forEach(layout -> { + if (layout.getLayoutOnLoadActions() != null) { + layout.getLayoutOnLoadActions().forEach(onLoadAction -> onLoadAction + .forEach(actionDTO -> actionDTO.setId(actionIdMap.get(actionDTO.getId())))); + } + }); + } + } + + private Mono createUniqueDatasourceIfNotPresent(Flux existingDatasourceFlux, + Datasource datasource, + String toOrgId) { + + final DatasourceConfiguration datasourceConfig = datasource.getDatasourceConfiguration(); + AuthenticationResponse authResponse = new AuthenticationResponse(); + if (datasourceConfig != null && datasourceConfig.getAuthentication() != null) { + BeanCopyUtils.copyNestedNonNullProperties( + datasourceConfig.getAuthentication().getAuthenticationResponse(), authResponse); + datasourceConfig.getAuthentication().setAuthenticationResponse(null); + datasourceConfig.getAuthentication().setAuthenticationType(null); + } + + return existingDatasourceFlux + .map(ds -> { + final DatasourceConfiguration dsAuthConfig = ds.getDatasourceConfiguration(); + if (dsAuthConfig != null && dsAuthConfig.getAuthentication() != null) { + dsAuthConfig.getAuthentication().setAuthenticationResponse(null); + dsAuthConfig.getAuthentication().setAuthenticationType(null); + } + return ds; + }) + .filter(ds -> ds.softEquals(datasource)) + .next() // Get the first matching datasource, we don't need more than one here. + .switchIfEmpty(Mono.defer(() -> { + if (datasourceConfig != null && datasourceConfig.getAuthentication() != null) { + datasourceConfig.getAuthentication().setAuthenticationResponse(authResponse); + } + // No matching existing datasource found, so create a new one. + return datasourceService + .findByNameAndOrganizationId(datasource.getName(), toOrgId, AclPermission.MANAGE_DATASOURCES) + .flatMap(duplicateNameDatasource -> + getUniqueSuffixForDuplicateNameEntity(duplicateNameDatasource, toOrgId) + ) + .map(suffix -> { + datasource.setName(datasource.getName() + suffix); + return datasource; + }) + .then(datasourceService.create(datasource)); + })); + } + + private Datasource updateAuthenticationDTO(Datasource datasource, DecryptedSensitiveFields decryptedFields) { + + final DatasourceConfiguration dsConfig = datasource.getDatasourceConfiguration(); + String authType = decryptedFields.getAuthType(); + if (dsConfig == null || authType == null) { + return datasource; + } + + if (StringUtils.equals(authType, DBAuth.class.getName())) { + final DBAuth dbAuth = decryptedFields.getDbAuth(); + dbAuth.setPassword(decryptedFields.getPassword()); + datasource.getDatasourceConfiguration().setAuthentication(dbAuth); + } else if (StringUtils.equals(authType, BasicAuth.class.getName())) { + final BasicAuth basicAuth = decryptedFields.getBasicAuth(); + basicAuth.setPassword(decryptedFields.getPassword()); + datasource.getDatasourceConfiguration().setAuthentication(basicAuth); + } else if (StringUtils.equals(authType, OAuth2.class.getName())) { + OAuth2 auth2 = decryptedFields.getOpenAuth2(); + AuthenticationResponse authResponse = new AuthenticationResponse(); + auth2.setClientSecret(decryptedFields.getPassword()); + authResponse.setToken(decryptedFields.getToken()); + authResponse.setRefreshToken(decryptedFields.getRefreshToken()); + authResponse.setTokenResponse(decryptedFields.getTokenResponse()); + authResponse.setExpiresAt(Instant.now()); + datasource.getDatasourceConfiguration().setAuthentication(auth2); + } + return datasource; + } + + private DecryptedSensitiveFields getDecryptedFields(Datasource datasource) { + final AuthenticationDTO authentication = datasource.getDatasourceConfiguration() == null + ? null : datasource.getDatasourceConfiguration().getAuthentication(); + + if (authentication != null) { + DecryptedSensitiveFields dsDecryptedFields = + authentication.getAuthenticationResponse() == null + ? new DecryptedSensitiveFields() + : new DecryptedSensitiveFields(authentication.getAuthenticationResponse()); + + if (authentication instanceof DBAuth) { + DBAuth auth = (DBAuth) authentication; + dsDecryptedFields.setPassword(auth.getPassword()); + dsDecryptedFields.setDbAuth(auth); + } else if (authentication instanceof OAuth2) { + OAuth2 auth = (OAuth2) authentication; + dsDecryptedFields.setPassword(auth.getClientSecret()); + dsDecryptedFields.setOpenAuth2(auth); + } else if (authentication instanceof BasicAuth) { + BasicAuth auth = (BasicAuth) authentication; + dsDecryptedFields.setPassword(auth.getPassword()); + dsDecryptedFields.setBasicAuth(auth); + } + dsDecryptedFields.setAuthType(authentication.getClass().getName()); + return dsDecryptedFields; + } + return null; + } +} diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java index ae6f8ca312..6c7b195e13 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java @@ -607,8 +607,10 @@ public class OrganizationServiceTest { Mono readOrganizationByNameMono = organizationRepository.findByName("Member Management Admin Test Organization") .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "organization by name"))); - Mono readDatasourceByNameMono = datasourceRepository.findByName("test datasource", READ_DATASOURCES) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "Datasource"))); + Mono readDatasourceByNameMono = organizationMono.flatMap(organization1 -> + datasourceRepository.findByNameAndOrganizationId("test datasource", organization1.getId(),READ_DATASOURCES) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "Datasource"))) + ); Mono> testMono = organizationMono // create application and datasource diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java new file mode 100644 index 0000000000..a18f2ab5e6 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java @@ -0,0 +1,534 @@ +package com.appsmith.server.solutions; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.DBAuth; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DecryptedSensitiveFields; +import com.appsmith.external.models.Policy; +import com.appsmith.external.models.Property; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationJson; +import com.appsmith.server.domains.ApplicationPage; +import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.domains.Organization; +import com.appsmith.server.domains.Plugin; +import com.appsmith.server.domains.PluginType; +import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; +import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.MockPluginExecutor; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.repositories.NewPageRepository; +import com.appsmith.server.repositories.PluginRepository; +import com.appsmith.server.services.ApplicationPageService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.LayoutActionService; +import com.appsmith.server.services.NewActionService; +import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.OrganizationService; +import com.appsmith.server.services.SessionUserService; +import com.appsmith.server.services.UserService; +import lombok.extern.slf4j.Slf4j; +import net.minidev.json.JSONArray; +import net.minidev.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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 org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.LinkedMultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; +import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; +import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; +import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; +import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS; +import static com.appsmith.server.acl.AclPermission.READ_PAGES; +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@RunWith(SpringRunner.class) +@SpringBootTest +@DirtiesContext +public class ImportExportApplicationServiceTests { + + @Autowired + private ImportExportApplicationService importExportApplicationService; + + @Autowired + private ApplicationPageService applicationPageService; + + @Autowired + private UserService userService; + + @Autowired + private PluginRepository pluginRepository; + + @Autowired + private ApplicationService applicationService; + + @Autowired + private DatasourceService datasourceService; + + @Autowired + private NewPageService newPageService; + + @Autowired + private NewActionService newActionService; + + @Autowired + private OrganizationService organizationService; + + @Autowired + private SessionUserService sessionUserService; + + @Autowired + private LayoutActionService layoutActionService; + + @Autowired + private NewPageRepository newPageRepository; + + @MockBean + private PluginExecutorHelper pluginExecutorHelper; + + private String invalid_json_file; + private Plugin installedPlugin; + private String orgId; + private String testAppId; + private Map datasourceMap = new HashMap<>(); + + private Flux getActionsInApplication(Application application) { + return newPageService + // fetch the unpublished pages + .findByApplicationId(application.getId(), READ_PAGES, false) + .flatMap(page -> newActionService.getUnpublishedActions(new LinkedMultiValueMap<>( + Map.of(FieldName.PAGE_ID, Collections.singletonList(page.getId()))))); + } + + @Before + @WithUserDetails(value = "api_user") + public void setup() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + installedPlugin = pluginRepository.findByPackageName("installed-plugin").block(); + User apiUser = userService.findByEmail("api_user").block(); + orgId = apiUser.getOrganizationIds().iterator().next(); + + invalid_json_file = importExportApplicationService.INVALID_JSON_FILE; + + Datasource ds1 = new Datasource(); + ds1.setName("DS1"); + ds1.setOrganizationId(orgId); + ds1.setPluginId(installedPlugin.getId()); + final DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + ds1.setDatasourceConfiguration(datasourceConfiguration); + datasourceConfiguration.setUrl("http://httpbin.org/get"); + datasourceConfiguration.setHeaders(List.of( + new Property("X-Answer", "42") + )); + + final Datasource ds2 = new Datasource(); + ds2.setName("DS2"); + ds2.setPluginId(installedPlugin.getId()); + ds2.setDatasourceConfiguration(new DatasourceConfiguration()); + DBAuth auth = new DBAuth(); + auth.setPassword("awesome-password"); + ds2.getDatasourceConfiguration().setAuthentication(auth); + + datasourceMap.put("DS1", ds1); + datasourceMap.put("DS2", ds2); + } + + @Test + public void exportApplicationWithNullApplicationIdTest() { + Mono resultMono = importExportApplicationService.exportApplicationById(null); + + StepVerifier + .create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException && + throwable.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.APPLICATION_ID))) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createExportAppJsonWithoutActionsAndDatasourceTest() { + + Application testApplication = new Application(); + testApplication.setName("Export Application TestApp"); + + final Mono resultMono = applicationPageService.createApplication(testApplication, orgId) + .flatMap(application -> importExportApplicationService.exportApplicationById(application.getId())); + + StepVerifier.create(resultMono) + .assertNext(applicationJson -> { + Application exportedApp = applicationJson.getExportedApplication(); + List pageList = applicationJson.getPageList(); + List actionList = applicationJson.getActionList(); + List datasourceList = applicationJson.getDatasourceList(); + + NewPage defaultPage = pageList.get(0); + + assertThat(exportedApp.getName()).isEqualTo(testApplication.getName()); + assertThat(exportedApp.getOrganizationId()).isNull(); + assertThat(exportedApp.getPages()).isNull(); + assertThat(exportedApp.getPolicies().size()).isEqualTo(0); + + assertThat(pageList.isEmpty()).isFalse(); + assertThat(defaultPage.getApplicationId()).isNull(); + assertThat(defaultPage.getUnpublishedPage().getLayouts().get(0).getLayoutOnLoadActions()).isNull(); + + assertThat(actionList.isEmpty()).isTrue(); + + assertThat(datasourceList.isEmpty()).isTrue(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createExportAppJsonWithDatasourceButWithoutActionsTest() { + + Application testApplication = new Application(); + testApplication.setName("Another Export Application"); + + final Mono resultMono = organizationService.getById(orgId) + .flatMap(organization -> { + + final Datasource ds1 = datasourceMap.get("DS1"); + ds1.setOrganizationId(organization.getId()); + + final Datasource ds2 = datasourceMap.get("DS2"); + ds2.setOrganizationId(organization.getId()); + + return Mono.zip( + datasourceService.create(ds1), + datasourceService.create(ds2), + applicationPageService.createApplication(testApplication, orgId) + ); + }) + .flatMap(tuple -> importExportApplicationService.exportApplicationById(tuple.getT3().getId())); + + StepVerifier.create(resultMono) + .assertNext(applicationJson -> { + + assertThat(applicationJson.getPageList()).hasSize(1); + assertThat(applicationJson.getActionList()).isEmpty(); + assertThat(applicationJson.getDatasourceList()).isEmpty(); + assertThat(applicationJson.getDecryptedFields()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createExportAppJsonWithActionsAndDatasourceTest() { + + Organization newOrganization = new Organization(); + newOrganization.setName("template-org-with-ds"); + + Application testApplication = new Application(); + testApplication.setName("ApplicationWithActionsAndDatasource"); + + final Mono resultMono = organizationService.create(newOrganization) + .zipWhen(org -> applicationPageService.createApplication(testApplication, org.getId())) + .flatMap(tuple -> { + + Organization organization = tuple.getT1(); + Application testApp = tuple.getT2(); + + final Datasource ds1 = datasourceMap.get("DS1"); + ds1.setOrganizationId(organization.getId()); + + final Datasource ds2 = datasourceMap.get("DS2"); + ds2.setOrganizationId(organization.getId()); + + final String pageId = testApp.getPages().get(0).getId(); + + return Mono.zip( + datasourceService.create(ds1), + datasourceService.create(ds2), + Mono.just(testApp), + newPageService.findPageById(pageId, READ_PAGES, false) + ); + }) + .flatMap(tuple -> { + + Datasource ds1 = tuple.getT1(); + Datasource ds2 = tuple.getT2(); + Application testApp = tuple.getT3(); + PageDTO testPage = tuple.getT4(); + + Layout layout = testPage.getLayouts().get(0); + JSONObject dsl = new JSONObject(Map.of("text", "{{ query1.data }}")); + + JSONObject dsl2 = new JSONObject(); + dsl2.put("widgetName", "Table1"); + dsl2.put("type", "TABLE_WIDGET"); + Map primaryColumns = new HashMap<>(); + JSONObject jsonObject = new JSONObject(Map.of("key", "value")); + primaryColumns.put("_id", "{{ query1.data }}"); + primaryColumns.put("_class", jsonObject); + dsl2.put("primaryColumns", primaryColumns); + final ArrayList objects = new ArrayList<>(); + JSONArray temp2 = new JSONArray(); + temp2.addAll(List.of(new JSONObject(Map.of("key", "primaryColumns._id")))); + dsl2.put("dynamicBindingPathList", temp2); + objects.add(dsl2); + dsl.put("children", objects); + + layout.setDsl(dsl); + layout.setPublishedDsl(dsl); + + ActionDTO action = new ActionDTO(); + action.setName("validAction"); + action.setPageId(testPage.getId()); + action.setExecuteOnLoad(true); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(ds2); + + return layoutActionService.createAction(action) + .flatMap(createdAction -> newActionService.findById(createdAction.getId(), READ_ACTIONS)) + .flatMap(newAction -> newActionService.generateActionByViewMode(newAction, false)) + .then(importExportApplicationService.exportApplicationById(testApp.getId())); + }); + + StepVerifier + .create(resultMono) + .assertNext(applicationJson -> { + + Application exportedApp = applicationJson.getExportedApplication(); + List pageList = applicationJson.getPageList(); + List actionList = applicationJson.getActionList(); + List datasourceList = applicationJson.getDatasourceList(); + + NewPage defaultPage = pageList.get(0); + + assertThat(exportedApp.getName()).isEqualTo(testApplication.getName()); + assertThat(exportedApp.getOrganizationId()).isNull(); + assertThat(exportedApp.getPages()).isNull(); + + assertThat(exportedApp.getPolicies()).hasSize(0); + + assertThat(pageList).hasSize(1); + assertThat(defaultPage.getApplicationId()).isNull(); + assertThat(defaultPage.getUnpublishedPage().getLayouts().get(0).getDsl()).isNotNull(); + assertThat(defaultPage.getId()).isNull(); + assertThat(defaultPage.getPolicies()).isEmpty(); + + assertThat(actionList.isEmpty()).isFalse(); + NewAction validAction = actionList.get(0); + assertThat(validAction.getApplicationId()).isNull(); + assertThat(validAction.getPluginId()).isEqualTo(installedPlugin.getPackageName()); + assertThat(validAction.getPluginType()).isEqualTo(PluginType.API); + assertThat(validAction.getOrganizationId()).isNull(); + assertThat(validAction.getPolicies()).isNull(); + assertThat(validAction.getId()).isNotNull(); + assertThat(validAction.getUnpublishedAction().getPageId()) + .isEqualTo(defaultPage.getUnpublishedPage().getName()); + + assertThat(datasourceList).hasSize(1); + Datasource datasource = datasourceList.get(0); + assertThat(datasource.getOrganizationId()).isNull(); + assertThat(datasource.getId()).isNull(); + assertThat(datasource.getPluginId()).isEqualTo(installedPlugin.getPackageName()); + assertThat(datasource.getDatasourceConfiguration().getAuthentication()).isNull(); + + DecryptedSensitiveFields decryptedFields = + applicationJson.getDecryptedFields().get(datasource.getName()); + + DBAuth auth = (DBAuth) datasourceMap.get("DS2").getDatasourceConfiguration().getAuthentication(); + assertThat(decryptedFields.getAuthType()).isEqualTo(auth.getClass().getName()); + assertThat(decryptedFields.getPassword()).isEqualTo("awesome-password"); + + assertThat(applicationJson.getUnpublishedLayoutmongoEscapedWidgets()).isNotEmpty(); + assertThat(applicationJson.getPublishedLayoutmongoEscapedWidgets()).isNotEmpty(); + }) + .verifyComplete(); + } + + @Test + public void importApplicationFromInvalidFileTest() { + FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); + Flux dataBufferFlux = DataBufferUtils + .read(new ClassPathResource("test_assets/OrganizationServiceTest/my_organization_logo.png"), new DefaultDataBufferFactory(), 4096) + .cache(); + + Mockito.when(filepart.content()).thenReturn(dataBufferFlux); + Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.IMAGE_PNG); + + Mono resultMono = importExportApplicationService.extractFileAndSaveApplication(orgId, filepart); + + StepVerifier + .create(resultMono) + .expectErrorMatches(error -> error instanceof AppsmithException) + .verify(); + } + + @Test + public void importApplicationWithNullOrganizationIdTest() { + FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); + + Mono resultMono = importExportApplicationService + .extractFileAndSaveApplication(null, filepart); + + StepVerifier + .create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException && + throwable.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.ORGANIZATION_ID))) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplicationFromInvalidJsonFileWithoutPagesTest() { + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/invalid-json-without-pages.json"); + Mono resultMono = importExportApplicationService.extractFileAndSaveApplication(orgId,filePart); + + StepVerifier + .create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException && + throwable.getMessage().equals(AppsmithError.NO_RESOURCE_FOUND.getMessage(FieldName.PAGES, invalid_json_file))) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplicationFromInvalidJsonFileWithoutApplicationTest() { + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/invalid-json-without-app.json"); + Mono resultMono = importExportApplicationService.extractFileAndSaveApplication(orgId,filePart); + + StepVerifier + .create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException && + throwable.getMessage().equals(AppsmithError.NO_RESOURCE_FOUND.getMessage(FieldName.APPLICATION, invalid_json_file))) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplicationFromValidJsonFileTest() { + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json"); + + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization"); + + Policy manageAppPolicy = Policy.builder().permission(MANAGE_APPLICATIONS.getValue()) + .users(Set.of("api_user")) + .build(); + Policy readAppPolicy = Policy.builder().permission(READ_APPLICATIONS.getValue()) + .users(Set.of("api_user")) + .build(); + + final Mono resultMono = organizationService + .create(newOrganization) + .flatMap(organization -> importExportApplicationService + .extractFileAndSaveApplication(organization.getId(), filePart) + ); + + StepVerifier + .create(resultMono + .flatMap(application -> Mono.zip( + Mono.just(application), + datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(), + getActionsInApplication(application).collectList(), + newPageService.findByApplicationId(application.getId(), MANAGE_PAGES, false).collectList() + ))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List datasourceList = tuple.getT2(); + final List actionDTOS = tuple.getT3(); + final List pageList = tuple.getT4(); + + assertThat(application.getName()).isEqualTo("valid_application"); + assertThat(application.getOrganizationId()).isNotNull(); + assertThat(application.getPages()).isNotEmpty(); + assertThat(application.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy)); + + assertThat(datasourceList).isNotEmpty(); + datasourceList.forEach(datasource -> { + assertThat(datasource.getOrganizationId()).isEqualTo(application.getOrganizationId()); + if (datasource.getName().contains("wo-auth")) { + assertThat(datasource.getDatasourceConfiguration().getAuthentication()).isNull(); + } else if (datasource.getName().contains("db")) { + DBAuth auth = (DBAuth) datasource.getDatasourceConfiguration().getAuthentication(); + assertThat(auth).isNotNull(); + assertThat(auth.getPassword()).isNotNull(); + assertThat(auth.getUsername()).isNotNull(); + } + }); + + assertThat(actionDTOS).isNotEmpty(); + actionDTOS.forEach(actionDTO -> { + assertThat(actionDTO.getPageId()).isNotEqualTo(pageList.get(0).getName()); + + }); + + assertThat(pageList).isNotEmpty(); + + ApplicationPage defaultAppPage = application.getPages() + .stream() + .filter(ApplicationPage::getIsDefault) + .findFirst() + .orElse(null); + assertThat(defaultAppPage).isNotNull(); + + PageDTO defaultPageDTO = pageList.stream() + .filter(pageDTO -> pageDTO.getId().equals(defaultAppPage.getId())).findFirst().orElse(null); + + assertThat(defaultPageDTO).isNotNull(); + assertThat(defaultPageDTO.getLayouts().get(0).getLayoutOnLoadActions()).isNotEmpty(); + }) + .verifyComplete(); + } + + private FilePart createFilePart(String filePath) { + FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); + Flux dataBufferFlux = DataBufferUtils + .read( + new ClassPathResource(filePath), + new DefaultDataBufferFactory(), + 4096) + .cache(); + + Mockito.when(filepart.content()).thenReturn(dataBufferFlux); + Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.APPLICATION_JSON); + + return filepart; + + } + +} diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-json-without-app.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-json-without-app.json new file mode 100644 index 0000000000..2c91f465bf --- /dev/null +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-json-without-app.json @@ -0,0 +1,18 @@ +{ + "datasourceList": [], + "pageList": [ + { + "applicationId": "test_app" + } + ], + "actionList": [ + { + "id": "randomId", + "userPermissions": [], + "unpublishedAction": {}, + "publishedAction": {} + } + ], + "decryptedFields": {}, + "mongoEscapedWidgets": {} +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-json-without-pages.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-json-without-pages.json new file mode 100644 index 0000000000..2918b2bea0 --- /dev/null +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-json-without-pages.json @@ -0,0 +1,12 @@ +{ + "exportedApplication": {}, + "datasourceList": [], + "pageList": [], + "actionList": [ + { + "id": "randomId" + } + ], + "decryptedFields": {}, + "mongoEscapedWidgets": {} +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application.json new file mode 100644 index 0000000000..8572858c94 --- /dev/null +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application.json @@ -0,0 +1,505 @@ +{ + "exportedApplication": { + "userPermissions": [ + "canComment:applications", + "manage:applications", + "read:applications", + "publish:applications", + "makePublic:applications" + ], + "name": "valid_application", + "isPublic": false, + "appIsExample": false, + "color": "#EA6179", + "icon": "medical", + "new": true + }, + "datasourceList": [ + { + "userPermissions": [ + "execute:datasources", + "manage:datasources", + "read:datasources" + ], + "name": "db-auth", + "pluginId": "mongo-plugin", + "datasourceConfiguration": { + "connection": { + "mode": "READ_WRITE", + "type": "REPLICA_SET", + "ssl": { + "authType": "DEFAULT" + } + }, + "endpoints": [ + { + "host": "db-auth-uri.net" + } + ], + "sshProxyEnabled": false, + "properties": [ + { + "key": "Use Mongo Connection String URI", + "value": "No" + } + ] + }, + "invalids": [], + "isValid": true, + "new": true + }, + { + "userPermissions": [ + "execute:datasources", + "manage:datasources", + "read:datasources" + ], + "name": "api_ds_wo_auth", + "pluginId": "restapi-plugin", + "datasourceConfiguration": { + "sshProxyEnabled": false, + "properties": [ + { + "key": "isSendSessionEnabled", + "value": "N" + }, + { + "key": "sessionSignatureKey", + "value": "" + } + ], + "url": "https://api-ds-wo-auth-uri.com", + "headers": [] + }, + "invalids": [], + "isValid": true, + "new": true + } + ], + "pageList": [ + { + "userPermissions": [ + "read:pages", + "manage:pages" + ], + "applicationId": "valid_application", + "unpublishedPage": { + "name": "Page1", + "layouts": [ + { + "id": "60aca056136c4b7178f67906", + "userPermissions": [], + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1280, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 800, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 4, + "minHeight": 840, + "parentColumnSpace": 1, + "dynamicTriggerPathList": [], + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "widgetName": "Table1", + "columnOrder": [ + "_id", + "username", + "active" + ], + "dynamicPropertyPathList": [], + "topRow": 4, + "bottomRow": 15, + "parentRowSpace": 40, + "type": "TABLE_WIDGET", + "parentColumnSpace": 77.5, + "dynamicTriggerPathList": [], + "dynamicBindingPathList": [ + { + "key": "tableData" + }, + { + "key": "primaryColumns._id.computedValue" + }, + { + "key": "primaryColumns.username.computedValue" + }, + { + "key": "primaryColumns.active.computedValue" + } + ], + "leftColumn": 0, + "primaryColumns": { + "appsmith_mongo_escape_id": { + "isDerived": false, + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => { return currentRow._id})}}", + "textSize": "PARAGRAPH", + "index": 4, + "isVisible": true, + "label": "_id", + "columnType": "text", + "horizontalAlignment": "LEFT", + "width": 150, + "enableFilter": true, + "enableSort": true, + "id": "_id", + "verticalAlignment": "CENTER" + }, + "active": { + "isDerived": false, + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => { return currentRow.active})}}", + "textSize": "PARAGRAPH", + "index": 8, + "isVisible": true, + "label": "active", + "columnType": "text", + "horizontalAlignment": "LEFT", + "width": 150, + "enableFilter": true, + "enableSort": true, + "id": "active", + "verticalAlignment": "CENTER" + }, + "username": { + "isDerived": false, + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => { return currentRow.username})}}", + "textSize": "PARAGRAPH", + "index": 7, + "isVisible": true, + "label": "username", + "columnType": "text", + "horizontalAlignment": "LEFT", + "width": 150, + "enableFilter": true, + "enableSort": true, + "id": "username", + "verticalAlignment": "CENTER" + } + }, + "derivedColumns": {}, + "rightColumn": 8, + "textSize": "PARAGRAPH", + "widgetId": "aisibaxwhb", + "tableData": "{{get_users.data}}", + "isVisible": true, + "label": "Data", + "searchKey": "", + "version": 1, + "parentId": "0", + "isLoading": false, + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnSizeMap": { + "task": 245, + "step": 62, + "status": 75 + } + }, + { + "widgetName": "Form1", + "backgroundColor": "white", + "rightColumn": 16, + "widgetId": "ut3l54pzqw", + "topRow": 4, + "bottomRow": 11, + "parentRowSpace": 40, + "isVisible": true, + "type": "FORM_WIDGET", + "parentId": "0", + "isLoading": false, + "parentColumnSpace": 77.5, + "leftColumn": 9, + "children": [ + { + "widgetName": "Canvas1", + "rightColumn": 542.5, + "detachFromLayout": true, + "widgetId": "mcsltg1l0j", + "containerStyle": "none", + "topRow": 0, + "bottomRow": 320, + "parentRowSpace": 1, + "isVisible": true, + "canExtend": false, + "type": "CANVAS_WIDGET", + "version": 1, + "parentId": "ut3l54pzqw", + "minHeight": 520, + "isLoading": false, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "widgetName": "Text1", + "rightColumn": 6, + "textAlign": "LEFT", + "widgetId": "7b4x786lxp", + "topRow": 0, + "bottomRow": 1, + "isVisible": true, + "fontStyle": "BOLD", + "type": "TEXT_WIDGET", + "textColor": "#231F20", + "version": 1, + "parentId": "mcsltg1l0j", + "isLoading": false, + "leftColumn": 0, + "fontSize": "HEADING1", + "text": "Form" + }, + { + "widgetName": "Text2", + "rightColumn": 16, + "textAlign": "LEFT", + "widgetId": "d0axuxiosp", + "topRow": 3, + "bottomRow": 6, + "parentRowSpace": 40, + "isVisible": true, + "fontStyle": "BOLD", + "type": "TEXT_WIDGET", + "textColor": "#231F20", + "version": 1, + "parentId": "mcsltg1l0j", + "isLoading": false, + "parentColumnSpace": 31.40625, + "dynamicTriggerPathList": [], + "leftColumn": 0, + "dynamicBindingPathList": [ + { + "key": "text" + } + ], + "fontSize": "PARAGRAPH2", + "text": "{{api_wo_auth.data.body}}" + }, + { + "widgetName": "Text3", + "rightColumn": 4, + "textAlign": "LEFT", + "widgetId": "lmfer0622c", + "topRow": 2, + "bottomRow": 3, + "parentRowSpace": 40, + "isVisible": true, + "fontStyle": "BOLD", + "type": "TEXT_WIDGET", + "textColor": "#231F20", + "version": 1, + "parentId": "mcsltg1l0j", + "isLoading": false, + "parentColumnSpace": 31.40625, + "dynamicTriggerPathList": [], + "leftColumn": 0, + "dynamicBindingPathList": [ + { + "key": "text" + } + ], + "fontSize": "PARAGRAPH", + "text": "{{api_wo_auth.data.id}}" + } + ] + } + ] + } + ] + }, + "layoutOnLoadActions": [ + [ + { + "id": "60aca24c136c4b7178f6790d", + "name": "api_wo_auth", + "pluginType": "API", + "jsonPathKeys": [], + "timeoutInMillisecond": 10000 + }, + { + "id": "60aca092136c4b7178f6790a", + "name": "get_users", + "pluginType": "DB", + "jsonPathKeys": [], + "timeoutInMillisecond": 10000 + } + ] + ], + "new": false + } + ], + "userPermissions": [] + }, + "publishedPage": { + "name": "Page1", + "layouts": [ + { + "id": "60aca056136c4b7178f67906", + "userPermissions": [], + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1254, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 4, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [] + }, + "new": false + } + ], + "userPermissions": [] + }, + "new": true + } + ], + "actionList": [ + { + "id": "60aca092136c4b7178f6790a", + "userPermissions": [], + "applicationId": "valid_application", + "pluginType": "DB", + "pluginId": "mongo-plugin", + "unpublishedAction": { + "name": "get_users", + "datasource": { + "id": "db-auth", + "userPermissions": [], + "isValid": true, + "new": false + }, + "pageId": "Page1", + "actionConfiguration": { + "timeoutInMillisecond": 10000, + "paginationType": "NONE", + "encodeParamsToggle": true, + "body": "{\n \"find\": \"users\",\n \"sort\": {\n \"id\": 1\n },\n \"limit\": 10\n}", + "pluginSpecifiedTemplates": [ + { + "value": false + } + ] + }, + "executeOnLoad": true, + "dynamicBindingPathList": [], + "isValid": true, + "invalids": [], + "jsonPathKeys": [], + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "publishedAction": { + "datasource": { + "userPermissions": [], + "isValid": true, + "new": true + }, + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "new": false + }, + { + "id": "60aca24c136c4b7178f6790d", + "userPermissions": [], + "applicationId": "valid_application", + "pluginType": "API", + "pluginId": "restapi-plugin", + "unpublishedAction": { + "name": "api_wo_auth", + "datasource": { + "id": "api_ds_wo_auth", + "userPermissions": [], + "isValid": true, + "new": false + }, + "pageId": "Page1", + "actionConfiguration": { + "timeoutInMillisecond": 10000, + "paginationType": "NONE", + "path": "/params", + "headers": [ + { + "key": "", + "value": "" + }, + { + "key": "", + "value": "" + } + ], + "encodeParamsToggle": true, + "queryParameters": [], + "body": "", + "httpMethod": "GET", + "pluginSpecifiedTemplates": [ + { + "value": false + } + ] + }, + "executeOnLoad": true, + "dynamicBindingPathList": [], + "isValid": true, + "invalids": [], + "jsonPathKeys": [], + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "publishedAction": { + "datasource": { + "userPermissions": [], + "isValid": true, + "new": true + }, + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "new": false + } + ], + "decryptedFields": { + "db-auth": { + "password": "CreativePassword", + "authType": "com.appsmith.external.models.DBAuth", + "dbAuth": { + "authenticationType": "dbAuth", + "authType": "SCRAM_SHA_1", + "username": "CreativeUser", + "databaseName": "db-name" + } + } + }, + "publishedDefaultPageName": "Page1", + "unpublishedDefaultPageName": "Page1", + "publishedLayoutmongoEscapedWidgets": { + "60aca056136c4b7178f67906": [ + "Table1" + ] + }, + "unpublishedLayoutmongoEscapedWidgets": { + "60aca056136c4b7178f67906": [ + "Table1" + ] + } +} \ No newline at end of file diff --git a/app/server/envs/dev.env.example b/app/server/envs/dev.env.example index 569a50c320..dcfadce836 100644 --- a/app/server/envs/dev.env.example +++ b/app/server/envs/dev.env.example @@ -23,4 +23,4 @@ APPSMITH_CODEC_SIZE=10 #APPSMITH_SENTRY_ENVIRONMENT= #APPSMITH_RECAPTCHA_SITE_KEY="" -#APPSMITH_RECAPTCHA_SECRET_KEY="" +#APPSMITH_RECAPTCHA_SECRET_KEY="" \ No newline at end of file