diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/spans/ce/PageSpanCE.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/spans/ce/PageSpanCE.java index 5934b6b6ae75..e7b23d3a91e9 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/spans/ce/PageSpanCE.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/spans/ce/PageSpanCE.java @@ -8,6 +8,7 @@ public class PageSpanCE { public static final String GET_PAGE_WITHOUT_BRANCH = PAGES + "without_branch"; public static final String GET_PAGE_WITH_BRANCH = PAGES + "with_branch"; public static final String FETCH_PAGE_FROM_DB = PAGES + "fetch_page"; + public static final String FETCH_PAGES_WITH_BASE_ID = PAGES + "fetch_pages_with_base_id"; public static final String FETCH_PAGES_BY_APP_ID_DB = PAGES + "fetch_pages_by_app_id"; public static final String MARK_RECENTLY_ACCESSED_RESOURCES_PAGES = PAGES + "update_recently_accessed_pages"; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/UniqueSlugDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/UniqueSlugDTO.java new file mode 100644 index 000000000000..641474ea38a9 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/UniqueSlugDTO.java @@ -0,0 +1,25 @@ +package com.appsmith.external.dtos; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@RequiredArgsConstructor +public class UniqueSlugDTO { + + String branchedPageId; + + String branchedApplicationId; + + String uniquePageSlug; + + String uniqueApplicationSlug; + + Boolean staticUrlEnabled; + + Boolean isUniqueSlugAvailable; +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java index c6fe73289975..998620279b76 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java @@ -31,6 +31,12 @@ public enum FeatureFlagEnum { release_git_autocommit_eligibility_enabled, release_dynamodb_connection_time_to_live_enabled, release_reactive_actions_enabled, + /** + * Enables static and human-readable URLs for applications and pages. When enabled, Appsmith apps use + * predictable, unique slugs for app and page routes instead of dynamic URLs, + * improving usability, cross-instance navigation, and compatibility with Git. + */ + release_static_url_enabled, /** * Feature flag to enable alphabetical ordering for workspaces and applications */ diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationServiceCE.java index f555f40a996b..553a288acc82 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationServiceCE.java @@ -105,4 +105,9 @@ Mono findByBaseIdBranchNameAndApplicationMode( String defaultApplicationId, String branchName, ApplicationMode mode); Mono findByBranchedApplicationIdAndApplicationMode(String branchedApplicationId, ApplicationMode mode); + + Flux findByUniqueAppName(String uniqueAppName, AclPermission aclPermission); + + Flux findByUniqueAppNameRefNameAndApplicationMode( + String uniqueAppName, String refName, ApplicationMode applicationMode); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationServiceCEImpl.java index 4645820bdd73..d66fda0e60d6 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationServiceCEImpl.java @@ -1050,4 +1050,23 @@ public Mono findByBranchedApplicationIdAndApplicationMode( .switchIfEmpty(Mono.error(new AppsmithException( AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, branchedApplicationId))); } + + @Override + public Flux findByUniqueAppName(String uniqueAppName, AclPermission aclPermission) { + return repository.findByUniqueAppName(uniqueAppName, aclPermission); + } + + @Override + public Flux findByUniqueAppNameRefNameAndApplicationMode( + String uniqueAppName, String refName, ApplicationMode applicationMode) { + AclPermission permissionForApplication = ApplicationMode.PUBLISHED.equals(applicationMode) + ? applicationPermission.getReadPermission() + : applicationPermission.getEditPermission(); + + if (!StringUtils.hasText(refName)) { + return repository.findByUniqueAppName(uniqueAppName, permissionForApplication); + } + + return repository.findByUniqueAppSlugRefName(uniqueAppName, refName, permissionForApplication); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceCEImpl.java index 5b0d33916ce0..8d70710daf67 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceCEImpl.java @@ -36,6 +36,7 @@ import com.appsmith.server.solutions.DatasourcePermission; import com.appsmith.server.solutions.PagePermission; import com.appsmith.server.solutions.WorkspacePermission; +import com.appsmith.server.staticurl.StaticUrlService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; @@ -66,6 +67,7 @@ public class ApplicationImportServiceCEImpl implements ArtifactBasedImportServiceCE { private final ApplicationService applicationService; + protected final StaticUrlService staticUrlService; private final ApplicationPageService applicationPageService; private final NewActionService newActionService; private final UpdateLayoutService updateLayoutService; @@ -397,9 +399,12 @@ public Mono updateAndSaveArtifactInContext( }); if (!StringUtils.hasText(importingMetaDTO.getArtifactId())) { - importApplicationMono = importApplicationMono.flatMap(application -> { - return applicationPageService.createOrUpdateSuffixedApplication(application, application.getName(), 0); - }); + importApplicationMono = importApplicationMono + .flatMap(staticUrlService::generateAndUpdateApplicationSlugForNewImports) + .flatMap(application -> { + return applicationPageService.createOrUpdateSuffixedApplication( + application, application.getName(), 0); + }); } else { Mono existingApplicationMono = applicationService .findById( @@ -432,18 +437,24 @@ public Mono updateAndSaveArtifactInContext( } else { importApplicationMono = importApplicationMono .zipWith(existingApplicationMono) - .map(objects -> { - Application newApplication = objects.getT1(); - Application existingApplication = objects.getT2(); + .map(applicationFromJsonAndDB -> { + Application appFromJson = applicationFromJsonAndDB.getT1(); + Application existingAppFromDB = applicationFromJsonAndDB.getT2(); // This method sets the published mode properties in the imported // application.When a user imports an application from the git repo, // since the git only stores the unpublished version, the current // deployed version in the newly imported app is not updated. // This function sets the initial deployed version to the same as the // edit mode one. - setPublishedApplicationProperties(newApplication); - setPropertiesToExistingApplication(newApplication, existingApplication); - return existingApplication; + setPublishedApplicationProperties(appFromJson); + setPropertiesToExistingApplication(appFromJson, existingAppFromDB); + return existingAppFromDB; + }) + .flatMap(application -> { + return staticUrlService + .generateAndUpdateApplicationSlugForImportsOnExistingApps( + application, application) + .thenReturn(application); }) .flatMap(application -> { Mono parentApplicationMono; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceImpl.java index fa5fe049b317..f156c53c0781 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceImpl.java @@ -20,6 +20,7 @@ import com.appsmith.server.solutions.DatasourcePermission; import com.appsmith.server.solutions.PagePermission; import com.appsmith.server.solutions.WorkspacePermission; +import com.appsmith.server.staticurl.StaticUrlService; import org.springframework.stereotype.Service; @Service @@ -28,6 +29,7 @@ public class ApplicationImportServiceImpl extends ApplicationImportServiceCEImpl public ApplicationImportServiceImpl( ApplicationService applicationService, + StaticUrlService staticUrlService, ApplicationPageService applicationPageService, NewActionService newActionService, UpdateLayoutService updateLayoutService, @@ -44,6 +46,7 @@ public ApplicationImportServiceImpl( ImportableService actionCollectionImportableService) { super( applicationService, + staticUrlService, applicationPageService, newActionService, updateLayoutService, 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 8836312ed8de..60c33b426cea 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 @@ -12,6 +12,7 @@ import com.appsmith.server.services.ApplicationPageService; import com.appsmith.server.services.ApplicationSnapshotService; import com.appsmith.server.solutions.UserReleaseNotes; +import com.appsmith.server.staticurl.StaticUrlService; import com.appsmith.server.themes.base.ThemeService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; @@ -25,6 +26,7 @@ public class ApplicationController extends ApplicationControllerCE { public ApplicationController( ArtifactService artifactService, ApplicationService service, + StaticUrlService staticUrlService, ApplicationPageService applicationPageService, UserReleaseNotes userReleaseNotes, ApplicationForkingService applicationForkingService, @@ -37,6 +39,7 @@ public ApplicationController( super( artifactService, service, + staticUrlService, applicationPageService, userReleaseNotes, applicationForkingService, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java index 56f1e709094a..54d297d0e250 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java @@ -5,6 +5,7 @@ import com.appsmith.server.newpages.base.NewPageService; import com.appsmith.server.services.ApplicationPageService; import com.appsmith.server.solutions.CreateDBTablePageSolution; +import com.appsmith.server.staticurl.StaticUrlService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -17,8 +18,9 @@ public class PageController extends PageControllerCE { public PageController( ApplicationPageService applicationPageService, NewPageService newPageService, - CreateDBTablePageSolution createDBTablePageSolution) { + CreateDBTablePageSolution createDBTablePageSolution, + StaticUrlService staticUrlService) { - super(applicationPageService, newPageService, createDBTablePageSolution); + super(applicationPageService, newPageService, createDBTablePageSolution, staticUrlService); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationControllerCE.java index 382ea25391b3..75f6370fc4c9 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationControllerCE.java @@ -1,5 +1,6 @@ package com.appsmith.server.controllers.ce; +import com.appsmith.external.dtos.UniqueSlugDTO; import com.appsmith.external.models.Datasource; import com.appsmith.external.views.Views; import com.appsmith.server.applications.base.ApplicationService; @@ -31,6 +32,7 @@ import com.appsmith.server.services.ApplicationPageService; import com.appsmith.server.services.ApplicationSnapshotService; import com.appsmith.server.solutions.UserReleaseNotes; +import com.appsmith.server.staticurl.StaticUrlService; import com.appsmith.server.themes.base.ThemeService; import com.fasterxml.jackson.annotation.JsonView; import jakarta.validation.Valid; @@ -66,6 +68,7 @@ public class ApplicationControllerCE { protected final ArtifactService artifactService; protected final ApplicationService service; + protected final StaticUrlService staticUrlService; private final ApplicationPageService applicationPageService; private final UserReleaseNotes userReleaseNotes; private final ApplicationForkingService applicationForkingService; @@ -337,4 +340,47 @@ public Mono> importBlock(@RequestBody Buil .importBuildingBlock(buildingBlockDTO) .map(fetchedResource -> new ResponseDTO<>(HttpStatus.CREATED, fetchedResource)); } + + @JsonView(Views.Public.class) + @GetMapping(value = "/{branchedApplicationId}/static-url/suggest-app-slug") + public Mono> suggestUniqueApplicationSlug(@PathVariable String branchedApplicationId) { + return staticUrlService + .suggestUniqueApplicationSlug(branchedApplicationId) + .map(url -> new ResponseDTO<>(HttpStatus.OK, url)); + } + + @JsonView(Views.Public.class) + @PostMapping(value = "/{branchedApplicationId}/static-url") + public Mono> autoGenerateStaticUrl( + @PathVariable String branchedApplicationId, @RequestBody UniqueSlugDTO uniqueSlugDTO) { + return staticUrlService + .autoGenerateStaticUrl(branchedApplicationId, uniqueSlugDTO) + .map(url -> new ResponseDTO<>(HttpStatus.OK, url)); + } + + @JsonView(Views.Public.class) + @PatchMapping(value = "/{branchedApplicationId}/static-url") + public Mono> updateApplicationSlug( + @PathVariable String branchedApplicationId, @RequestBody UniqueSlugDTO staticUrlDTO) { + return staticUrlService + .updateApplicationSlug(branchedApplicationId, staticUrlDTO) + .map(url -> new ResponseDTO<>(HttpStatus.OK, url)); + } + + @JsonView(Views.Public.class) + @DeleteMapping(value = "/{branchedApplicationId}/static-url") + public Mono> deleteUnique(@PathVariable String branchedApplicationId) { + return staticUrlService + .deleteStaticUrlSettings(branchedApplicationId) + .map(url -> new ResponseDTO<>(HttpStatus.OK, url)); + } + + @JsonView(Views.Public.class) + @GetMapping(value = "/{branchedApplicationId}/static-url/{uniqueSlugName}") + public Mono> isApplicationSlugUnique( + @PathVariable String branchedApplicationId, @PathVariable String uniqueSlugName) { + return staticUrlService + .isApplicationSlugUnique(branchedApplicationId, uniqueSlugName) + .map(url -> new ResponseDTO<>(HttpStatus.OK, url)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/PageControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/PageControllerCE.java index e2363bfcdce4..49fa5062d538 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/PageControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/PageControllerCE.java @@ -1,10 +1,12 @@ package com.appsmith.server.controllers.ce; +import com.appsmith.external.dtos.UniqueSlugDTO; import com.appsmith.external.git.constants.ce.RefType; import com.appsmith.external.views.Views; import com.appsmith.server.constants.FieldName; import com.appsmith.server.constants.Url; import com.appsmith.server.domains.ApplicationMode; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.dtos.ApplicationPagesDTO; import com.appsmith.server.dtos.CRUDPageResourceDTO; import com.appsmith.server.dtos.CRUDPageResponseDTO; @@ -15,6 +17,7 @@ import com.appsmith.server.newpages.base.NewPageService; import com.appsmith.server.services.ApplicationPageService; import com.appsmith.server.solutions.CreateDBTablePageSolution; +import com.appsmith.server.staticurl.StaticUrlService; import com.fasterxml.jackson.annotation.JsonView; import jakarta.validation.Valid; import lombok.NonNull; @@ -23,6 +26,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -44,6 +48,7 @@ public class PageControllerCE { private final ApplicationPageService applicationPageService; private final NewPageService newPageService; private final CreateDBTablePageSolution createDBTablePageSolution; + protected final StaticUrlService staticUrlService; @JsonView(Views.Public.class) @PostMapping @@ -193,4 +198,19 @@ public Mono> updateDependencyMap( .updateDependencyMap(defaultPageId, dependencyMap, RefType.branch, branchName) .map(updatedResource -> new ResponseDTO<>(HttpStatus.OK, updatedResource)); } + + @JsonView(Views.Public.class) + @PatchMapping(value = "/static-url") + public Mono> updatePageSlug(@RequestBody UniqueSlugDTO uniqueSlugDTO) { + return staticUrlService.updatePageSlug(uniqueSlugDTO).map(url -> new ResponseDTO<>(HttpStatus.OK, url)); + } + + @JsonView(Views.Public.class) + @GetMapping(value = "/{branchedPageId}/static-url/verify/{requestedSlug}") + public Mono> isPageSlugUnique( + @PathVariable String branchedPageId, @PathVariable String requestedSlug) { + return staticUrlService + .isPageSlugUnique(branchedPageId, requestedSlug) + .map(url -> new ResponseDTO<>(HttpStatus.OK, url)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java index 4e3aecd74f9b..0dbaad2e4eec 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java @@ -65,6 +65,21 @@ public AppPositioning(AppPositioning.Type type) { } } + /** + * StaticUrlSettings stores the static URL configuration for the application + */ + @Data + @NoArgsConstructor + @FieldNameConstants + @EqualsAndHashCode(callSuper = true) + public static class StaticUrlSettings extends ApplicationCE.StaticUrlSettingsCE { + public StaticUrlSettings(boolean enabled, String uniqueSlug) { + super(enabled, uniqueSlug); + } + + public static class Fields extends ApplicationCE.StaticUrlSettingsCE.Fields {} + } + @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewPage.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewPage.java index 8b050a6db5a5..055a64c5cdaf 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewPage.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewPage.java @@ -28,6 +28,23 @@ public class NewPage extends RefAwareDomain implements Context { @JsonView(Views.Public.class) PageDTO publishedPage; + @JsonView(Views.Internal.class) + public String getUniqueSlugOrFallback(boolean viewMode) { + if (viewMode) { + if (this.publishedPage == null) { + return ""; + } + + return this.publishedPage.getUniqueSlugOrFallback(); + } + + if (this.getUnpublishedPage() == null) { + return ""; + } + + return this.unpublishedPage.getUniqueSlugOrFallback(); + } + @Override public void sanitiseToExportDBObject() { this.setApplicationId(null); @@ -73,6 +90,7 @@ public static class Fields extends RefAwareDomain.Fields { public static String unpublishedPage_isHidden = unpublishedPage + "." + PageDTO.Fields.isHidden; public static String unpublishedPage_slug = unpublishedPage + "." + PageDTO.Fields.slug; public static String unpublishedPage_customSlug = unpublishedPage + "." + PageDTO.Fields.customSlug; + public static String unpublishedPage_uniqueSlug = unpublishedPage + "." + PageDTO.Fields.uniqueSlug; public static String unpublishedPage_deletedAt = unpublishedPage + "." + PageDTO.Fields.deletedAt; public static String unpublishedPage_dependencyMap = unpublishedPage + "." + PageDTO.Fields.dependencyMap; @@ -82,5 +100,6 @@ public static class Fields extends RefAwareDomain.Fields { public static String publishedPage_isHidden = publishedPage + "." + PageDTO.Fields.isHidden; public static String publishedPage_slug = publishedPage + "." + PageDTO.Fields.slug; public static String publishedPage_customSlug = publishedPage + "." + PageDTO.Fields.customSlug; + public static String publishedPage_uniqueSlug = publishedPage + "." + PageDTO.Fields.uniqueSlug; } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationCE.java index 787a85106810..86385013759e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationCE.java @@ -5,6 +5,7 @@ import com.appsmith.external.views.Views; import com.appsmith.server.constants.ArtifactType; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.Application.StaticUrlSettings; import com.appsmith.server.domains.ApplicationDetail; import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.GitArtifactMetadata; @@ -92,6 +93,9 @@ public class ApplicationCE extends BaseDomain implements ArtifactCE { @JsonView(Views.Public.class) private String slug; + @JsonView({Views.Public.class, Views.Export.class, Git.class}) + StaticUrlSettings staticUrlSettings; + @JsonView({Views.Internal.class, Git.class}) Application.AppLayout unpublishedAppLayout; @@ -508,6 +512,24 @@ public enum AppMaxWidth { } } + /** + * StaticUrlSettings stores the static URL configuration for the application + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @FieldNameConstants + public static class StaticUrlSettingsCE implements Serializable { + + @JsonView({Views.Public.class, Views.Export.class, Git.class}) + private Boolean enabled; + + @JsonView({Views.Public.class, Views.Export.class, Git.class}) + private String uniqueSlug; + + public static class Fields {} + } + public static class Fields extends BaseDomain.Fields { public static final String gitApplicationMetadata_gitAuth = dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.gitAuth); @@ -517,9 +539,15 @@ public static class Fields extends BaseDomain.Fields { dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.defaultArtifactId); public static final String gitApplicationMetadata_branchName = dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.branchName); + public static final String gitApplicationMetadata_refName = + dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.refName); + public static final String gitApplicationMetadata_refType = + dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.refType); public static final String gitApplicationMetadata_isRepoPrivate = dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.isRepoPrivate); public static final String gitApplicationMetadata_isProtectedBranch = dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.isProtectedBranch); + public static final String staticUrlSettings_uniqueSlug = + dotted(staticUrlSettings, StaticUrlSettings.Fields.uniqueSlug); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java index 52618c40be5d..6c26e8a7ff2b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java @@ -14,6 +14,7 @@ import lombok.ToString; import lombok.experimental.FieldNameConstants; import org.springframework.data.annotation.Transient; +import org.springframework.util.StringUtils; import java.time.Instant; import java.util.HashMap; @@ -37,6 +38,9 @@ public class PageDTO implements LayoutContainer { @JsonView({Views.Public.class}) private String baseId; + @JsonView({Views.Public.class, Views.Export.class, Git.class}) + String uniqueSlug; + @JsonView({Views.Public.class, Views.Export.class, Git.class}) String name; @@ -115,4 +119,13 @@ public void sanitiseToExportDBObject() { this.setDependencyMap(null); this.getLayouts().forEach(Layout::sanitiseToExportDBObject); } + + @JsonView(Views.Internal.class) + public String getUniqueSlugOrFallback() { + if (StringUtils.hasText(uniqueSlug)) { + return this.uniqueSlug; + } + + return this.slug; + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageNameIdDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageNameIdDTO.java index 7e19bb5e44da..945be905dd79 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageNameIdDTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageNameIdDTO.java @@ -25,6 +25,9 @@ public class PageNameIdDTO { @JsonView(Views.Public.class) String slug; + @JsonView(Views.Public.class) + String uniqueSlug; + @JsonView(Views.Public.class) String customSlug; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java index ab6e454a9c0a..54867acac3c9 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java @@ -1020,6 +1020,14 @@ public enum AppsmithError { "Insufficient password strength", ErrorType.ARGUMENT_ERROR, null), + UNIQUE_SLUG_UNAVAILABLE( + 409, + AppsmithErrorCode.UNIQUE_SLUG_UNAVAILABLE.getCode(), + "Duplicate {0} slug detected. Slug value: {1} is already in use. It must be unique", + AppsmithErrorAction.DEFAULT, + "Slug already in use", + ErrorType.ARGUMENT_ERROR, + null), GIT_ROUTE_METADATA_NOT_FOUND( 404, AppsmithErrorCode.GIT_ROUTE_METADATA_NOT_FOUND.getCode(), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java index 5f11cbf93bff..6f1b759e1ea5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java @@ -131,6 +131,7 @@ public enum AppsmithErrorCode { INVALID_METHOD_LEVEL_ANNOTATION_USAGE("AE-APP-4094", "Invalid usage for custom annotation"), FEATURE_FLAG_MIGRATION_FAILURE("AE-APP-5045", "Feature flag based migration error"), + UNIQUE_SLUG_UNAVAILABLE("AE-APP-5007", "Slug is unavailable"), // Git route related error codes GIT_ROUTE_HANDLER_NOT_FOUND("AE-GIT-5005", "Git route handler not found"), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/fork/internal/ApplicationForkingServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/fork/internal/ApplicationForkingServiceCEImpl.java index c7baf7604b3f..77d8682bb294 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/fork/internal/ApplicationForkingServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/fork/internal/ApplicationForkingServiceCEImpl.java @@ -455,6 +455,7 @@ public Mono forkApplicationToWorkspaceWithEnvironment( // If the forking application is connected to git, do not copy those data to the new forked // application application.setGitApplicationMetadata(null); + application.setStaticUrlSettings(null); boolean allowFork = ( // Is this a non-anonymous user that has access to this application? diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/TextUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/TextUtils.java index b749eb1ee15d..6349fb6dbc89 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/TextUtils.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/TextUtils.java @@ -1,6 +1,7 @@ package com.appsmith.server.helpers; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; import java.text.Normalizer; import java.util.Arrays; @@ -24,6 +25,24 @@ public class TextUtils { */ private static final Pattern SEPARATORS = Pattern.compile("[\\s\\p{Punct}]+"); + /** + * Pattern to recognize mongo style UUIDs. 24 hexadecimal characters. + */ + static final Pattern MONGO_STYLE_UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{24}$"); + + /** + * Pattern to recognize Standard UUIDs. Standard UUID pattern: 8-4-4-4-12 hex groups + */ + static final Pattern STANDARD_UUID_PATTERN = + Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + + /** + * Patter for unique slugs. + */ + static final Pattern ALLOWED_UNIQUE_SLUG_PATTERN = Pattern.compile("^[A-Za-z0-9_-]+$"); + + static final Pattern WHITE_SPACE_PATTERN = Pattern.compile("\\s"); + /** * Creates URL safe text aka slug from the input text. It supports english locale only. * See the test cases for sample conversions @@ -74,4 +93,20 @@ public static Set csvToSet(String inputStringCsv) { public static String generateDefaultRoleNameForResource(String roleType, String resourceName) { return roleType + " - " + resourceName; } + + public static boolean isSlugFormatValid(String slug) { + // This check disallows: + // Null values or Empty Strings + // Any WhiteSpace Characters + // Mongo Style UUIDs + // Standard UUIDs + if (!StringUtils.hasText(slug) + || WHITE_SPACE_PATTERN.matcher(slug).find() + || MONGO_STYLE_UUID_PATTERN.matcher(slug).matches() + || STANDARD_UUID_PATTERN.matcher(slug).matches()) { + return false; + } + + return ALLOWED_UNIQUE_SLUG_PATTERN.matcher(slug).matches(); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration074AddUniqueSlugIndicesForApplicationAndNewPage.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration074AddUniqueSlugIndicesForApplicationAndNewPage.java new file mode 100644 index 000000000000..6a91e15f657f --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration074AddUniqueSlugIndicesForApplicationAndNewPage.java @@ -0,0 +1,93 @@ +package com.appsmith.server.migrations.db.ce; + +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.Application.Fields; +import com.appsmith.server.domains.NewPage; +import io.mongock.api.annotations.ChangeUnit; +import io.mongock.api.annotations.Execution; +import io.mongock.api.annotations.RollbackExecution; +import lombok.extern.slf4j.Slf4j; +import org.bson.Document; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.index.Index; + +import static com.appsmith.server.migrations.DatabaseChangelog1.dropIndexIfExists; +import static com.appsmith.server.migrations.DatabaseChangelog1.ensureIndexes; +import static com.appsmith.server.migrations.DatabaseChangelog1.makeIndex; + +@Slf4j +@ChangeUnit(order = "074", id = "add-idx-unique-slug-on-application-and-newpage", author = " ") +public class Migration074AddUniqueSlugIndicesForApplicationAndNewPage { + + private final MongoTemplate mongoTemplate; + + // Indices name have been cut short due to mongodb index name lenght limitation + public static final String NEWPAGE_APPID_EDIT_UNIQUESLUG_INDEX = "appId_edit_uniqSlug_dltdAt"; + public static final String NEWPAGE_APPID_VIEW_UNIQUESLUG_INDEX = "appId_view_uniqSlug_dltdAt"; + public static final String APPLICATION_UNIQUESLUG_DEFAULTAPPLICATIONID_BRANCH_INDEX = + "defAppId_uniqSlug_brnch_dltdAt"; + + public Migration074AddUniqueSlugIndicesForApplicationAndNewPage(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @RollbackExecution + public void rollbackExecution() {} + + @Execution + public void createIndexes() { + // NewPage: index on applicationId, uniqueSlug, and deletedAt + dropIndexIfExists(mongoTemplate, NewPage.class, NEWPAGE_APPID_EDIT_UNIQUESLUG_INDEX); + Index editModeIndex = makeIndex( + NewPage.Fields.applicationId, + NewPage.Fields.unpublishedPage_uniqueSlug, + NewPage.Fields.unpublishedPage_deletedAt, + NewPage.Fields.deletedAt) + .named(NEWPAGE_APPID_EDIT_UNIQUESLUG_INDEX) + .unique() + .partial(() -> { + Document document = new Document(); + Document condition = new Document(); + condition.put("$exists", true); + document.put(NewPage.Fields.unpublishedPage_uniqueSlug, condition); + return document; + }) + .background(); + + ensureIndexes(mongoTemplate, NewPage.class, editModeIndex); + + dropIndexIfExists(mongoTemplate, NewPage.class, NEWPAGE_APPID_VIEW_UNIQUESLUG_INDEX); + Index viewModeIndex = makeIndex( + NewPage.Fields.applicationId, NewPage.Fields.publishedPage_uniqueSlug, NewPage.Fields.deletedAt) + .named(NEWPAGE_APPID_VIEW_UNIQUESLUG_INDEX) + .unique() + .partial(() -> { + Document document = new Document(); + Document condition = new Document(); + condition.put("$exists", true); + document.put(NewPage.Fields.publishedPage_uniqueSlug, condition); + return document; + }) + .background(); + ensureIndexes(mongoTemplate, NewPage.class, viewModeIndex); + + // Application: index on uniqueSlug, deletedAt, and gitApplicationMetadata.defaultArtifactId + dropIndexIfExists(mongoTemplate, Application.class, APPLICATION_UNIQUESLUG_DEFAULTAPPLICATIONID_BRANCH_INDEX); + Index applicationIndex = makeIndex( + Application.Fields.staticUrlSettings_uniqueSlug, + Application.Fields.deletedAt, + Fields.gitApplicationMetadata_defaultApplicationId, + Fields.gitApplicationMetadata_branchName) + .named(APPLICATION_UNIQUESLUG_DEFAULTAPPLICATIONID_BRANCH_INDEX) + .unique() + .partial(() -> { + Document document = new Document(); + Document condition = new Document(); + condition.put("$exists", true); + document.put(Fields.staticUrlSettings_uniqueSlug, condition); + return document; + }) + .background(); + ensureIndexes(mongoTemplate, Application.class, applicationIndex); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/base/NewPageServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/base/NewPageServiceCE.java index b981ce2516a1..11024c59725b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/base/NewPageServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/base/NewPageServiceCE.java @@ -26,6 +26,8 @@ public interface NewPageServiceCE extends CrudService { Flux findByApplicationId(String applicationId, AclPermission permission, Boolean view); + Flux findByBasePageId(String basePageId, AclPermission permission, List projectedFieldNames); + Flux findNewPagesByApplicationId(String applicationId, AclPermission permission); Flux findNewPagesByApplicationId( @@ -96,4 +98,12 @@ Mono updateDependencyMap( Flux findByApplicationIdAndApplicationMode( String applicationId, AclPermission permission, ApplicationMode applicationMode); + + Mono findByApplicationIdAndUniquePageSlug( + String applicationId, String uniquePageName, ApplicationMode applicationMode); + + Mono findByIdAndApplicationMode(String id, ApplicationMode mode); + + Mono findByApplicationIdAndPageSlug( + String applicationId, String pageSlug, ApplicationMode mode, List projections); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/base/NewPageServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/base/NewPageServiceCEImpl.java index fb32b727ee06..e02887d182f6 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/base/NewPageServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/base/NewPageServiceCEImpl.java @@ -137,6 +137,21 @@ public Flux findByApplicationId(String applicationId, AclPermission per return findNewPagesByApplicationId(applicationId, permission).flatMap(page -> getPageByViewMode(page, view)); } + @Override + public Flux findByBasePageId( + String basePageId, AclPermission permission, List projectedFieldNames) { + return repository.findByBasePageId(basePageId, permission, projectedFieldNames); + } + + @Override + public Mono findByIdAndApplicationMode(String id, ApplicationMode mode) { + AclPermission permission = ApplicationMode.PUBLISHED.equals(mode) + ? pagePermission.getReadPermission() + : pagePermission.getEditPermission(); + + return findById(id, permission); + } + @Override public Mono saveUnpublishedPage(PageDTO page) { @@ -344,6 +359,8 @@ public ApplicationPagesDTO getApplicationPagesDTO( Application application, List newPages, boolean viewMode) { String homePageId = getHomePageId(application, viewMode); + boolean isStaticUrlEnabled = application.getStaticUrlSettings() != null + && Boolean.TRUE.equals(application.getStaticUrlSettings().getEnabled()); List pageNameIdDTOList = new ArrayList<>(); List applicationPages = application.getPages(); List publishedApplicationPages = application.getPublishedPages(); @@ -363,7 +380,7 @@ public ApplicationPagesDTO getApplicationPagesDTO( } for (NewPage pageFromDb : newPages) { - PageNameIdDTO pageNameIdDTO = getPageNameIdDTO(pageFromDb, homePageId, viewMode); + PageNameIdDTO pageNameIdDTO = getPageNameIdDTO(pageFromDb, homePageId, viewMode, isStaticUrlEnabled); pageNameIdDTOList.add(pageNameIdDTO); } @@ -384,7 +401,8 @@ public ApplicationPagesDTO getApplicationPagesDTO( return applicationPagesDTO; } - private static PageNameIdDTO getPageNameIdDTO(NewPage pageFromDb, String homePageId, boolean viewMode) { + private static PageNameIdDTO getPageNameIdDTO( + NewPage pageFromDb, String homePageId, boolean viewMode, boolean isStaticUrlEnabled) { PageNameIdDTO pageNameIdDTO = new PageNameIdDTO(); pageNameIdDTO.setId(pageFromDb.getId()); pageNameIdDTO.setBaseId(pageFromDb.getBaseIdOrFallback()); @@ -402,6 +420,10 @@ private static PageNameIdDTO getPageNameIdDTO(NewPage pageFromDb, String homePag pageDTO = pageFromDb.getUnpublishedPage(); } + if (isStaticUrlEnabled) { + pageNameIdDTO.setUniqueSlug(pageFromDb.getUniqueSlugOrFallback(viewMode)); + } + pageNameIdDTO.setName(pageDTO.getName()); pageNameIdDTO.setIsHidden(pageDTO.getIsHidden()); pageNameIdDTO.setSlug(pageDTO.getSlug()); @@ -662,4 +684,26 @@ public Flux findByApplicationIdAndApplicationMode( }) .flatMap(page -> getPageByViewMode(page, viewMode)); } + + @Override + public Mono findByApplicationIdAndUniquePageSlug( + String applicationId, String uniquePageName, ApplicationMode applicationMode) { + AclPermission permission = ApplicationMode.PUBLISHED.equals(applicationMode) + ? pagePermission.getReadPermission() + : pagePermission.getEditPermission(); + + boolean viewMode = ApplicationMode.PUBLISHED.equals(applicationMode); + return repository.findByUniquePageSlug(applicationId, uniquePageName, permission, viewMode); + } + + @Override + public Mono findByApplicationIdAndPageSlug( + String applicationId, String pageSlug, ApplicationMode mode, List projections) { + boolean viewMode = ApplicationMode.PUBLISHED.equals(mode); + AclPermission permission = ApplicationMode.PUBLISHED.equals(mode) + ? pagePermission.getReadPermission() + : pagePermission.getEditPermission(); + + return repository.findByApplicationIdAndPageSlug(applicationId, pageSlug, viewMode, permission, projections); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/importable/NewPageImportableServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/importable/NewPageImportableServiceCEImpl.java index 164b520acdae..1b63c7bbe34f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/importable/NewPageImportableServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/importable/NewPageImportableServiceCEImpl.java @@ -16,12 +16,14 @@ import com.appsmith.server.dtos.MappedImportableResourcesDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.GitUtils; import com.appsmith.server.helpers.TextUtils; import com.appsmith.server.imports.importable.ImportableServiceCE; import com.appsmith.server.imports.importable.artifactbased.ArtifactBasedImportableService; import com.appsmith.server.newactions.base.NewActionService; import com.appsmith.server.newpages.base.NewPageService; import com.appsmith.server.services.ApplicationPageService; +import com.appsmith.server.staticurl.StaticUrlService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; @@ -30,10 +32,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -45,12 +45,14 @@ import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties; import static com.appsmith.server.constants.ResourceModes.EDIT; import static com.appsmith.server.constants.ResourceModes.VIEW; +import static org.springframework.util.StringUtils.hasText; @RequiredArgsConstructor @Slf4j public class NewPageImportableServiceCEImpl implements ImportableServiceCE { private final NewPageService newPageService; + protected final StaticUrlService staticUrlService; private final ApplicationPageService applicationPageService; private final NewActionService newActionService; @@ -72,42 +74,57 @@ public Mono importEntities( ArtifactExchangeJson artifactExchangeJson) { ApplicationJson applicationJson = (ApplicationJson) artifactExchangeJson; - - List importedNewPageList = applicationJson.getPageList(); + List pagesToImport = applicationJson.getPageList(); // Import and save pages, also update the pages related fields in saved application - assert importedNewPageList != null : "Unable to find pages in the imported application"; + assert pagesToImport != null : "Unable to find pages in the imported application"; + + Mono importableArtifactMonoCached = importableArtifactMono.cache(); // For git-sync this will not be empty - Mono> existingPagesMono = importableArtifactMono + Mono> dbPagesFromCurrentAppMono = importableArtifactMonoCached .flatMap(application -> newPageService .findNewPagesByApplicationId(application.getId(), null) .collectList()) .cache(); - Mono, Map>> importedNewPagesMono = getImportNewPagesMono( - importedNewPageList, - existingPagesMono, - importableArtifactMono, - importingMetaDTO, - mappedImportableResourcesDTO) + Mono, Map>> importedNewPagesMono = importableArtifactMonoCached + .zipWith(dbPagesFromCurrentAppMono) + .flatMap(artifactAndDbPagesTuple -> { + Artifact artifact = artifactAndDbPagesTuple.getT1(); + List dbPagesFromCurrentApp = artifactAndDbPagesTuple.getT2(); + return getImportNewPagesMono( + pagesToImport, + dbPagesFromCurrentApp, + artifact, + importingMetaDTO, + mappedImportableResourcesDTO); + }) .cache(); - Mono> pageNameMapMono = - getPageNameMapMono(importedNewPagesMono).cache(); - - Mono updatedApplicationMono = savePagesToApplicationMono( - applicationJson.getExportedApplication(), - pageNameMapMono, - importableArtifactMono, - importingMetaDTO.getAppendToArtifact(), - importingMetaDTO.getArtifactId(), - existingPagesMono, - importedNewPagesMono, - mappedImportableResourcesDTO) + Mono updatedApplicationMono = Mono.zip( + importableArtifactMonoCached, dbPagesFromCurrentAppMono, importedNewPagesMono) + .flatMap(objects -> { + Application application = (Application) objects.getT1(); + List dbPagesFromCurrentApp = objects.getT2(); + List importedPages = objects.getT3().getT1(); + Map newNamesForClashingPageNames = + objects.getT3().getT2(); + Map pageNameToPageMap = getPageNameToPage(importedPages); + return savePagesToApplicationMono( + importingMetaDTO.getAppendToArtifact(), + importingMetaDTO.getArtifactId(), + ((ApplicationJson) artifactExchangeJson).getExportedApplication(), + application, + dbPagesFromCurrentApp, + importedPages, + pageNameToPageMap, + newNamesForClashingPageNames, + mappedImportableResourcesDTO); + }) .cache(); - return updatedApplicationMono.then(importedNewPagesMono).then(); + return updatedApplicationMono.then(); } @Override @@ -144,74 +161,61 @@ public Mono updateImportedEntities( }); } - private Mono> getPageNameMapMono( - Mono, Map>> importedNewPagesMono) { - return importedNewPagesMono.map(objects -> { - Map pageNameMap = new HashMap<>(); - objects.getT1().forEach(newPage -> { - // Save the map of pageName and NewPage - if (newPage.getUnpublishedPage() != null - && newPage.getUnpublishedPage().getName() != null) { - pageNameMap.put(newPage.getUnpublishedPage().getName(), newPage); - } - if (newPage.getPublishedPage() != null - && newPage.getPublishedPage().getName() != null) { - pageNameMap.put(newPage.getPublishedPage().getName(), newPage); - } - }); - return pageNameMap; - }); + private Map getPageNameToPage(List importedNewPages) { + Map pageNameMap = new HashMap<>(); + for (NewPage newPage : importedNewPages) { + // Save the map of pageName and NewPage + if (newPage.getUnpublishedPage() != null + && newPage.getUnpublishedPage().getName() != null) { + pageNameMap.put(newPage.getUnpublishedPage().getName(), newPage); + } + + if (newPage.getPublishedPage() != null && newPage.getPublishedPage().getName() != null) { + pageNameMap.put(newPage.getPublishedPage().getName(), newPage); + } + } + + return pageNameMap; } private Mono, Map>> getImportNewPagesMono( - List importedNewPageList, - Mono> existingPagesMono, - Mono importableArtifactMono, + List pagesToImport, + List dbPagesFromCurrentApp, + Artifact importedApplication, ImportingMetaDTO importingMetaDTO, MappedImportableResourcesDTO mappedImportableResourcesDTO) { - return Mono.just(importedNewPageList) - .zipWith(existingPagesMono) - .map(objects -> { - List importedNewPages = objects.getT1(); - List existingPages = objects.getT2(); - Map newToOldNameMap; - if (importingMetaDTO.getAppendToArtifact()) { - newToOldNameMap = updateNewPagesBeforeMerge(existingPages, importedNewPages); - } else { - newToOldNameMap = Map.of(); - } - mappedImportableResourcesDTO.setContextNewNameToOldName(newToOldNameMap); - return Tuples.of(importedNewPages, newToOldNameMap); - }) - .zipWith(importableArtifactMono) - .flatMap(objects -> { - List importedNewPages = objects.getT1().getT1(); - Map newToOldNameMap = objects.getT1().getT2(); - Application application = (Application) objects.getT2(); - return importAndSavePages(importedNewPages, application, importingMetaDTO, existingPagesMono) - .collectList() - .zipWith(Mono.just(newToOldNameMap)); + // maps new names with old names + Map newNameForClashingPageNames = new HashMap<>(); + if (importingMetaDTO.getAppendToArtifact()) { + updateNameForClashingPages(newNameForClashingPageNames, dbPagesFromCurrentApp, pagesToImport); + } + + mappedImportableResourcesDTO.setContextNewNameToOldName(newNameForClashingPageNames); + Application application = (Application) importedApplication; + return importAndSavePages(pagesToImport, application, importingMetaDTO, dbPagesFromCurrentApp) + .collectList() + .elapsed() + .map(objects -> { + log.info("time to import {} pages: {}", objects.getT2().size(), objects.getT1()); + return objects.getT2(); }) + .zipWith(Mono.just(newNameForClashingPageNames)) .onErrorResume(throwable -> { log.error("Error importing pages", throwable); return Mono.error(throwable); - }) - .elapsed() - .map(objects -> { - log.debug("time to import {} pages: {}", objects.getT2().size(), objects.getT1()); - return objects.getT2(); }); } Mono savePagesToApplicationMono( - Application importedApplication, - Mono> pageNameMapMono, - Mono applicationMono, boolean appendToApp, String applicationId, - Mono> existingPagesMono, - Mono, Map>> importedNewPagesMono, + Application applicationFromJson, + Application importedApplication, + List dbPagesFromCurrentApp, + List importedPages, + Map pageNameToPageMap, + Map newNameForClashingPageNames, MappedImportableResourcesDTO mappedImportableResourcesDTO) { /** @@ -225,9 +229,9 @@ Mono savePagesToApplicationMono( // this conditional is being placed just for compatibility of the PR #29691 if (CollectionUtils.isEmpty(editModeApplicationPages)) { - editModeApplicationPages = CollectionUtils.isEmpty(importedApplication.getPages()) + editModeApplicationPages = CollectionUtils.isEmpty(applicationFromJson.getPages()) ? new ArrayList<>() - : importedApplication.getPages(); + : applicationFromJson.getPages(); } List publishedModeApplicationPages = (List) mappedImportableResourcesDTO @@ -236,140 +240,126 @@ Mono savePagesToApplicationMono( // this conditional is being placed just for compatibility of the PR #29691 if (CollectionUtils.isEmpty(publishedModeApplicationPages)) { - publishedModeApplicationPages = CollectionUtils.isEmpty(importedApplication.getPublishedPages()) + publishedModeApplicationPages = CollectionUtils.isEmpty(applicationFromJson.getPublishedPages()) ? new ArrayList<>() - : importedApplication.getPublishedPages(); + : applicationFromJson.getPublishedPages(); } - Mono> unpublishedPagesMono = - importUnpublishedPages(editModeApplicationPages, appendToApp, applicationMono, importedNewPagesMono); + if (appendToApp) { + editModeApplicationPages = updateEditModeApplicationPageNamesOldToNew( + editModeApplicationPages, importedPages, newNameForClashingPageNames, importedApplication); + } - Mono> publishedPagesMono = Mono.just(publishedModeApplicationPages); + mappedImportableResourcesDTO.setContextMap(pageNameToPageMap); + log.info("New pages imported for application: {}", importedApplication.getId()); + + Map> applicationPages = new HashMap<>(); + applicationPages.put(EDIT, editModeApplicationPages); + applicationPages.put(VIEW, publishedModeApplicationPages); + + Iterator unpublishedPageItr = editModeApplicationPages.iterator(); + while (unpublishedPageItr.hasNext()) { + ApplicationPage applicationPage = unpublishedPageItr.next(); + NewPage newPage = pageNameToPageMap.get(applicationPage.getId()); + if (newPage == null) { + if (appendToApp) { + // Don't remove the page reference if doing the partial import and appending + // to the existing application + continue; + } - Mono>> applicationPagesMono = Mono.zip( - unpublishedPagesMono, publishedPagesMono, pageNameMapMono, applicationMono) - .map(objects -> { - List unpublishedPages = objects.getT1(); - List publishedPages = objects.getT2(); - Map pageNameMap = objects.getT3(); - Application savedApp = (Application) objects.getT4(); - - mappedImportableResourcesDTO.setContextMap(pageNameMap); - - log.debug("New pages imported for application: {}", savedApp.getId()); - Map> applicationPages = new HashMap<>(); - applicationPages.put(EDIT, unpublishedPages); - applicationPages.put(VIEW, publishedPages); - - Iterator unpublishedPageItr = unpublishedPages.iterator(); - while (unpublishedPageItr.hasNext()) { - ApplicationPage applicationPage = unpublishedPageItr.next(); - NewPage newPage = pageNameMap.get(applicationPage.getId()); - if (newPage == null) { - if (appendToApp) { - // Don't remove the page reference if doing the partial import and appending - // to the existing application - continue; - } - log.debug( - "Unable to find the page during import for appId {}, with name {}", - applicationId, - applicationPage.getId()); - unpublishedPageItr.remove(); - } else { - applicationPage.setId(newPage.getId()); - applicationPage.setDefaultPageId(newPage.getBaseId()); - // Keep the existing page as the default one - if (appendToApp) { - applicationPage.setIsDefault(false); - } - } - } + log.info( + "Unable to find the page during import for appId {}, with name {}", + applicationId, + applicationPage.getId()); + unpublishedPageItr.remove(); + } else { + applicationPage.setId(newPage.getId()); + applicationPage.setDefaultPageId(newPage.getBaseId()); + // Keep the existing page as the default one + if (appendToApp) { + applicationPage.setIsDefault(false); + } + } + } - Iterator publishedPagesItr; - // Remove the newly added pages from merge app flow. Keep only the existing page from the old app - if (appendToApp) { - List existingPagesId = savedApp.getPublishedPages().stream() - .map(applicationPage -> applicationPage.getId()) - .collect(Collectors.toList()); - List publishedApplicationPages = publishedPages.stream() - .filter(applicationPage -> existingPagesId.contains(applicationPage.getId())) - .collect(Collectors.toList()); - applicationPages.replace(VIEW, publishedApplicationPages); - publishedPagesItr = publishedApplicationPages.iterator(); - } else { - publishedPagesItr = publishedPages.iterator(); - } - while (publishedPagesItr.hasNext()) { - ApplicationPage applicationPage = publishedPagesItr.next(); - NewPage newPage = pageNameMap.get(applicationPage.getId()); - if (newPage == null) { - log.debug( - "Unable to find the page during import for appId {}, with name {}", - applicationId, - applicationPage.getId()); - if (!appendToApp) { - publishedPagesItr.remove(); - } - } else { - applicationPage.setId(newPage.getId()); - applicationPage.setDefaultPageId(newPage.getBaseId()); - if (appendToApp) { - applicationPage.setIsDefault(false); - } - } - } + Iterator publishedPagesItr; + // Remove the newly added pages from merge app flow. Keep only the existing page from the old app + if (appendToApp) { + Set existingPagesId = importedApplication.getPublishedPages().stream() + .map(applicationPage -> applicationPage.getId()) + .collect(Collectors.toSet()); - return applicationPages; - }); + List publishedApplicationPages = publishedModeApplicationPages.stream() + .filter(applicationPage -> existingPagesId.contains(applicationPage.getId())) + .collect(Collectors.toList()); - if (!StringUtils.isEmpty(applicationId) && !appendToApp) { - applicationPagesMono = applicationPagesMono - .zipWith(existingPagesMono) - .flatMap(objects -> { - Map> applicationPages = objects.getT1(); - List existingPagesList = objects.getT2(); - Set validPageIds = applicationPages.get(EDIT).stream() - .map(ApplicationPage::getId) - .collect(Collectors.toSet()); - - validPageIds.addAll(applicationPages.get(VIEW).stream() - .map(ApplicationPage::getId) - .collect(Collectors.toSet())); - - Set invalidPageIds = new HashSet<>(); - for (NewPage newPage : existingPagesList) { - if (!validPageIds.contains(newPage.getId())) { - invalidPageIds.add(newPage.getId()); - } - } + applicationPages.replace(VIEW, publishedApplicationPages); + publishedPagesItr = publishedApplicationPages.iterator(); + } else { + publishedPagesItr = publishedModeApplicationPages.iterator(); + } - // Delete the pages which were removed during git merge operation - // This does not apply to the traditional import via file approach - return Flux.fromIterable(invalidPageIds) - .flatMap(pageId -> { - return applicationPageService.deleteUnpublishedPage(pageId, null, null, null, null); - }) - .flatMap(page -> newPageService - .archiveByIdWithoutPermission(page.getId()) - .onErrorResume(e -> { - log.debug( - "Unable to archive page {} with error {}", - page.getId(), - e.getMessage()); - return Mono.empty(); - })) - .then() - .thenReturn(applicationPages); - }); + while (publishedPagesItr.hasNext()) { + ApplicationPage applicationPage = publishedPagesItr.next(); + NewPage newPage = pageNameToPageMap.get(applicationPage.getId()); + if (newPage == null) { + log.info( + "Unable to find the page during import for appId {}, with name {}", + applicationId, + applicationPage.getId()); + if (!appendToApp) { + publishedPagesItr.remove(); + } + } else { + applicationPage.setId(newPage.getId()); + applicationPage.setDefaultPageId(newPage.getBaseId()); + if (appendToApp) { + applicationPage.setIsDefault(false); + } + } } - return applicationMono.zipWith(applicationPagesMono).map(objects -> { - Application application = (Application) objects.getT1(); - Map> applicationPages = objects.getT2(); - application.setPages(applicationPages.get(EDIT)); - application.setPublishedPages(applicationPages.get(VIEW)); - return application; - }); + + // Normal file import or partial import + if (!hasText(applicationId) || appendToApp) { + importedApplication.setPages(applicationPages.get(EDIT)); + importedApplication.setPublishedPages(applicationPages.get(VIEW)); + return Mono.just(importedApplication); + } + + // Git applications + + Set validPageIds = + applicationPages.get(EDIT).stream().map(ApplicationPage::getId).collect(Collectors.toSet()); + + validPageIds.addAll( + applicationPages.get(VIEW).stream().map(ApplicationPage::getId).collect(Collectors.toSet())); + + Set invalidPageIds = new HashSet<>(); + for (NewPage newPage : dbPagesFromCurrentApp) { + if (!validPageIds.contains(newPage.getId())) { + invalidPageIds.add(newPage.getId()); + } + } + + // Delete the pages which were removed during git merge operation + // This does not apply to the traditional import via file approach + return Flux.fromIterable(invalidPageIds) + .flatMap(pageId -> { + return applicationPageService.deleteUnpublishedPage(pageId, null, null, null, null); + }) + .flatMap(page -> newPageService + .archiveByIdWithoutPermission(page.getId()) + .onErrorResume(e -> { + log.debug("Unable to archive page {} with error {}", page.getId(), e.getMessage()); + return Mono.empty(); + })) + .collectList() + .map(deletedPageList -> { + importedApplication.setPages(applicationPages.get(EDIT)); + importedApplication.setPublishedPages(applicationPages.get(VIEW)); + return importedApplication; + }); } /** @@ -379,22 +369,24 @@ Mono savePagesToApplicationMono( * - set the policies for the page * - update default resource ids along with branch-name if the application is connected to git * - * @param pages pagelist extracted from the imported JSON file - * @param application saved application where pages needs to be added - * @param existingPages existing pages in DB if the application is connected to git + * @param pagesToImport pagelist extracted from the imported JSON file + * @param importedApplication saved application where pages needs to be added + * @param dbPagesFromCurrentApp existing pages in DB if the application is connected to git * @return flux of saved pages in DB */ private Flux importAndSavePages( - List pages, - Application application, + List pagesToImport, + Application importedApplication, ImportingMetaDTO importingMetaDTO, - Mono> existingPages) { + List dbPagesFromCurrentApp) { + + final String createPage = "create page"; Map oldToNewLayoutIds = new HashMap<>(); - pages.forEach(newPage -> { - newPage.setApplicationId(application.getId()); + pagesToImport.forEach(newPage -> { + newPage.setApplicationId(importedApplication.getId()); if (newPage.getUnpublishedPage() != null) { - applicationPageService.generateAndSetPagePolicies(application, newPage.getUnpublishedPage()); + applicationPageService.generateAndSetPagePolicies(importedApplication, newPage.getUnpublishedPage()); newPage.setPolicies(newPage.getUnpublishedPage().getPolicies()); newPage.getUnpublishedPage().getLayouts().forEach(layout -> { String layoutId = new ObjectId().toString(); @@ -404,7 +396,7 @@ private Flux importAndSavePages( } if (newPage.getPublishedPage() != null) { - applicationPageService.generateAndSetPagePolicies(application, newPage.getPublishedPage()); + applicationPageService.generateAndSetPagePolicies(importedApplication, newPage.getPublishedPage()); newPage.getPublishedPage().getLayouts().forEach(layout -> { String layoutId = oldToNewLayoutIds.containsKey(layout.getId()) ? oldToNewLayoutIds.get(layout.getId()) @@ -414,90 +406,127 @@ private Flux importAndSavePages( } }); - Mono> pagesInOtherBranchesMono; - if (application.getGitArtifactMetadata() != null) { - pagesInOtherBranchesMono = newPageService - .findAllByApplicationIds(importingMetaDTO.getBranchedArtifactIds(), null) - .filter(newAction -> newAction.getGitSyncId() != null) - .collectMap(NewPage::getGitSyncId) - .cache(); - } else { - pagesInOtherBranchesMono = Mono.just(Collections.emptyMap()); + Mono> pagesWithUpdatedUniqueSlugMono = staticUrlService.updateUniquePageSlugsBeforeImport( + pagesToImport, dbPagesFromCurrentApp, importedApplication); + + Mono hasPageCreatePermissionMonoCached = importingMetaDTO + .getPermissionProvider() + .canCreatePage(importedApplication) + .cache(); + + // If not connected to git, let's go ahead and import pages directly + if (!GitUtils.isArtifactConnectedToGit(importedApplication.getGitArtifactMetadata())) { + return hasPageCreatePermissionMonoCached + .zipWith(pagesWithUpdatedUniqueSlugMono) + .flatMapMany(tuple2 -> { + Boolean canCreatePages = tuple2.getT1(); + List updatedSlugpagesToImport = tuple2.getT2(); + if (!canCreatePages) { + log.error( + "User does not have permission to create pages in application with id: {}", + importedApplication.getId()); + return Mono.error( + new AppsmithException(AppsmithError.ACTION_IS_NOT_AUTHORIZED, createPage)); + } + + return insertPagesInBulkFlux(updatedSlugpagesToImport, importingMetaDTO); + }); } - return existingPages - .zipWith(pagesInOtherBranchesMono) - .flatMapMany(pageTuples -> { - List existingSavedPages = pageTuples.getT1(); - Map pagesFromOtherBranches = pageTuples.getT2(); - Map savedPagesGitIdToPageMap = new HashMap<>(); - existingSavedPages.stream() - .filter(newPage -> !StringUtils.isEmpty(newPage.getGitSyncId())) - .forEach(newPage -> savedPagesGitIdToPageMap.put(newPage.getGitSyncId(), newPage)); - - return Flux.fromIterable(pages).flatMap(newPage -> { - log.debug( + // Git Applications only + Mono> gitSyncToPagesFromAllBranchesMono = newPageService + .findAllByApplicationIds(importingMetaDTO.getBranchedArtifactIds(), null) + .filter(page -> page.getGitSyncId() != null) + .collectMap(NewPage::getGitSyncId) + .cache(); + + return Mono.zip( + gitSyncToPagesFromAllBranchesMono, + pagesWithUpdatedUniqueSlugMono, + hasPageCreatePermissionMonoCached) + .flatMapMany(tuple3 -> { + List updatedSlugpagesToImport = tuple3.getT2(); + Boolean canCreatePage = tuple3.getT3(); + + Map gitSyncToPagesFromAllBranches = tuple3.getT1(); + Map gitSyncToDbPagesFromCurrentApp = new HashMap<>(); + + dbPagesFromCurrentApp.stream() + .filter(pageFromDb -> !StringUtils.isEmpty(pageFromDb.getGitSyncId())) + .forEach(pageFromDb -> + gitSyncToDbPagesFromCurrentApp.put(pageFromDb.getGitSyncId(), pageFromDb)); + + return Flux.fromIterable(updatedSlugpagesToImport).flatMap(pageToImport -> { + log.info( "Importing page: {}", - newPage.getUnpublishedPage().getName()); - // Check if the page has gitSyncId and if it's already in DB - if (newPage.getGitSyncId() != null - && savedPagesGitIdToPageMap.containsKey(newPage.getGitSyncId())) { - // Since the resource is already present in DB, just update resource - NewPage existingPage = savedPagesGitIdToPageMap.get(newPage.getGitSyncId()); - if (!importingMetaDTO.getPermissionProvider().hasEditPermission(existingPage)) { + pageToImport.getUnpublishedPage().getName()); + String gitSyncId = pageToImport.getGitSyncId(); + + // If git sync id of page to import doesn't match any existing page, insert this into db + if (!hasText(gitSyncId) + || (!gitSyncToDbPagesFromCurrentApp.containsKey(gitSyncId) + && !gitSyncToPagesFromAllBranches.containsKey(gitSyncId))) { + // Insert this resource + if (!canCreatePage) { + log.error( + "User does not have permission to create page in application with id: {}", + importedApplication.getId()); + return Mono.error( + new AppsmithException(AppsmithError.ACTION_IS_NOT_AUTHORIZED, createPage)); + } + + return saveNewPageAndUpdateBaseId(pageToImport, importingMetaDTO); + } + + // check if the current app pages has this git sync id present + if (gitSyncToDbPagesFromCurrentApp.containsKey(pageToImport.getGitSyncId())) { + // Page from current app matches this resource, updating the page from app + NewPage existingPage = gitSyncToDbPagesFromCurrentApp.get(pageToImport.getGitSyncId()); + boolean canEditPage = + importingMetaDTO.getPermissionProvider().hasEditPermission(existingPage); + if (!canEditPage) { log.error( "User does not have permission to edit page with id: {}", existingPage.getId()); return Mono.error(new AppsmithException( AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE, existingPage.getId())); } + Set existingPagePolicy = existingPage.getPolicies(); - copyNestedNonNullProperties(newPage, existingPage); + copyNestedNonNullProperties(pageToImport, existingPage); + staticUrlService.deleteUniqueSlugFromDbWhenAbsentFromPageJson(pageToImport, existingPage); // Update branchName existingPage.setRefType(importingMetaDTO.getRefType()); existingPage.setRefName(importingMetaDTO.getRefName()); // Recover the deleted state present in DB from imported page existingPage .getUnpublishedPage() - .setDeletedAt(newPage.getUnpublishedPage().getDeletedAt()); - existingPage.setDeletedAt(newPage.getDeletedAt()); + .setDeletedAt( + pageToImport.getUnpublishedPage().getDeletedAt()); + existingPage.setDeletedAt(pageToImport.getDeletedAt()); existingPage.setPolicies(existingPagePolicy); return newPageService.save(existingPage); } + // check if user has permission to add new page to the application - return importingMetaDTO - .getPermissionProvider() - .canCreatePage(application) - .flatMap(canCreatePage -> { - if (!canCreatePage) { - log.error( - "User does not have permission to create page in application with id: {}", - application.getId()); - return Mono.error(new AppsmithException( - AppsmithError.ACL_NO_RESOURCE_FOUND, - FieldName.APPLICATION, - application.getId())); - } - if (application.getGitApplicationMetadata() != null) { - - if (!pagesFromOtherBranches.containsKey(newPage.getGitSyncId())) { - return saveNewPageAndUpdateBaseId(newPage, importingMetaDTO); - } - - NewPage branchedPage = pagesFromOtherBranches.get(newPage.getGitSyncId()); - newPage.setBaseId(branchedPage.getBaseId()); - newPage.setRefType(importingMetaDTO.getRefType()); - newPage.setRefName(importingMetaDTO.getRefName()); - newPage.getUnpublishedPage() - .setDeletedAt(branchedPage - .getUnpublishedPage() - .getDeletedAt()); - newPage.setDeletedAt(branchedPage.getDeletedAt()); - // Set policies from existing branch object - newPage.setPolicies(branchedPage.getPolicies()); - return newPageService.save(newPage); - } - return saveNewPageAndUpdateBaseId(newPage, importingMetaDTO); - }); + if (!canCreatePage) { + log.error( + "User does not have permission to create page in application with id: {}", + importedApplication.getId()); + return Mono.error( + new AppsmithException(AppsmithError.ACTION_IS_NOT_AUTHORIZED, "create page")); + } + + NewPage branchedPage = gitSyncToPagesFromAllBranches.get(pageToImport.getGitSyncId()); + pageToImport.setBaseId(branchedPage.getBaseId()); + pageToImport.setRefType(importingMetaDTO.getRefType()); + pageToImport.setRefName(importingMetaDTO.getRefName()); + pageToImport + .getUnpublishedPage() + .setDeletedAt(branchedPage.getUnpublishedPage().getDeletedAt()); + pageToImport.setDeletedAt(branchedPage.getDeletedAt()); + // Set policies from existing branch object + pageToImport.setPolicies(branchedPage.getPolicies()); + return newPageService.save(pageToImport); }); }) .onErrorResume(error -> { @@ -520,17 +549,35 @@ private Mono saveNewPageAndUpdateBaseId(NewPage newPage, ImportingMetaD }); } - private Map updateNewPagesBeforeMerge(List existingPages, List importedPages) { - Map newToOldToPageNameMap = new HashMap<>(); // maps new names with old names + private Flux insertPagesInBulkFlux(List pages, ImportingMetaDTO importingMetaDTO) { + pages.forEach(page -> { + page.setRefType(importingMetaDTO.getRefType()); + page.setRefName(importingMetaDTO.getRefName()); + }); + return newPageService.saveAll(pages).flatMap(page -> { + NewPage update = new NewPage(); + if (StringUtils.isEmpty(page.getBaseId())) { + update.setBaseId(page.getId()); + return newPageService.update(page.getId(), update); + } else { + return Mono.just(page); + } + }); + } + + private void updateNameForClashingPages( + Map newToOldPageName, + List dbPagesFromCurrentApp, + List pagesToBeImported) { // get a list of unpublished page names that already exists - List unpublishedPageNames = existingPages.stream() + Set unpublishedPageNames = dbPagesFromCurrentApp.stream() .filter(newPage -> newPage.getUnpublishedPage() != null) .map(newPage -> newPage.getUnpublishedPage().getName()) - .collect(Collectors.toList()); + .collect(Collectors.toSet()); - // modify each new page - for (NewPage newPage : importedPages) { + // modify each page to be imported + for (NewPage newPage : pagesToBeImported) { newPage.setPublishedPage(null); // we'll not merge published pages so removing this // let's check if page name conflicts, rename in that case @@ -544,47 +591,33 @@ private Map updateNewPagesBeforeMerge(List existingPage } newPage.getUnpublishedPage().setName(newPageName); // set new name. may be same as before or not newPage.getUnpublishedPage().setSlug(TextUtils.makeSlug(newPageName)); // set the slug also - newToOldToPageNameMap.put(newPageName, oldPageName); // map: new name -> old name + newToOldPageName.put(newPageName, oldPageName); // map: new name -> old name } - return newToOldToPageNameMap; } - private Mono> importUnpublishedPages( + private List updateEditModeApplicationPageNamesOldToNew( List editModeApplicationPages, - boolean appendToApp, - Mono importApplicationMono, - Mono, Map>> importedNewPagesMono) { - Mono> unpublishedPagesMono = Mono.just(editModeApplicationPages); - if (appendToApp) { - unpublishedPagesMono = unpublishedPagesMono - .zipWith(importApplicationMono) - .map(objects -> { - Application application = (Application) objects.getT2(); - List applicationPages = objects.getT1(); - applicationPages.addAll(application.getPages()); - return applicationPages; - }) - .zipWith(importedNewPagesMono) - .map(objects -> { - List unpublishedPages = objects.getT1(); - Map newToOldNameMap = objects.getT2().getT2(); - List importedPages = objects.getT2().getT1(); - for (NewPage newPage : importedPages) { - // we need to map the newly created page with old name - // because other related resources e.g. actions will refer the page with old name - String newPageName = newPage.getUnpublishedPage().getName(); - if (newToOldNameMap.containsKey(newPageName)) { - String oldPageName = newToOldNameMap.get(newPageName); - unpublishedPages.stream() - .filter(applicationPage -> oldPageName.equals(applicationPage.getId())) - .findAny() - .ifPresent(applicationPage -> applicationPage.setId(newPageName)); - } - } - return unpublishedPages; - }); + List importedPages, + Map newNameForClashingOldPageNames, + Application importedApplication) { + + editModeApplicationPages.addAll(importedApplication.getPages()); + for (NewPage newPage : importedPages) { + // we need to map the newly created page with old name + // because other related resources e.g. actions will refer the page with old name + String newPageName = newPage.getUnpublishedPage().getName(); + if (!newNameForClashingOldPageNames.containsKey(newPageName)) { + continue; + } + + String oldPageName = newNameForClashingOldPageNames.get(newPageName); + editModeApplicationPages.stream() + .filter(applicationPage -> oldPageName.equals(applicationPage.getId())) + .findAny() + .ifPresent(applicationPage -> applicationPage.setId(newPageName)); } - return unpublishedPagesMono; + + return editModeApplicationPages; } // This method will update the action id in saved page for layoutOnLoadAction diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/importable/NewPageImportableServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/importable/NewPageImportableServiceImpl.java index a250a0b19fc0..2c44e29e79bc 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/importable/NewPageImportableServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/newpages/importable/NewPageImportableServiceImpl.java @@ -5,14 +5,16 @@ import com.appsmith.server.newactions.base.NewActionService; import com.appsmith.server.newpages.base.NewPageService; import com.appsmith.server.services.ApplicationPageService; +import com.appsmith.server.staticurl.StaticUrlService; import org.springframework.stereotype.Service; @Service public class NewPageImportableServiceImpl extends NewPageImportableServiceCEImpl implements ImportableService { public NewPageImportableServiceImpl( NewPageService newPageService, + StaticUrlService staticUrlService, ApplicationPageService applicationPageService, NewActionService newActionService) { - super(newPageService, applicationPageService, newActionService); + super(newPageService, staticUrlService, applicationPageService, newActionService); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCE.java index d37a6ab7650c..6e3eda846201 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCE.java @@ -93,4 +93,8 @@ Flux findAllBranchedApplicationIdsByBranchedApplicationId( Mono countByDeletedAtNull(); Mono findByIdAndExportWithConfiguration(String id, boolean exportWithConfiguration); + + Flux findByUniqueAppSlugRefName(String uniqueAppName, String refName, AclPermission aclPermission); + + Flux findByUniqueAppName(String uniqueAppName, AclPermission aclPermission); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCEImpl.java index dc6ee3856570..95addb8a4ea6 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCEImpl.java @@ -2,6 +2,7 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.Application.Fields; import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.User; import com.appsmith.server.helpers.ce.bridge.Bridge; @@ -278,8 +279,9 @@ public Mono protectBranchedApplications( @Override public Flux findBranchedApplicationIdsByBaseApplicationId(String baseApplicationId) { - final BridgeQuery q = - Bridge.equal(Application.Fields.gitApplicationMetadata_defaultApplicationId, baseApplicationId); + final BridgeQuery q = Bridge.or( + Bridge.equal(Application.Fields.gitApplicationMetadata_defaultApplicationId, baseApplicationId), + Bridge.equal(Application.Fields.gitApplicationMetadata_defaultArtifactId, baseApplicationId)); return queryBuilder().criteria(q).fields(Application.Fields.id).all().map(application -> application.getId()); } @@ -337,4 +339,23 @@ public Mono findByIdAndExportWithConfiguration(String id, boolean e .equal(Application.Fields.exportWithConfiguration, exportWithConfiguration); return queryBuilder().criteria(q).one(); } + + @Override + public Flux findByUniqueAppSlugRefName( + String uniqueAppName, String refName, AclPermission aclPermission) { + final BridgeQuery criteria = Bridge.and( + Bridge.equal(Fields.staticUrlSettings_uniqueSlug, uniqueAppName), + Bridge.or( + Bridge.equal(Fields.gitApplicationMetadata_branchName, refName), + Bridge.equal(Fields.gitApplicationMetadata_refName, refName))); + + return queryBuilder().criteria(criteria).permission(aclPermission).all(); + } + + @Override + public Flux findByUniqueAppName(String uniqueAppName, AclPermission aclPermission) { + final BridgeQuery criteria = Bridge.equal(Fields.staticUrlSettings_uniqueSlug, uniqueAppName); + + return queryBuilder().criteria(criteria).permission(aclPermission).all(); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCE.java index 4f901e9fa30c..c66ea26f1ffd 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCE.java @@ -33,6 +33,9 @@ Mono findByNameAndApplicationIdAndViewMode( Mono getNameByPageId(String pageId, boolean isPublishedName); + public Flux findByBasePageId( + String basePageId, AclPermission permission, List projectedFieldNames); + Mono findPageByRefTypeAndRefNameAndBasePageId( RefType refType, String refName, @@ -51,4 +54,14 @@ Mono findPageByRefTypeAndRefNameAndBasePageId( Flux findByApplicationId(String applicationId); Mono countByDeletedAtNull(); + + Mono findByUniquePageSlug( + String applicationId, String uniquePageName, AclPermission permission, boolean viewMode); + + Mono findByApplicationIdAndPageSlug( + String applicationId, + String pageSlug, + boolean viewMode, + AclPermission aclPermission, + List projections); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCEImpl.java index 5f6bec3f82e8..90214844a58a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCEImpl.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Set; +import static com.appsmith.external.constants.spans.ce.PageSpanCE.FETCH_PAGES_WITH_BASE_ID; import static com.appsmith.external.constants.spans.ce.PageSpanCE.FETCH_PAGE_FROM_DB; import static com.appsmith.external.helpers.StringUtils.dotted; import static org.springframework.data.mongodb.core.query.Criteria.where; @@ -139,11 +140,13 @@ public Flux findAllPageDTOsByIds(List ids, AclPermission aclPer NewPage.Fields.unpublishedPage_isHidden, NewPage.Fields.unpublishedPage_slug, NewPage.Fields.unpublishedPage_customSlug, + NewPage.Fields.unpublishedPage_uniqueSlug, NewPage.Fields.publishedPage_name, NewPage.Fields.publishedPage_icon, NewPage.Fields.publishedPage_isHidden, NewPage.Fields.publishedPage_slug, - NewPage.Fields.publishedPage_customSlug); + NewPage.Fields.publishedPage_customSlug, + NewPage.Fields.publishedPage_uniqueSlug); return this.queryBuilder() .criteria(Bridge.in(NewPage.Fields.id, ids)) @@ -158,6 +161,20 @@ private BridgeQuery getNameCriterion(String name, Boolean viewMode) { name); } + private BridgeQuery getSlugCriterion(String pageSlug, Boolean viewMode) { + return Bridge.equal( + Boolean.TRUE.equals(viewMode) ? NewPage.Fields.publishedPage_slug : NewPage.Fields.unpublishedPage_slug, + pageSlug); + } + + private BridgeQuery getUniqueSlugCriterion(String uniqueSlug, Boolean viewMode) { + return Bridge.equal( + Boolean.TRUE.equals(viewMode) + ? NewPage.Fields.publishedPage_uniqueSlug + : NewPage.Fields.unpublishedPage_uniqueSlug, + uniqueSlug); + } + @Override public Mono getNameByPageId(String pageId, boolean isPublishedName) { return queryBuilder().byId(pageId).one().map(p -> { @@ -203,6 +220,23 @@ public Mono findPageByRefTypeAndRefNameAndBasePageId( .tap(Micrometer.observation(observationRegistry)); } + @Override + public Flux findByBasePageId( + String basePageId, AclPermission permission, List projectedFieldNames) { + + final BridgeQuery q = + // defaultPageIdCriteria + Bridge.equal(NewPage.Fields.baseId, basePageId); + + return queryBuilder() + .criteria(q) + .permission(permission) + .fields(projectedFieldNames) + .all() + .name(FETCH_PAGES_WITH_BASE_ID) + .tap(Micrometer.observation(observationRegistry)); + } + @Override public Flux findAllByApplicationIds(List applicationIds, List includedFields) { return queryBuilder() @@ -271,4 +305,45 @@ public Mono countByDeletedAtNull() { final BridgeQuery q = Bridge.notExists(NewPage.Fields.deletedAt); return queryBuilder().criteria(q).count(); } + + @Override + public Mono findByUniquePageSlug( + String applicationId, String uniquePageName, AclPermission permission, boolean viewMode) { + final BridgeQuery uniqueSlugCriteria = getUniqueSlugCriterion(uniquePageName, viewMode); + final BridgeQuery appCriteria = Bridge.equal(NewPage.Fields.applicationId, applicationId); + + if (Boolean.FALSE.equals(viewMode)) { + // In case a page has been deleted in edit mode, but still exists in deployed mode, NewPage object would + // exist. To handle this, only fetch non-deleted pages + uniqueSlugCriteria.isNull(NewPage.Fields.unpublishedPage_deletedAt); + } + + return queryBuilder() + .criteria(appCriteria.and(uniqueSlugCriteria)) + .permission(permission) + .one(); + } + + public Mono findByApplicationIdAndPageSlug( + String applicationId, + String pageSlug, + boolean viewMode, + AclPermission aclPermission, + List projections) { + final BridgeQuery q = + getSlugCriterion(pageSlug, viewMode).equal(NewPage.Fields.applicationId, applicationId); + q.and(Bridge.equal(NewPage.Fields.applicationId, applicationId)); + + if (Boolean.FALSE.equals(viewMode)) { + // In case a page has been deleted in edit mode, but still exists in deployed mode, NewPage object would + // exist. To handle this, only fetch non-deleted pages + q.isNull(NewPage.Fields.unpublishedPage_deletedAt); + } + + return queryBuilder() + .criteria(q) + .fields(projections) + .permission(aclPermission) + .one(); + } } 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 7b4ed4daf6fd..f8d3e95e18e1 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 @@ -26,6 +26,7 @@ import com.appsmith.server.solutions.DatasourcePermission; import com.appsmith.server.solutions.PagePermission; import com.appsmith.server.solutions.WorkspacePermission; +import com.appsmith.server.staticurl.StaticUrlService; import com.appsmith.server.themes.base.ThemeService; import io.micrometer.observation.ObservationRegistry; import lombok.extern.slf4j.Slf4j; @@ -38,6 +39,7 @@ public class ApplicationPageServiceImpl extends ApplicationPageServiceCEImpl imp public ApplicationPageServiceImpl( WorkspaceService workspaceService, ApplicationService applicationService, + StaticUrlService staticUrlService, SessionUserService sessionUserService, WorkspaceRepository workspaceRepository, UpdateLayoutService updateLayoutService, @@ -69,6 +71,7 @@ public ApplicationPageServiceImpl( super( workspaceService, applicationService, + staticUrlService, sessionUserService, workspaceRepository, updateLayoutService, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java index cbf20d6813c2..437a8287ff80 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java @@ -10,6 +10,7 @@ import com.appsmith.server.plugins.base.PluginService; import com.appsmith.server.repositories.CacheableRepositoryHelper; import com.appsmith.server.services.ce_compatible.ConsolidatedAPIServiceCECompatibleImpl; +import com.appsmith.server.staticurl.StaticUrlService; import com.appsmith.server.themes.base.ThemeService; import io.micrometer.observation.ObservationRegistry; import lombok.extern.slf4j.Slf4j; @@ -34,6 +35,7 @@ public ConsolidatedAPIServiceImpl( CustomJSLibService customJSLibService, PluginService pluginService, DatasourceService datasourceService, + StaticUrlService staticUrlService, MockDataService mockDataService, ObservationRegistry observationRegistry, CacheableRepositoryHelper cacheableRepositoryHelper, @@ -53,6 +55,7 @@ public ConsolidatedAPIServiceImpl( customJSLibService, pluginService, datasourceService, + staticUrlService, mockDataService, observationRegistry, cacheableRepositoryHelper, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java index 434eee93cbbc..6aa2646bc8a3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java @@ -30,7 +30,6 @@ import com.appsmith.server.dtos.ClonePageMetaDTO; import com.appsmith.server.dtos.CustomJSLibContextDTO; import com.appsmith.server.dtos.PageDTO; -import com.appsmith.server.dtos.PageNameIdDTO; import com.appsmith.server.dtos.PluginTypeAndCountDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; @@ -60,6 +59,7 @@ import com.appsmith.server.solutions.DatasourcePermission; import com.appsmith.server.solutions.PagePermission; import com.appsmith.server.solutions.WorkspacePermission; +import com.appsmith.server.staticurl.StaticUrlService; import com.appsmith.server.themes.base.ThemeService; import com.google.common.base.Strings; import io.micrometer.observation.ObservationRegistry; @@ -88,6 +88,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -106,6 +107,7 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE { private final WorkspaceService workspaceService; private final ApplicationService applicationService; + private final StaticUrlService staticUrlService; private final SessionUserService sessionUserService; private final WorkspaceRepository workspaceRepository; private final UpdateLayoutService updateLayoutService; @@ -278,6 +280,7 @@ public Mono> getPagesBasedOnApplicationMode( NewPage.Fields.publishedPage_icon, NewPage.Fields.publishedPage_slug, NewPage.Fields.publishedPage_customSlug, + NewPage.Fields.publishedPage_uniqueSlug, NewPage.Fields.publishedPage_isHidden, NewPage.Fields.userPermissions, NewPage.Fields.policies, @@ -633,27 +636,44 @@ protected Mono clonePageGivenApplicationId( return sourcePageMono .flatMap(page -> { clonePageMetaDTO.setBranchedSourcePageId(page.getId()); - Mono pageNamesMono = - newPageService.findApplicationPagesByBranchedApplicationIdAndViewMode( - page.getApplicationId(), false, false); + + List pageFields = List.of( + NewPage.Fields.id, + NewPage.Fields.baseId, + NewPage.Fields.applicationId, + NewPage.Fields.refName, + NewPage.Fields.refType, + NewPage.Fields.branchName, + NewPage.Fields.gitSyncId, + NewPage.Fields.unpublishedPage_name, + NewPage.Fields.unpublishedPage_slug, + NewPage.Fields.unpublishedPage_customSlug, + NewPage.Fields.unpublishedPage_uniqueSlug); + + Mono> editModePagesMono = newPageService + .findNewPagesByApplicationId( + page.getApplicationId(), pagePermission.getReadPermission(), pageFields) + .collectList(); Mono destinationApplicationMono = applicationService .findById(applicationId, applicationPermission.getEditPermission()) .switchIfEmpty(Mono.error(new AppsmithException( AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId))); - return Mono.zip(pageNamesMono, destinationApplicationMono) + return Mono.zip(editModePagesMono, destinationApplicationMono) // If a new page name suffix is given, // set a unique name for the cloned page and then create the page. .flatMap(tuple -> { - ApplicationPagesDTO pageNames = tuple.getT1(); + List editModePages = tuple.getT1(); Application application = tuple.getT2(); if (!Strings.isNullOrEmpty(newPageNameSuffix)) { String newPageName = page.getName() + newPageNameSuffix; - Set names = pageNames.getPages().stream() - .map(PageNameIdDTO::getName) + Set names = editModePages.stream() + .map(NewPage::getUnpublishedPage) + .filter(Objects::nonNull) + .map(PageDTO::getName) .collect(Collectors.toSet()); int i = 0; @@ -673,7 +693,10 @@ protected Mono clonePageGivenApplicationId( if (gitData != null) { page.setRefName(gitData.getRefName()); } - return newPageService.createDefault(page); + + return staticUrlService + .updateUniqueSlugBeforeClone(page, editModePages) + .flatMap(incomingPageDTO -> newPageService.createDefault(incomingPageDTO)); }); }) .flatMap(clonedPage -> { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java index 9b266cce8c08..5b1aa73fc24d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java @@ -20,6 +20,7 @@ import com.appsmith.server.dtos.ResponseDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.TextUtils; import com.appsmith.server.jslibs.base.CustomJSLibService; import com.appsmith.server.newactions.base.NewActionService; import com.appsmith.server.newpages.base.NewPageService; @@ -32,6 +33,7 @@ import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.UserDataService; import com.appsmith.server.services.UserService; +import com.appsmith.server.staticurl.StaticUrlService; import com.appsmith.server.themes.base.ThemeService; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -64,6 +66,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.appsmith.external.constants.PluginConstants.PLUGINS_THAT_ALLOW_QUERY_CREATION_WITHOUT_DATASOURCE; @@ -102,6 +105,8 @@ public class ConsolidatedAPIServiceCEImpl implements ConsolidatedAPIServiceCE { public static final String INTERNAL_SERVER_ERROR_CODE = AppsmithError.INTERNAL_SERVER_ERROR.getAppErrorCode(); public static final String EMPTY_WORKSPACE_ID_ON_ERROR = ""; + protected Pattern objectIdPattern = Pattern.compile("^[0-9a-fA-F]+$", Pattern.CASE_INSENSITIVE); + private final SessionUserService sessionUserService; private final UserService userService; private final UserDataService userDataService; @@ -116,6 +121,7 @@ public class ConsolidatedAPIServiceCEImpl implements ConsolidatedAPIServiceCE { private final CustomJSLibService customJSLibService; private final PluginService pluginService; private final DatasourceService datasourceService; + protected final StaticUrlService staticUrlService; private final MockDataService mockDataService; private final ObservationRegistry observationRegistry; private final CacheableRepositoryHelper cacheableRepositoryHelper; @@ -223,22 +229,25 @@ protected List> getAllFetchableMonos( .name(getQualifiedSpanName(PRODUCT_ALERT_SPAN, mode)) .tap(Micrometer.observation(observationRegistry))); - if (isBlank(basePageId) && isBlank(baseApplicationId)) { + if (isBlank(basePageId)) { return fetches; } /* Get view mode - EDIT or PUBLISHED */ boolean isViewMode = isViewMode(mode); + Boolean isStaticMode = isStaticMode(baseApplicationId, basePageId); + Mono> applicationAndPageTupleMono; - /* Fetch default application id if not provided */ - if (isBlank(basePageId)) { - return fetches; - } - - Mono baseApplicationIdMono = getBaseApplicationIdMono(basePageId, baseApplicationId, mode, isViewMode); + if (isStaticMode) { + applicationAndPageTupleMono = staticUrlService.getApplicationAndPageTupleFromStaticNames( + baseApplicationId, basePageId, refName, mode); - Mono> applicationAndPageTupleMono = - getApplicationAndPageTupleMono(basePageId, refType, refName, mode, baseApplicationIdMono, isViewMode); + } else { + Mono baseApplicationIdMono = + getBaseApplicationIdMono(basePageId, baseApplicationId, mode, isViewMode); + applicationAndPageTupleMono = getApplicationAndPageTupleMono( + basePageId, refType, refName, mode, baseApplicationIdMono, isViewMode); + } Mono branchedPageMonoCached = applicationAndPageTupleMono.map(Tuple2::getT2).cache(); @@ -479,6 +488,15 @@ protected List> getAllFetchableMonos( return fetches; } + protected Boolean isStaticMode(String baseApplicationId, String basePageId) { + if (TextUtils.isSlugFormatValid(baseApplicationId) + && (isBlank(basePageId) || TextUtils.isSlugFormatValid(basePageId))) { + return Boolean.TRUE; + } + + return Boolean.FALSE; + } + protected Mono getBaseApplicationIdMono( String basePageId, String baseApplicationId, ApplicationMode mode, boolean isViewMode) { Mono baseApplicationIdMono = Mono.just(""); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/ConsolidatedAPIServiceCECompatibleImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/ConsolidatedAPIServiceCECompatibleImpl.java index 5baec349b02a..f7cdeadd9115 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/ConsolidatedAPIServiceCECompatibleImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/ConsolidatedAPIServiceCECompatibleImpl.java @@ -17,6 +17,7 @@ import com.appsmith.server.services.UserDataService; import com.appsmith.server.services.UserService; import com.appsmith.server.services.ce.ConsolidatedAPIServiceCEImpl; +import com.appsmith.server.staticurl.StaticUrlService; import com.appsmith.server.themes.base.ThemeService; import io.micrometer.observation.ObservationRegistry; @@ -37,6 +38,7 @@ public ConsolidatedAPIServiceCECompatibleImpl( CustomJSLibService customJSLibService, PluginService pluginService, DatasourceService datasourceService, + StaticUrlService staticUrlService, MockDataService mockDataService, ObservationRegistry observationRegistry, CacheableRepositoryHelper cacheableRepositoryHelper, @@ -56,6 +58,7 @@ public ConsolidatedAPIServiceCECompatibleImpl( customJSLibService, pluginService, datasourceService, + staticUrlService, mockDataService, observationRegistry, cacheableRepositoryHelper, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlService.java new file mode 100644 index 000000000000..b411d20e8968 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlService.java @@ -0,0 +1,3 @@ +package com.appsmith.server.staticurl; + +public interface StaticUrlService extends StaticUrlServiceCECompatible {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlServiceCECompatible.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlServiceCECompatible.java new file mode 100644 index 000000000000..0c2a01e5ede9 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlServiceCECompatible.java @@ -0,0 +1,50 @@ +package com.appsmith.server.staticurl; + +import com.appsmith.external.dtos.UniqueSlugDTO; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationMode; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.dtos.PageDTO; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +import java.util.List; + +public interface StaticUrlServiceCECompatible { + + // ------------------------- Application Section --------------------- + + Mono suggestUniqueApplicationSlug(String branchedAppId); + + Mono autoGenerateStaticUrl(String branchedAppId, UniqueSlugDTO uniqueSlugDTO); + + Mono updateApplicationSlug(String branchedAppId, UniqueSlugDTO staticUrlDTO); + + Mono deleteStaticUrlSettings(String branchedAppId); + + Mono isApplicationSlugUnique(String branchedAppId, String uniqueSlug); + + // ------------------------- Pages Section --------------------- + + Mono updatePageSlug(UniqueSlugDTO uniqueSlugDTO); + + Mono isPageSlugUnique(String branchedPageId, String uniquePageSlug); + + // ------------------------- Routing Section --------------------- + Mono> getApplicationAndPageTupleFromStaticNames( + String uniqueAppSlug, String uniquePageSlug, String refName, ApplicationMode mode); + + // ------------------------- Import Section --------------------- + + Mono generateAndUpdateApplicationSlugForNewImports(Application application); + + Mono generateAndUpdateApplicationSlugForImportsOnExistingApps( + Application applicationFromJson, Application applicationFromDB); + + Mono> updateUniquePageSlugsBeforeImport( + List pagesToImport, List pagesFromDb, Application importedApplication); + + Mono updateUniqueSlugBeforeClone(PageDTO incomingPageDTO, List newPages); + + void deleteUniqueSlugFromDbWhenAbsentFromPageJson(NewPage pageFromJson, NewPage pageFromDb); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlServiceCECompatibleImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlServiceCECompatibleImpl.java new file mode 100644 index 000000000000..ab859dd448eb --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlServiceCECompatibleImpl.java @@ -0,0 +1,101 @@ +package com.appsmith.server.staticurl; + +import com.appsmith.external.dtos.UniqueSlugDTO; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationMode; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +import java.util.List; + +@Component +public class StaticUrlServiceCECompatibleImpl implements StaticUrlServiceCECompatible { + + @Override + public Mono autoGenerateStaticUrl(String branchedAppId, UniqueSlugDTO uniqueSlugDTO) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + + @Override + public Mono suggestUniqueApplicationSlug(String branchedAppId) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + + @Override + public Mono updateApplicationSlug(String branchedAppId, UniqueSlugDTO staticUrlDTO) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + + @Override + public Mono deleteStaticUrlSettings(String branchedAppId) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + + @Override + public Mono isApplicationSlugUnique(String branchedAppId, String uniqueSlug) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + + @Override + public Mono updatePageSlug(UniqueSlugDTO uniqueSlugDTO) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + + @Override + public Mono isPageSlugUnique(String branchedPageId, String uniquePageSlug) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + + @Override + public Mono> getApplicationAndPageTupleFromStaticNames( + String uniqueAppSlug, String uniquePageSlug, String refName, ApplicationMode mode) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + + @Override + public Mono generateAndUpdateApplicationSlugForNewImports(Application application) { + application.setStaticUrlSettings(null); + return Mono.just(application); + } + + @Override + public Mono generateAndUpdateApplicationSlugForImportsOnExistingApps( + Application applicationFromJson, Application applicationFromDB) { + applicationFromJson.setStaticUrlSettings(null); + return Mono.just(applicationFromJson); + } + + @Override + public Mono> updateUniquePageSlugsBeforeImport( + List pagesToImport, List pagesFromDb, Application importedApplication) { + return Mono.just(pagesToImport); + } + + @Override + public Mono updateUniqueSlugBeforeClone(PageDTO incomingPageDTO, List existingPagesFromApp) { + incomingPageDTO.setUniqueSlug(null); + return Mono.just(incomingPageDTO); + } + + @Override + public void deleteUniqueSlugFromDbWhenAbsentFromPageJson(NewPage pageFromJson, NewPage pageFromDb) { + PageDTO jsonPageEditDTO = pageFromJson.getUnpublishedPage(); + PageDTO dbPageEditDTO = pageFromDb.getUnpublishedPage(); + + if (jsonPageEditDTO == null || dbPageEditDTO == null) { + return; + } + + if (!StringUtils.hasText(jsonPageEditDTO.getUniqueSlug())) { + dbPageEditDTO.setUniqueSlug(null); + } + + return; + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlServiceImpl.java new file mode 100644 index 000000000000..cc9d784b7f4c --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/staticurl/StaticUrlServiceImpl.java @@ -0,0 +1,1272 @@ +package com.appsmith.server.staticurl; + +import com.appsmith.external.annotations.FeatureFlagged; +import com.appsmith.external.dtos.UniqueSlugDTO; +import com.appsmith.external.enums.FeatureFlagEnum; +import com.appsmith.server.applications.base.ApplicationService; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.Application.Fields; +import com.appsmith.server.domains.Application.StaticUrlSettings; +import com.appsmith.server.domains.ApplicationMode; +import com.appsmith.server.domains.ApplicationPage; +import com.appsmith.server.domains.GitArtifactMetadata; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.GitUtils; +import com.appsmith.server.helpers.TextUtils; +import com.appsmith.server.newpages.base.NewPageService; +import com.appsmith.server.solutions.ApplicationPermission; +import com.appsmith.server.solutions.PagePermission; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static com.appsmith.server.helpers.GitUtils.MAX_RETRIES; + +/** + * Service implementation for managing static URLs and unique slugs for applications and pages. + * + *

This service provides functionality to:

+ *
    + *
  • Generate and validate unique application slugs
  • + *
  • Manage page-level unique slugs within applications
  • + *
  • Handle static URL enablement and configuration
  • + *
  • Support Git-connected applications with branch-specific slug management
  • + *
  • Process slug generation during application imports
  • + *
+ * + *

The service ensures slug uniqueness across applications and pages while maintaining + * compatibility with Git workflows and import/export operations.

+ * + * @author Appsmith Team + * @since 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class StaticUrlServiceImpl extends StaticUrlServiceCECompatibleImpl implements StaticUrlService { + + protected final NewPageService pageService; + protected final PagePermission pagePermission; + + protected final ApplicationService applicationService; + protected final ApplicationPermission applicationPermission; + + private static final String SLUG_APPEND_FORMAT = "%s-%s"; + private static final List pageFields = List.of( + NewPage.Fields.id, + NewPage.Fields.baseId, + NewPage.Fields.applicationId, + NewPage.Fields.refName, + NewPage.Fields.refType, + NewPage.Fields.branchName, + NewPage.Fields.gitSyncId, + NewPage.Fields.unpublishedPage_uniqueSlug); + + /** + * Updates the unique slug for an application. + * + *

This method validates the provided slug format and ensures uniqueness across + * all applications. For Git-connected applications, it considers the base application + * ID to maintain proper slug isolation between branches.

+ * + * @param branchedApplicationId the ID of the application (supports branched applications) + * @param staticUrlDTO contains the unique application slug to be set + * @return Mono<Application> the updated application with the new slug + * @throws AppsmithException if the application ID is invalid or slug format is invalid + * @throws AppsmithException if the slug is already taken by another application + * + * @see #isApplicationSlugUnique(Application, String) + * @see TextUtils#isSlugFormatValid(String) + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono updateApplicationSlug(String branchedApplicationId, UniqueSlugDTO staticUrlDTO) { + log.info("Starting application slug update for applicationId: {}", branchedApplicationId); + + if (!StringUtils.hasLength(branchedApplicationId)) { + log.error("Invalid application ID provided: {}", branchedApplicationId); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.APPLICATION_ID)); + } + + if (!StringUtils.hasText(staticUrlDTO.getUniqueApplicationSlug())) { + log.error("Invalid slug provided for applicationId: {}", branchedApplicationId); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PROPERTIES_CONFIGURATION)); + } + + log.debug("Input slug: {}", staticUrlDTO.getUniqueApplicationSlug()); + return setUniqueApplicationSlug(branchedApplicationId, staticUrlDTO) + .doOnSuccess(app -> + log.info("Application slug updated successfully for applicationId: {}", branchedApplicationId)) + .doOnError(error -> log.error( + "Failed to update application slug for applicationId: {}", branchedApplicationId, error)); + } + + /** + * Suggests a unique application slug based on the application's current name or slug. + * + *

This method generates a unique slug by:

+ *
    + *
  1. Using the existing slug if available and valid
  2. + *
  3. Converting the application name to a slug format
  4. + *
  5. Appending a random suffix if the slug is already taken
  6. + *
+ * + *

This method is feature-flagged and requires the {@code release_static_url_enabled} + * feature flag to be enabled.

+ * + * @param branchedApplicationId the ID of the application + * @return Mono<String> a unique slug suggestion + * @throws AppsmithException if the application ID is invalid + * @throws AppsmithException if the application is not found or access is denied + * @throws AppsmithException if the current slug format is invalid + * + * @see #generateUniqueApplicationSlug(Application, String, int) + * @see FeatureFlagEnum#release_static_url_enabled + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono suggestUniqueApplicationSlug(String branchedApplicationId) { + log.info("Suggesting unique application slug for applicationId: {}", branchedApplicationId); + + if (!StringUtils.hasLength(branchedApplicationId)) { + log.error("Invalid application ID provided: {}", branchedApplicationId); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.APPLICATION_ID)); + } + + return applicationService + .findById(branchedApplicationId, applicationPermission.getReadPermission()) + .switchIfEmpty(Mono.error(new AppsmithException( + AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION, branchedApplicationId))) + .flatMap(application -> { + String applicationSlug = getApplicationSlug(application); + log.debug("Current application slug: {}", applicationSlug); + + if (!TextUtils.isSlugFormatValid(applicationSlug)) { + log.error("Invalid slug format: {}", applicationSlug); + return Mono.error(new AppsmithException( + AppsmithError.INVALID_PARAMETER, Fields.staticUrlSettings_uniqueSlug)); + } + + return generateUniqueApplicationSlug(application, applicationSlug, 0); + }) + .doOnSuccess(suggestedSlug -> log.info( + "Generated unique slug: {} for applicationId: {}", suggestedSlug, branchedApplicationId)) + .doOnError(error -> + log.error("Failed to suggest unique slug for applicationId: {}", branchedApplicationId, error)); + } + + /** + * Automatically generates and enables static URL for an application. + * + *

This method enables static URLs for an application by:

+ *
    + *
  1. Validating the provided unique slug
  2. + *
  3. Checking slug availability across applications
  4. + *
  5. Enabling static URL settings on the application
  6. + *
  7. Generating unique slugs for all pages in the application
  8. + *
+ * + *

This method is feature-flagged and requires the {@code release_static_url_enabled} + * feature flag to be enabled.

+ * + * @param branchedApplicationId the ID of the application + * @param uniqueSlugDTO contains the unique application slug to be set + * @return Mono<Application> the updated application with static URL enabled + * @throws AppsmithException if the application ID is invalid + * @throws AppsmithException if the slug format is invalid + * @throws AppsmithException if the slug is already taken by another application + * + * @see FeatureFlagEnum#release_static_url_enabled + * @see #generateUniquePageSlugsForApplication(Application) + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono autoGenerateStaticUrl(String branchedApplicationId, UniqueSlugDTO uniqueSlugDTO) { + log.info("Auto-generating static URL for applicationId: {}", branchedApplicationId); + + if (!StringUtils.hasLength(branchedApplicationId)) { + log.error("Invalid application ID provided: {}", branchedApplicationId); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.APPLICATION_ID)); + } + + if (!StringUtils.hasText(uniqueSlugDTO.getUniqueApplicationSlug())) { + log.error("Invalid slug provided for applicationId: {}", branchedApplicationId); + return Mono.error( + new AppsmithException(AppsmithError.INVALID_PARAMETER, Fields.staticUrlSettings_uniqueSlug)); + } + + return applicationService + .findById(branchedApplicationId, applicationPermission.getEditPermission()) + .switchIfEmpty(Mono.error(new AppsmithException( + AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION, branchedApplicationId))) + .flatMap(application -> { + StaticUrlSettings staticUrlSettings; + if (application.getStaticUrlSettings() == null) { + application.setStaticUrlSettings(new StaticUrlSettings()); + } + + staticUrlSettings = application.getStaticUrlSettings(); + + log.debug("Static URL already enabled: {}", staticUrlSettings.getEnabled()); + // No action required for already enabled static urls + if (Boolean.TRUE.equals(staticUrlSettings.getEnabled())) { + log.info("Static URL already enabled for applicationId: {}", branchedApplicationId); + return Mono.just(application); + } + + String normalizedUniqueSlug = TextUtils.makeSlug(uniqueSlugDTO.getUniqueApplicationSlug()); + log.debug("Normalized slug: {}", normalizedUniqueSlug); + + if (!TextUtils.isSlugFormatValid(normalizedUniqueSlug)) { + log.error("Invalid slug format: {}", normalizedUniqueSlug); + return Mono.error(new AppsmithException( + AppsmithError.INVALID_PARAMETER, Fields.staticUrlSettings_uniqueSlug)); + } + + return isApplicationSlugUnique(application, normalizedUniqueSlug) + .flatMap(isSlugAvailable -> { + if (!isSlugAvailable) { + log.error("Slug not available: {}", normalizedUniqueSlug); + return Mono.error(new AppsmithException( + AppsmithError.UNIQUE_SLUG_UNAVAILABLE, + FieldName.APPLICATION, + normalizedUniqueSlug)); + } + + UniqueSlugDTO slugDTO = new UniqueSlugDTO(); + slugDTO.setUniqueApplicationSlug(normalizedUniqueSlug); + slugDTO.setStaticUrlEnabled(Boolean.TRUE); + + return applicationService + .findById(application.getId()) + .flatMap(dbApplication -> { + modifyStaticUrlSettings(dbApplication, slugDTO); + return applicationService.save(dbApplication); + }) + .flatMap(dbApplication -> { + log.debug( + "Generating unique page slugs for application: {}", + dbApplication.getId()); + // add static url to all pages for current app. + return generateUniquePageSlugsForApplication(dbApplication) + .then(Mono.just(dbApplication)); + }); + }); + }) + .doOnSuccess(app -> + log.info("Static URL auto-generated successfully for applicationId: {}", branchedApplicationId)) + .doOnError(error -> log.error( + "Failed to auto-generate static URL for applicationId: {}", branchedApplicationId, error)); + } + + /** + * Gets the application slug from the application object. + * + *

This method returns the slug in the following priority order:

+ *
    + *
  1. Existing slug if available and not empty
  2. + *
  3. Slug generated from application name
  4. + *
  5. null if neither is available
  6. + *
+ * + * @param application the application to extract slug from + * @return String the application slug or null if not available + */ + private String getApplicationSlug(Application application) { + if (StringUtils.hasText(application.getSlug())) { + return application.getSlug(); + } + + if (StringUtils.hasText(application.getName())) { + return TextUtils.makeSlug(application.getName()); + } + + return null; + } + + /** + * Generates a unique application slug by appending random suffixes if needed. + * + *

This method recursively generates unique slugs by appending a 4-character + * random UUID suffix when the original slug is already taken. It will retry + * up to MAX_RETRIES times before giving up.

+ * + * @param application the application for which to generate the slug + * @param slugName the base slug name to make unique + * @param retry the current retry count (0-based) + * @return Mono<String> a unique slug + * @throws AppsmithException if unable to generate a unique slug after MAX_RETRIES attempts + * + * @see #isApplicationSlugUnique(Application, String) + * @see GitUtils#MAX_RETRIES + */ + private Mono generateUniqueApplicationSlug(Application application, String slugName, int retry) { + log.debug("Generating unique application slug: {} (retry: {})", slugName, retry); + + return isApplicationSlugUnique(application, slugName).flatMap(isUniqueNameAvailable -> { + if (Boolean.TRUE.equals(isUniqueNameAvailable)) { + log.debug("Slug is unique: {}", slugName); + return Mono.just(slugName); + } + + if (retry > MAX_RETRIES) { + log.error("Failed to generate unique slug after {} retries for base slug: {}", MAX_RETRIES, slugName); + return Mono.error(new AppsmithException(AppsmithError.DUPLICATE_KEY)); + } + + String suffix = UUID.randomUUID().toString().substring(0, 4); + String newUniqueSlug = String.format(SLUG_APPEND_FORMAT, slugName, suffix); + log.debug("Generated new slug with suffix: {}", newUniqueSlug); + return generateUniqueApplicationSlug(application, newUniqueSlug, retry + 1); + }); + } + + /** + * Sets a unique application slug after validation. + * + *

This method validates the provided slug and ensures it's unique before + * updating the application. It handles both Git-connected and non-Git applications.

+ * + * @param branchedApplicationId the ID of the application + * @param staticUrlDTO contains the unique application slug to be set + * @return Mono<Application> the updated application + * @throws AppsmithException if the application ID is invalid + * @throws AppsmithException if the slug format is invalid + * @throws AppsmithException if the slug is already taken + */ + private Mono setUniqueApplicationSlug(String branchedApplicationId, UniqueSlugDTO staticUrlDTO) { + log.debug("Setting unique application slug for applicationId: {}", branchedApplicationId); + + if (!StringUtils.hasText(branchedApplicationId)) { + log.error("Invalid application ID provided: {}", branchedApplicationId); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.APPLICATION_ID)); + } + + final String normalizedSlug = TextUtils.makeSlug(staticUrlDTO.getUniqueApplicationSlug()); + log.debug("Normalized slug: {}", normalizedSlug); + + if (!TextUtils.isSlugFormatValid(normalizedSlug)) { + log.error("Invalid slug format: {}", normalizedSlug); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PROPERTIES_CONFIGURATION)); + } + + return applicationService + .findById(branchedApplicationId, applicationPermission.getEditPermission()) + .switchIfEmpty(Mono.error(new AppsmithException( + AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION, branchedApplicationId))) + .flatMap(application -> { + return setUniqueApplicationSlug(application, normalizedSlug); + }); + } + + /** + * Sets a unique application slug for the given application. + * + *

This method checks slug uniqueness and updates the application if the slug + * is available. It preserves the existing static URL enabled status.

+ * + * @param application the application to update + * @param normalizedSlug the normalized slug to set + * @return Mono<Application> the updated application + * @throws AppsmithException if the slug is already taken + */ + private Mono setUniqueApplicationSlug(Application application, String normalizedSlug) { + log.debug("Setting unique slug: {} for application: {}", normalizedSlug, application.getId()); + + StaticUrlSettings staticUrlSettings = application.getStaticUrlSettings(); + if (staticUrlSettings == null || !Boolean.TRUE.equals(staticUrlSettings.getEnabled())) { + log.error("Static url is not enabled for app: {}", application.getId()); + return Mono.error(new AppsmithException( + AppsmithError.INVALID_PARAMETER, Fields.staticUrlSettings, StaticUrlSettings.Fields.enabled)); + } + + return isApplicationSlugUnique(application, normalizedSlug).flatMap(isUniqueSlugAvailable -> { + if (!Boolean.TRUE.equals(isUniqueSlugAvailable)) { + log.error("Slug not available: {}", normalizedSlug); + return Mono.error(new AppsmithException( + AppsmithError.DUPLICATE_KEY_USER_ERROR, normalizedSlug, Fields.staticUrlSettings_uniqueSlug)); + } + + UniqueSlugDTO uniqueSlugDTO = new UniqueSlugDTO(); + uniqueSlugDTO.setUniqueApplicationSlug(normalizedSlug); + uniqueSlugDTO.setStaticUrlEnabled(Boolean.TRUE); + + return applicationService.findById(application.getId()).flatMap(appFromDB -> { + modifyStaticUrlSettings(appFromDB, uniqueSlugDTO); + return applicationService.save(appFromDB); + }); + }); + } + + /** + * Checks if the given application slug is unique across all applications. + * + *

For Git-connected applications, this method ensures that slugs are unique + * within the same base application (across all branches). For non-Git applications, + * it checks uniqueness across all applications.

+ * + * @param application the application for which to check slug uniqueness + * @param normalizedUniqueApplicationSlug the slug to check (must be normalized) + * @return Mono<Boolean> true if the slug is unique, false otherwise + * + * @see GitUtils#isArtifactConnectedToGit(GitArtifactMetadata) + */ + protected Mono isApplicationSlugUnique(Application application, String normalizedUniqueApplicationSlug) { + log.debug( + "Checking application slug uniqueness: {} for applicationId: {}", + normalizedUniqueApplicationSlug, + application.getId()); + + String baseApplicationId = application.getId(); + log.debug("Base application ID: {}", baseApplicationId); + + return applicationService + .findByUniqueAppName(normalizedUniqueApplicationSlug, null) + .filter(app -> { + // the base id for other applications should be same as the current one. + if (GitUtils.isArtifactConnectedToGit(app.getGitArtifactMetadata())) { + log.debug("Git connected application found: {}", app.getId()); + return !baseApplicationId.equals( + app.getGitArtifactMetadata().getDefaultArtifactId()); + } + + // valid if the current app has acquired the old name + if (app.getId().equals(application.getId())) { + log.debug("Same application found, slug is not unique"); + return Boolean.FALSE; + } + + log.debug("Different application found with same slug: {}", app.getId()); + return Boolean.TRUE; + }) + .hasElements() + .map(Boolean.FALSE::equals) + .doOnNext(isUnique -> log.debug("Slug uniqueness result: {}", isUnique)); + } + + /** + * Deletes static URL settings for an application. + * + *

This method disables static URLs and removes unique slugs from both the + * application and all its pages. It ensures complete cleanup of static URL + * configuration.

+ * + * @param branchedApplicationId the ID of the application + * @return Mono<Application> the updated application with static URL disabled + * @throws AppsmithException if the application ID is invalid + * @throws AppsmithException if the application is not found + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono deleteStaticUrlSettings(String branchedApplicationId) { + log.info("Deleting static URL settings for applicationId: {}", branchedApplicationId); + + if (!StringUtils.hasLength(branchedApplicationId)) { + log.error("Invalid application ID provided: {}", branchedApplicationId); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.APPLICATION_ID)); + } + + return applicationService + .findById(branchedApplicationId, applicationPermission.getEditPermission()) + .switchIfEmpty(Mono.error(new AppsmithException( + AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, branchedApplicationId))) + .flatMap(application -> deleteStaticUrlSettings(application)) + .doOnSuccess(app -> log.info( + "Static URL settings deleted successfully for applicationId: {}", branchedApplicationId)) + .doOnError(error -> log.error( + "Failed to delete static URL settings for applicationId: {}", branchedApplicationId, error)); + } + + /** + * Deletes static URL settings for the given application. + * + *

This method performs the actual deletion by:

+ *
    + *
  1. Disabling static URL settings on the application
  2. + *
  3. Removing unique slugs from all pages in the application
  4. + *
  5. Saving all changes to the database
  6. + *
+ * + * @param application the application to process + * @return Mono<Application> the updated application + */ + private Mono deleteStaticUrlSettings(Application application) { + log.debug("Processing static URL deletion for application: {}", application.getId()); + + disableStaticUrlSettings(application); + return applicationService.save(application).flatMap(disabledStaticUrlApp -> { + return pageService + .findNewPagesByApplicationId(disabledStaticUrlApp.getId(), null) + .collectList() + .flatMap(pages -> { + log.debug("Found {} pages to update", pages.size()); + pages.forEach(page -> { + PageDTO editPage = page.getUnpublishedPage(); + PageDTO viewPage = page.getPublishedPage(); + + if (editPage != null) { + editPage.setUniqueSlug(null); + } + + if (viewPage != null) { + viewPage.setUniqueSlug(null); + } + }); + return pageService.saveAll(pages).then(Mono.just(disabledStaticUrlApp)); + }); + }); + } + + /** + * Disables static URL settings on an application. + * + *

This method sets the static URL enabled flag to false and clears the + * unique slug from the application.

+ * + * @param application the application to disable static URLs for + */ + protected void disableStaticUrlSettings(Application application) { + log.debug("Disabling static URL settings for application: {}", application.getId()); + application.setStaticUrlSettings(null); + } + + /** + * Checks if an application slug is unique and returns the result. + * + *

This method validates the slug format and checks uniqueness across + * applications. It returns a DTO containing both the normalized slug and + * the availability status.

+ * + * @param branchedApplicationId the ID of the application + * @param uniqueSlug the slug to check for uniqueness + * @return Mono<UniqueSlugDTO> contains the normalized slug and availability status + * @throws AppsmithException if the application ID is invalid + * @throws AppsmithException if the slug format is invalid + * @throws AppsmithException if the application is not found + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono isApplicationSlugUnique(String branchedApplicationId, String uniqueSlug) { + log.info( + "Checking application slug uniqueness for applicationId: {}, slug: {}", + branchedApplicationId, + uniqueSlug); + + if (!StringUtils.hasLength(branchedApplicationId)) { + log.error("Invalid application ID provided: {}", branchedApplicationId); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.APPLICATION_ID)); + } + + final String normalizedAppSlug = TextUtils.makeSlug(uniqueSlug); + log.debug("Normalized app slug: {}", normalizedAppSlug); + + if (!TextUtils.isSlugFormatValid(normalizedAppSlug)) { + log.error("Invalid slug format: {}", normalizedAppSlug); + return Mono.error( + new AppsmithException(AppsmithError.INVALID_PARAMETER, Fields.staticUrlSettings_uniqueSlug)); + } + + return applicationService + .findById(branchedApplicationId, applicationPermission.getEditPermission()) + .switchIfEmpty(Mono.error(new AppsmithException( + AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION, branchedApplicationId))) + .flatMap(application -> { + return isApplicationSlugUnique(application, normalizedAppSlug) + .map(isUniqueSlugAvailable -> { + log.debug("Slug uniqueness result: {}", isUniqueSlugAvailable); + UniqueSlugDTO uniqueSlugDTO = new UniqueSlugDTO(); + uniqueSlugDTO.setUniqueApplicationSlug(normalizedAppSlug); + uniqueSlugDTO.setIsUniqueSlugAvailable(isUniqueSlugAvailable); + return uniqueSlugDTO; + }); + }) + .doOnSuccess(result -> log.info( + "Application slug uniqueness check completed for applicationId: {}", branchedApplicationId)) + .doOnError(error -> log.error( + "Failed to check application slug uniqueness for applicationId: {}", + branchedApplicationId, + error)); + } + + /** + * Modifies static URL settings on an application. + * + *

This method updates the application's static URL configuration based on + * the provided DTO. It handles both enabling/disabling static URLs and + * setting unique slugs.

+ * + * @param application the application to modify + * @param uniqueSlugDTO contains the new static URL settings + */ + protected void modifyStaticUrlSettings(Application application, UniqueSlugDTO uniqueSlugDTO) { + log.debug("Modifying static URL settings for application: {}", application.getId()); + + if (application.getStaticUrlSettings() == null) { + application.setStaticUrlSettings(new StaticUrlSettings()); + } + + StaticUrlSettings staticUrlSettings = application.getStaticUrlSettings(); + + if (uniqueSlugDTO.getStaticUrlEnabled() == null + && !StringUtils.hasText(uniqueSlugDTO.getUniqueApplicationSlug())) { + log.debug("Disabling static URL settings"); + application.setStaticUrlSettings(null); + return; + } + + if (uniqueSlugDTO.getStaticUrlEnabled() != null) { + log.debug("Setting static URL enabled: {}", uniqueSlugDTO.getStaticUrlEnabled()); + application.getStaticUrlSettings().setEnabled(uniqueSlugDTO.getStaticUrlEnabled()); + } + + if (StringUtils.hasText(uniqueSlugDTO.getUniqueApplicationSlug())) { + log.debug("Setting unique slug: {}", uniqueSlugDTO.getUniqueApplicationSlug()); + staticUrlSettings.setUniqueSlug(uniqueSlugDTO.getUniqueApplicationSlug()); + } + } + + // -------------------------------------------- Page operations -------------------------------------------- // + + /** + * Updates the unique slug for a page. + * + *

This method validates the provided page slug and ensures it's unique within + * the application. It handles both setting new slugs and clearing existing ones.

+ * + * @param uniqueSlugDTO contains the page ID and unique slug to be set + * @return Mono<NewPage> the updated page + * @throws AppsmithException if the page ID is invalid + * @throws AppsmithException if the slug format is invalid + * @throws AppsmithException if the slug is already taken by another page + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono updatePageSlug(UniqueSlugDTO uniqueSlugDTO) { + String pageId = uniqueSlugDTO.getBranchedPageId(); + final String normalizedPageSlug = TextUtils.makeSlug(uniqueSlugDTO.getUniquePageSlug()); + + log.info("Updating page slug for pageId: {}", pageId); + log.debug("Normalized page slug: {}", normalizedPageSlug); + + return verifyUniqueSlugArgs(pageId, normalizedPageSlug) + .then(pageService.findById(pageId, pagePermission.getEditPermission())) + .switchIfEmpty( + Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE, pageId))) + .zipWhen(page -> { + return applicationService + .findById(page.getApplicationId(), applicationPermission.getEditPermission()) + .switchIfEmpty(Mono.error(new AppsmithException( + AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, page.getApplicationId()))); + }) + .flatMap(pageAndAppTuple -> { + NewPage page = pageAndAppTuple.getT1(); + Application app = pageAndAppTuple.getT2(); + + if (!StringUtils.hasText(normalizedPageSlug)) { + log.debug("Clearing page slug for pageId: {}", pageId); + PageDTO editPage = page.getUnpublishedPage(); + if (editPage == null) { + return Mono.just(page); + } + + editPage.setUniqueSlug(null); + return pageService.save(page); + } + + return isUniquePageSlugAvailable(app, page, normalizedPageSlug) + .flatMap(isUniquePageSlugAvailable -> { + if (!Boolean.TRUE.equals(isUniquePageSlugAvailable)) { + log.error("Page slug not available: {}", normalizedPageSlug); + return Mono.error(new AppsmithException( + AppsmithError.UNIQUE_SLUG_UNAVAILABLE, FieldName.PAGE, normalizedPageSlug)); + } + + return pageService.findById(pageId, null).flatMap(pageFromDB -> { + PageDTO editPage = pageFromDB.getUnpublishedPage(); + if (editPage != null) { + editPage.setUniqueSlug(normalizedPageSlug); + } + + return pageService.save(pageFromDB); + }); + }); + }) + .doOnSuccess(updatedPage -> log.info("Page slug updated successfully for pageId: {}", pageId)) + .doOnError(error -> log.error("Failed to update page slug for pageId: {}", pageId, error)); + } + + /** + * Checks if a page slug is unique within an application. + * + *

This method ensures that page slugs are unique within the same application. + * It excludes the current page from the uniqueness check.

+ * + * @param application the application containing the pages + * @param newPage the page for which to check slug uniqueness + * @param normalizedUniquePageSlug the slug to check (must be normalized) + * @return Mono<Boolean> true if the slug is unique, false otherwise + * @throws AppsmithException if the slug is empty or invalid + */ + protected Mono isUniquePageSlugAvailable( + Application application, NewPage newPage, String normalizedUniquePageSlug) { + log.debug( + "Checking page slug uniqueness: {} for pageId: {} in applicationId: {}", + normalizedUniquePageSlug, + newPage.getId(), + application.getId()); + + if (!StringUtils.hasText(normalizedUniquePageSlug)) { + log.error("Empty slug provided for pageId: {}", newPage.getId()); + return Mono.error( + new AppsmithException(AppsmithError.INVALID_PARAMETER, Fields.staticUrlSettings_uniqueSlug)); + } + + return pageService + .findAllByApplicationIds(List.of(application.getId()), pageFields) + .filter(page -> { + boolean isDifferentPage = !page.getId().equals(newPage.getId()); + PageDTO editPageDTO = page.getUnpublishedPage(); + // unique-page slug utilized by any other page of same app should result in a clash + boolean haveSameUniqueSlug = + editPageDTO != null && normalizedUniquePageSlug.equals(editPageDTO.getUniqueSlug()); + return isDifferentPage && haveSameUniqueSlug; + }) + .hasElements() + .map(Boolean.FALSE::equals) + .doOnNext(isUnique -> log.debug("Page slug uniqueness result: {}", isUnique)); + } + + /** + * Checks if a page slug is unique for Git-connected applications. + * + *

This method is deprecated and should not be used for new implementations. + * It was designed for Git-connected applications but has been replaced by + * {@link #isUniquePageSlugAvailable(Application, NewPage, String)}.

+ * + * @param gitApplication the Git-connected application + * @param newPage the page for which to check slug uniqueness + * @param normalizedUniquePageSlug the slug to check (must be normalized) + * @return Mono<Boolean> true if the slug is unique, false otherwise + * @throws AppsmithException if the slug is empty or invalid + * + * @deprecated Use {@link #isUniquePageSlugAvailable(Application, NewPage, String)} instead + */ + @Deprecated + protected Mono isUniquePageSlugAvailableForGitApp( + Application gitApplication, NewPage newPage, String normalizedUniquePageSlug) { + log.debug("Using deprecated method isUniquePageSlugAvailableForGitApp for pageId: {}", newPage.getId()); + + if (!StringUtils.hasText(normalizedUniquePageSlug)) { + log.error("Empty slug provided for pageId: {}", newPage.getId()); + return Mono.error( + new AppsmithException(AppsmithError.INVALID_PARAMETER, Fields.staticUrlSettings_uniqueSlug)); + } + + String basePageId = newPage.getBaseIdOrFallback(); + String defaultArtifactId = gitApplication.getGitArtifactMetadata().getDefaultArtifactId(); + + return applicationService + .findAllApplicationsByBaseApplicationId(defaultArtifactId, null) + .map(app -> app.getId()) + .collectList() + .flatMapMany(applicationIds -> { + return pageService.findAllByApplicationIds(applicationIds, pageFields); + }) + .filter(page -> { + // pages with same base page id is not counted as those are the page in question. + // unique-page slug should not be utilized by any other page across all branches of the git app. + boolean hasDifferentBasePage = !basePageId.equals(page.getBaseIdOrFallback()); + PageDTO editPageDTO = page.getUnpublishedPage(); + boolean haveSameUniqueSlug = + editPageDTO != null && normalizedUniquePageSlug.equals(editPageDTO.getUniqueSlug()); + // unique-page slug utilized by any other page of same app should result in a clash + return hasDifferentBasePage && haveSameUniqueSlug; + }) + .hasElements() + .map(Boolean.FALSE::equals); + } + + /** + * Checks if a page slug is unique and returns the result. + * + *

This method validates the slug format and checks uniqueness within + * the application. It returns a DTO containing both the normalized slug and + * the availability status.

+ * + * @param branchedPageId the ID of the page + * @param uniquePageSlug the slug to check for uniqueness + * @return Mono<UniqueSlugDTO> contains the normalized slug and availability status + * @throws AppsmithException if the page ID or slug is invalid + * @throws AppsmithException if the page or application is not found + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono isPageSlugUnique(String branchedPageId, String uniquePageSlug) { + log.info("Checking page slug uniqueness for pageId: {}, slug: {}", branchedPageId, uniquePageSlug); + + if (!StringUtils.hasText(branchedPageId) || !StringUtils.hasText(uniquePageSlug)) { + log.error("Invalid parameters - pageId: {}, slug: {}", branchedPageId, uniquePageSlug); + return Mono.error(new AppsmithException(AppsmithError.GENERIC_BAD_REQUEST)); + } + + final String normalizedPageSlug = TextUtils.makeSlug(uniquePageSlug); + log.debug("Normalized page slug: {}", normalizedPageSlug); + + return verifyUniqueSlugArgs(branchedPageId, normalizedPageSlug) + .then(pageService.findById(branchedPageId, pagePermission.getEditPermission())) + .switchIfEmpty(Mono.error( + new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE, branchedPageId))) + .zipWhen(page -> { + return applicationService + .findById(page.getApplicationId(), applicationPermission.getEditPermission()) + .switchIfEmpty(Mono.error(new AppsmithException( + AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, page.getApplicationId()))); + }) + .flatMap(pageAndAppTuple -> { + NewPage page = pageAndAppTuple.getT1(); + Application app = pageAndAppTuple.getT2(); + + return isUniquePageSlugAvailable(app, page, normalizedPageSlug) + .map(isUniquePageSlugAvailable -> { + log.debug("Page slug uniqueness result: {}", isUniquePageSlugAvailable); + UniqueSlugDTO uniqueSlugDTO1 = new UniqueSlugDTO(); + uniqueSlugDTO1.setUniquePageSlug(normalizedPageSlug); + uniqueSlugDTO1.setIsUniqueSlugAvailable(isUniquePageSlugAvailable); + return uniqueSlugDTO1; + }); + }) + .doOnSuccess(result -> log.info("Page slug uniqueness check completed for pageId: {}", branchedPageId)) + .doOnError(error -> + log.error("Failed to check page slug uniqueness for pageId: {}", branchedPageId, error)); + } + + /** + * Verifies the arguments for unique slug operations. + * + *

This method validates that the page ID is provided and that the slug + * format is valid if a slug is provided.

+ * + * @param pageId the page ID to validate + * @param normalizedPageSlug the normalized slug to validate + * @return Mono<Boolean> true if validation passes + * @throws AppsmithException if validation fails + */ + private Mono verifyUniqueSlugArgs(String pageId, String normalizedPageSlug) { + log.debug("Verifying unique slug arguments for pageId: {}", pageId); + + if (!StringUtils.hasText(pageId)) { + log.error("Empty page ID provided"); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.PAGE_ID)); + } + + if (StringUtils.hasText(normalizedPageSlug) && !TextUtils.isSlugFormatValid(normalizedPageSlug)) { + log.error("Invalid slug format: {}", normalizedPageSlug); + return Mono.error( + new AppsmithException(AppsmithError.INVALID_PARAMETER, Fields.staticUrlSettings_uniqueSlug)); + } + + return Mono.just(Boolean.TRUE); + } + + /** + * Generates a unique page slug for the given base ID and current slug. + * + *

If the current slug is already taken, this method appends an incremental + * number suffix until a unique slug is found.

+ * + * @param baseId the base ID used for uniqueness tracking + * @param currentSlug the desired slug (may be modified if not unique) + * @param uniqueSlugToBaseId map tracking existing slugs and their base IDs + * @return String a unique slug + */ + private String generateUniquePageSlug(String baseId, String currentSlug, Map uniqueSlugToBaseId) { + log.debug("Generating unique page slug for baseId: {}, currentSlug: {}", baseId, currentSlug); + + // let's use this to generate the pageId + if (!uniqueSlugToBaseId.containsKey(currentSlug)) { + log.debug("Slug is unique: {}", currentSlug); + return currentSlug; + } + + int iteration = 1; + String iterativeSlug; + while (true) { + iterativeSlug = String.format(SLUG_APPEND_FORMAT, currentSlug, iteration); + iteration += 1; + + if (!uniqueSlugToBaseId.containsKey(iterativeSlug)) { + log.debug("Generated unique slug with iteration: {}", iterativeSlug); + return iterativeSlug; + } + } + } + + /** + * Generates unique page slugs for all pages in an application. + * + *

This method processes all pages in an application and generates unique slugs + * for each page. It skips pages that don't have valid names or slugs.

+ * + * @param dbApplication the application containing the pages + * @return Flux<NewPage> the updated pages with unique slugs + */ + private Flux generateUniquePageSlugsForApplication(Application dbApplication) { + log.info("Generating unique page slugs for application: {}", dbApplication.getId()); + + return pageService + .findNewPagesByApplicationId(dbApplication.getId(), null) + .collectList() + .flatMapMany(pages -> { + log.debug("Processing {} pages", pages.size()); + Map uniquePageSlugToBaseId = new HashMap<>(); + + for (NewPage page : pages) { + String baseId = page.getBaseIdOrFallback(); + String uniquePageSlug; + + PageDTO editPageDTO = page.getUnpublishedPage(); + // If edit DTO is null or page doesn't have a page name or page slug, + // we cannot move further with this page as we don't have a base name + // for generating unique page slugs + if (editPageDTO == null + || !StringUtils.hasText(editPageDTO.getSlug()) + || !StringUtils.hasText(editPageDTO.getName())) { + log.debug("Skipping page due to missing data: {}", page.getId()); + continue; + } + + String pageSlug = StringUtils.hasText(editPageDTO.getSlug()) + ? editPageDTO.getSlug() + : TextUtils.makeSlug(editPageDTO.getName()); + + if (!TextUtils.isSlugFormatValid(pageSlug)) { + log.debug("Skipping page due to invalid slug format: {}", page.getId()); + continue; + } + + uniquePageSlug = generateUniquePageSlug(baseId, pageSlug, uniquePageSlugToBaseId); + uniquePageSlugToBaseId.put(uniquePageSlug, baseId); + editPageDTO.setUniqueSlug(uniquePageSlug); + } + + log.info("Generated unique slugs for {} pages", pages.size()); + return pageService.saveAll(pages); + }); + } + + // ------------------------- Routing Section --------------------- + + /** + * Resolves an application and page tuple from static URL names. + * + *

This method handles the routing logic for static URLs by:

+ *
    + *
  1. Finding the application by its unique slug
  2. + *
  3. Verifying that static URLs are enabled for the application
  4. + *
  5. Handling Git-connected applications by resolving to the correct branch
  6. + *
  7. Finding the page by its unique slug or falling back to the default page
  8. + *
+ * + *

For Git-connected applications, if no refName is provided, it resolves to + * the default branch. If a page slug is not provided, it returns the default page.

+ * + * @param uniqueAppSlug the unique slug of the application + * @param uniquePageSlug the unique slug of the page (can be null for default page) + * @param refName the Git reference name (branch name for Git-connected apps) + * @param mode the application mode (PUBLISHED or EDIT) + * @return Mono<Tuple2<Application, NewPage>> tuple containing the resolved application and page + * @throws AppsmithException if the application is not found + * @throws AppsmithException if the page is not found + * + * @see ApplicationMode + * @see GitUtils#isArtifactConnectedToGit(GitArtifactMetadata) + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono> getApplicationAndPageTupleFromStaticNames( + String uniqueAppSlug, String uniquePageSlug, String refName, ApplicationMode mode) { + + log.info( + "Resolving application and page from static names - appSlug: {}, pageSlug: {}, mode: {}", + uniqueAppSlug, + uniquePageSlug, + mode); + + Mono applicationWithUniqueNameMono = applicationService + .findByUniqueAppNameRefNameAndApplicationMode(uniqueAppSlug, null, mode) + .filter(application -> { + StaticUrlSettings staticUrlSettings = application.getStaticUrlSettings(); + if (staticUrlSettings == null || !Boolean.TRUE.equals(staticUrlSettings.getEnabled())) { + log.debug("Application found with static url has settings disabled"); + return Boolean.FALSE; + } + + return Boolean.TRUE; + }) + .next() + .flatMap(application -> { + GitArtifactMetadata gitArtifactMetadata = application.getGitArtifactMetadata(); + log.debug("Git connected application: {}", GitUtils.isArtifactConnectedToGit(gitArtifactMetadata)); + // non git connected app, then it's already unique. + if (!GitUtils.isArtifactConnectedToGit(gitArtifactMetadata)) { + return Mono.just(application); + } + + // Git application only + String defaultArtifactId = application.getBaseId(); + + // No name has been provided, moving to default page. + return applicationService.findByBaseIdBranchNameAndApplicationMode( + defaultArtifactId, refName, mode); + }) + .switchIfEmpty(Mono.error( + new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, uniqueAppSlug))); + + return applicationWithUniqueNameMono + .zipWhen(application -> { + // unique name is not present, let's get to default page + if (!StringUtils.hasText(uniquePageSlug)) { + log.debug("No page slug provided, resolving to default page"); + List applicationPages = ApplicationMode.EDIT.equals(mode) + ? application.getPublishedPages() + : application.getPages(); + + String homePageId = applicationPages.stream() + .filter(ApplicationPage::getIsDefault) + .map(applicationPage -> applicationPage.getId()) + .findAny() + .get(); + ; + + return pageService + .findByIdAndApplicationMode(homePageId, mode) + .switchIfEmpty(Mono.error( + new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PAGE))); + } + + log.debug("Resolving page with slug: {}", uniquePageSlug); + Mono pageWithPageSlugMono = pageService + .findByApplicationIdAndPageSlug(application.getId(), uniquePageSlug, mode, null) + .switchIfEmpty(Mono.error(new AppsmithException( + AppsmithError.NO_RESOURCE_FOUND, FieldName.PAGE, uniquePageSlug))); + + return pageService + .findByApplicationIdAndUniquePageSlug(application.getId(), uniquePageSlug, mode) + .switchIfEmpty(pageWithPageSlugMono); + }) + .doOnSuccess(tuple -> log.info("Successfully resolved application and page from static names")) + .doOnError(error -> log.error("Failed to resolve application and page from static names", error)); + } + + // ------------------------- Import Section --------------------- + + /** + * Generates and updates application slug for new imports. + * + *

This method is called when importing a new application to ensure that + * the application has a unique slug. If the static URL is not enabled or + * the slug format is invalid, it clears the slug.

+ * + *

This method is feature-flagged and requires the {@code release_static_url_enabled} + * feature flag to be enabled.

+ * + * @param application the application being imported + * @return Mono<Application> the application with updated slug + * + * @see FeatureFlagEnum#release_static_url_enabled + * @see #generateUniqueApplicationSlug(Application, String, int) + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono generateAndUpdateApplicationSlugForNewImports(Application application) { + log.info("Generating application slug for new import - applicationId: {}", application.getId()); + StaticUrlSettings staticUrlSettingsFromJson = application.getStaticUrlSettings(); + if (staticUrlSettingsFromJson == null + || !Boolean.TRUE.equals(staticUrlSettingsFromJson.getEnabled()) + || !TextUtils.isSlugFormatValid(staticUrlSettingsFromJson.getUniqueSlug())) { + log.debug( + "Static URL not enabled or invalid slug format for new import : {}, clearing slug", + application.getName()); + application.setStaticUrlSettings(null); + return Mono.just(application); + } + + return generateUniqueApplicationSlug(application, staticUrlSettingsFromJson.getUniqueSlug(), 0) + .map(uniqueApplicationSlug -> { + log.debug("Generated unique slug for import: {}", uniqueApplicationSlug); + application.getStaticUrlSettings().setUniqueSlug(uniqueApplicationSlug); + application.getStaticUrlSettings().setEnabled(Boolean.TRUE); + return application; + }) + .switchIfEmpty(Mono.just(application)); + } + + /** + * Generates and updates application slug for imports on existing applications. + * + *

This method is called when importing an application into an existing + * application to ensure slug uniqueness. It uses the existing application + * as the base for uniqueness checking.

+ * + *

This method is feature-flagged and requires the {@code release_static_url_enabled} + * feature flag to be enabled.

+ * + * @param applicationFromJson the application being imported from JSON + * @param applicationFromDB the existing application in the database + * @return Mono<Application> the application with updated slug + * + * @see FeatureFlagEnum#release_static_url_enabled + * @see #generateUniqueApplicationSlug(Application, String, int) + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono generateAndUpdateApplicationSlugForImportsOnExistingApps( + Application applicationFromJson, Application applicationFromDB) { + log.info( + "Generating application slug for import on existing app - jsonAppId: {}, dbAppId: {}", + applicationFromJson.getId(), + applicationFromDB.getId()); + + StaticUrlSettings staticUrlSettingsFromJson = applicationFromJson.getStaticUrlSettings(); + if (staticUrlSettingsFromJson == null + || !Boolean.TRUE.equals(staticUrlSettingsFromJson.getEnabled()) + || !TextUtils.isSlugFormatValid(staticUrlSettingsFromJson.getUniqueSlug())) { + log.debug( + "Static URL not enabled or invalid slug format for import : {}, clearing slug", + applicationFromJson.getName()); + applicationFromJson.setStaticUrlSettings(null); + // should we null it over here? + applicationFromDB.setStaticUrlSettings(null); + return Mono.just(applicationFromJson); + } + + return generateUniqueApplicationSlug(applicationFromDB, staticUrlSettingsFromJson.getUniqueSlug(), 0) + .map(uniqueApplicationSlug -> { + log.debug("Generated unique slug for existing app import: {}", uniqueApplicationSlug); + applicationFromJson.getStaticUrlSettings().setEnabled(Boolean.TRUE); + applicationFromJson.getStaticUrlSettings().setUniqueSlug(uniqueApplicationSlug); + return applicationFromJson; + }) + .switchIfEmpty(Mono.just(applicationFromJson)); + } + + /** + * Updates unique page slugs before importing pages. + * + *

This method processes pages to be imported and ensures that their unique + * slugs don't conflict with existing pages. It handles both Git-connected + * and non-Git applications differently.

+ * + *

This method is feature-flagged and requires the {@code release_static_url_enabled} + * feature flag to be enabled.

+ * + * @param pagesToImport the pages being imported + * @param pagesFromDb the existing pages in the database + * @param importedApplication the application being imported into + * @return Mono<List<NewPage>> the pages with updated unique slugs + * + * @see FeatureFlagEnum#release_static_url_enabled + * @see GitUtils#isArtifactConnectedToGit(GitArtifactMetadata) + */ + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono> updateUniquePageSlugsBeforeImport( + List pagesToImport, List pagesFromDb, Application importedApplication) { + + log.info("Updating unique page slugs before import - {} pages to process", pagesToImport.size()); + + Map uniqueSlugToGitSyncId = new HashMap<>(); + Set gitSyncIds = new HashSet<>(); + + pagesFromDb.stream() + .filter(page -> page.getUnpublishedPage() != null + && StringUtils.hasText(page.getGitSyncId()) + && StringUtils.hasText(page.getUnpublishedPage().getUniqueSlug())) + .forEach(page -> { + gitSyncIds.add(page.getGitSyncId()); + uniqueSlugToGitSyncId.put(page.getUnpublishedPage().getUniqueSlug(), page.getGitSyncId()); + }); + + log.debug( + "Git connected application: {}", + GitUtils.isArtifactConnectedToGit(importedApplication.getGitArtifactMetadata())); + log.debug("Found {} existing page slugs", uniqueSlugToGitSyncId.size()); + + // If it's not git connected, update all the slugs + if (!GitUtils.isArtifactConnectedToGit(importedApplication.getGitArtifactMetadata())) { + log.debug("Non-Git application, updating all page slugs"); + pagesToImport.stream() + .filter(page -> { + return page.getUnpublishedPage() != null + && StringUtils.hasText(page.getUnpublishedPage().getUniqueSlug()); + }) + .forEach(page -> { + String gitSyncId = StringUtils.hasText(page.getGitSyncId()) ? page.getGitSyncId() : "gitSync"; + String newUniqueSlug = generateUniquePageSlug( + gitSyncId, page.getUnpublishedPage().getUniqueSlug(), uniqueSlugToGitSyncId); + page.getUnpublishedPage().setUniqueSlug(newUniqueSlug); + uniqueSlugToGitSyncId.put(newUniqueSlug, gitSyncId); + }); + + return Mono.just(pagesToImport); + } + + log.debug("Git-connected application, processing page slugs"); + pagesToImport.stream() + .filter(page -> { + PageDTO editPageDTO = page.getUnpublishedPage(); + if (editPageDTO == null || !StringUtils.hasText(editPageDTO.getUniqueSlug())) { + return false; + } + + // if the git sync ids is matching then it should simply override + return !StringUtils.hasText(page.getGitSyncId()) || !gitSyncIds.contains(page.getGitSyncId()); + }) + .forEach(page -> { + String gitSyncId = StringUtils.hasText(page.getGitSyncId()) ? page.getGitSyncId() : "gitSync"; + String newUniqueSlug = generateUniquePageSlug( + page.getGitSyncId(), page.getUnpublishedPage().getUniqueSlug(), uniqueSlugToGitSyncId); + page.getUnpublishedPage().setUniqueSlug(newUniqueSlug); + uniqueSlugToGitSyncId.put(newUniqueSlug, gitSyncId); + }); + + log.info("Page slugs updated successfully for import"); + return Mono.just(pagesToImport); + } + + @Override + @FeatureFlagged(featureFlagName = FeatureFlagEnum.release_static_url_enabled) + public Mono updateUniqueSlugBeforeClone(PageDTO incomingPageDTO, List existingPagesFromApp) { + if (!StringUtils.hasText(incomingPageDTO.getUniqueSlug())) { + return Mono.just(incomingPageDTO); + } + + Map uniqueSlugToGitSyncId = new HashMap<>(); + + existingPagesFromApp.stream() + .filter(page -> page.getUnpublishedPage() != null + && StringUtils.hasText(page.getUnpublishedPage().getUniqueSlug())) + .forEach(page -> { + uniqueSlugToGitSyncId.put(page.getUnpublishedPage().getUniqueSlug(), "placeholder"); + }); + + String updatedUniqueSlug = generateUniquePageSlug(null, incomingPageDTO.getUniqueSlug(), uniqueSlugToGitSyncId); + incomingPageDTO.setUniqueSlug(updatedUniqueSlug); + return Mono.just(incomingPageDTO); + } +} diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/domains/EqualityTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/domains/EqualityTest.java index 9068612b2659..de67f8823b1f 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/domains/EqualityTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/domains/EqualityTest.java @@ -23,6 +23,7 @@ public class EqualityTest { TenantConfiguration.class, Application.AppLayout.class, Application.EmbedSetting.class, + Application.StaticUrlSettings.class, GitArtifactMetadata.class); @SneakyThrows @@ -62,20 +63,16 @@ void testOrganizationConfiguration() { } @Test - void testApplicationDetail() { - Application.AppPositioning p1 = new Application.AppPositioning(Application.AppPositioning.Type.AUTO); - Application.AppPositioning p2 = new Application.AppPositioning(Application.AppPositioning.Type.AUTO); - Application.AppPositioning p3 = new Application.AppPositioning(Application.AppPositioning.Type.FIXED); + void testStaticUrlSettings() { + Application.StaticUrlSettings p1 = new Application.StaticUrlSettings(true, "unique"); + Application.StaticUrlSettings p2 = new Application.StaticUrlSettings(true, "unique"); + Application.StaticUrlSettings p3 = new Application.StaticUrlSettings(true, "notUnique"); assertThat(p1).isEqualTo(p2).isNotEqualTo(p3); - ApplicationDetail d1 = new ApplicationDetail(); - d1.setAppPositioning(p1); - ApplicationDetail d2 = new ApplicationDetail(); - d2.setAppPositioning(p2); - ApplicationDetail d3 = new ApplicationDetail(); - d3.setAppPositioning(p3); - assertThat(d1).isEqualTo(d2); - assertThat(d1).isNotEqualTo(d3); + Application.StaticUrlSettings c1 = new Application.StaticUrlSettings(false, "unique"); + Application.StaticUrlSettings c2 = new Application.StaticUrlSettings(false, "unique"); + Application.StaticUrlSettings c3 = new Application.StaticUrlSettings(false, "notUnique"); + assertThat(c1).isEqualTo(c2).isNotEqualTo(c3); } @Test diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java index 35390828f876..7f21d33b92e3 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java @@ -1883,10 +1883,10 @@ public void discardChange_addNewPageAfterImport_addedPageRemoved() { // Import the same application again to find if the added page is deleted final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( importedApplication -> applicationJsonMono.flatMap(applicationJson -> { - importedApplication.setGitApplicationMetadata(new GitArtifactMetadata()); - importedApplication - .getGitApplicationMetadata() - .setDefaultApplicationId(importedApplication.getId()); + GitArtifactMetadata gitData = new GitArtifactMetadata(); + importedApplication.setGitApplicationMetadata(gitData); + gitData.setDefaultApplicationId(importedApplication.getId()); + gitData.setRemoteUrl("https://dummy.url/dummy-user/dummy-app"); return applicationService .save(importedApplication) .then(importService.importArtifactInWorkspaceFromGit( diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ApplicationServiceCETest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ApplicationServiceCETest.java index 7813cff5a25f..cd7c4abd9c4a 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ApplicationServiceCETest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ApplicationServiceCETest.java @@ -103,6 +103,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.dao.DuplicateKeyException; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.query.Query; import org.springframework.http.HttpMethod; @@ -4531,4 +4532,138 @@ public void testCacheEviction_whenPagesDeletedInEditModeFollowedByAppPublish_sho .block(); assertThat(cachedBaseAppId2).isNull(); } + + @Test + @WithUserDetails(value = "api_user") + public void createApplicationWithUniqueSlug() { + Application app1 = new Application(); + app1.setName("SlugTestApp1"); + Application.StaticUrlSettings staticUrlSettings1 = + new Application.StaticUrlSettings(true, "test-unique-slug-app1"); + app1.setStaticUrlSettings(staticUrlSettings1); + Mono create1 = applicationPageService.createApplication(app1, workspaceId); + StepVerifier.create(create1) + .assertNext(app -> { + assertThat(app.getStaticUrlSettings().getUniqueSlug()).isEqualTo("test-unique-slug-app1"); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createDuplicateUniqueSlugShouldFail() { + String slug = "duplicate-slug"; + Application app1 = new Application(); + app1.setName("App D1"); + app1.setStaticUrlSettings(new Application.StaticUrlSettings(true, slug)); + Application app2 = new Application(); + app2.setName("App D2"); + app2.setStaticUrlSettings(new Application.StaticUrlSettings(true, slug)); + Mono create1 = applicationPageService.createApplication(app1, workspaceId); + Mono create2 = + create1.flatMap(saved -> applicationPageService.createApplication(app2, workspaceId)); + StepVerifier.create(create2) + .expectErrorMatches(throwable -> throwable instanceof DuplicateKeyException) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createWithSameSlugAfterDelete() { + String slug = "deleted-slug-ok"; + Application app1 = new Application(); + app1.setName("App E1"); + app1.setStaticUrlSettings(new Application.StaticUrlSettings(true, slug)); + Application app2 = new Application(); + app2.setName("App E2"); + app2.setStaticUrlSettings(new Application.StaticUrlSettings(true, slug)); + Mono createAndDelete = applicationPageService + .createApplication(app1, workspaceId) + .flatMap(savedApp -> applicationService.archive(savedApp)); + Mono create2 = createAndDelete.then(applicationPageService.createApplication(app2, workspaceId)); + StepVerifier.create(create2) + .assertNext(app -> { + assertThat(app.getStaticUrlSettings().getUniqueSlug()).isEqualTo(slug); + assertThat(app.isDeleted()).isFalse(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void findByUniqueAppNameTest() { + String slug = "find-by-slug"; + Application app = new Application(); + app.setName("FindBySlug"); + app.setStaticUrlSettings(new Application.StaticUrlSettings(true, slug)); + Mono create = applicationPageService.createApplication(app, workspaceId); + StepVerifier.create(create.flatMap(savedApp -> applicationService + .findByUniqueAppName(slug, applicationPermission.getReadPermission()) + .next())) + .assertNext(found -> { + assertThat(found.getName()).isEqualTo("FindBySlug"); + assertThat(found.getStaticUrlSettings().getUniqueSlug()).isEqualTo(slug); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void findByUniqueAppNameRefNameAndApplicationModeTest() { + String slug = "find-by-slug-ref"; + Application app = new Application(); + app.setName("FindBySlugRef"); + app.setStaticUrlSettings(new Application.StaticUrlSettings(true, slug)); + Mono create = applicationPageService.createApplication(app, workspaceId); + StepVerifier.create(create.flatMap(savedApp -> applicationService + .findByUniqueAppNameRefNameAndApplicationMode(slug, null, ApplicationMode.EDIT) + .next())) + .assertNext(found -> { + assertThat(found.getName()).isEqualTo("FindBySlugRef"); + assertThat(found.getStaticUrlSettings().getUniqueSlug()).isEqualTo(slug); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void updateUniqueSlug() { + String slug1 = "updatable-slug"; + String slug2 = "updatable-slug-2"; + Application app = new Application(); + app.setName("UpdatableSlug"); + app.setStaticUrlSettings(new Application.StaticUrlSettings(true, slug1)); + Mono create = applicationPageService.createApplication(app, workspaceId); + Mono update = create.flatMap(saved -> { + saved.getStaticUrlSettings().setUniqueSlug(slug2); + return applicationService.update(saved.getId(), saved); + }); + StepVerifier.create(update) + .assertNext(updated -> { + assertThat(updated.getStaticUrlSettings().getUniqueSlug()).isEqualTo(slug2); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void failUpdateUniqueSlugToExistingOne() { + String slug = "dup-slug-update"; + Application app1 = new Application(); + app1.setName("App Z1"); + app1.setStaticUrlSettings(new Application.StaticUrlSettings(true, slug)); + Application app2 = new Application(); + app2.setName("App Z2"); + app2.setStaticUrlSettings(new Application.StaticUrlSettings(true, slug + "-other")); + Mono m1 = applicationPageService.createApplication(app1, workspaceId); + Mono m2 = applicationPageService.createApplication(app2, workspaceId); + Mono failUpdate = Mono.zip(m1, m2).flatMap(tuple -> { + Application updateApp2 = tuple.getT2(); + updateApp2.getStaticUrlSettings().setUniqueSlug(slug); + return applicationService.update(updateApp2.getId(), updateApp2); + }); + StepVerifier.create(failUpdate) + .expectErrorMatches(thr -> thr instanceof AppsmithException) + .verify(); + } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java index 16f6198daa64..bee9e4a6ab74 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java @@ -1,5 +1,6 @@ package com.appsmith.server.services.ce; +import com.appsmith.external.enums.FeatureFlagEnum; import com.appsmith.external.git.constants.ce.RefType; import com.appsmith.external.models.ActionDTO; import com.appsmith.external.models.Datasource; @@ -41,6 +42,7 @@ import com.appsmith.server.repositories.NewPageRepository; import com.appsmith.server.services.ApplicationPageService; import com.appsmith.server.services.ConsolidatedAPIService; +import com.appsmith.server.services.FeatureFlagService; import com.appsmith.server.services.MockDataService; import com.appsmith.server.services.OrganizationService; import com.appsmith.server.services.ProductAlertService; @@ -137,6 +139,9 @@ public class ConsolidatedAPIServiceImplTest { @SpyBean NewPageRepository mockNewPageRepository; + @SpyBean + FeatureFlagService featureFlagService; + @Autowired CacheableRepositoryHelper cacheableRepositoryHelper; @@ -172,6 +177,8 @@ public void testPageLoadResponseWhenPageIdAndApplicationIdMissing() { when(mockProductAlertService.getSingleApplicableMessage()) .thenReturn(Mono.just(List.of(sampleProductAlertResponseDTO))); + doReturn(Mono.just(Boolean.TRUE)).when(featureFlagService).check(FeatureFlagEnum.release_static_url_enabled); + Mono consolidatedInfoForPageLoad = consolidatedAPIService.getConsolidatedInfoForPageLoad( "pageId", "appId", RefType.branch, "branch", ApplicationMode.PUBLISHED); @@ -740,6 +747,8 @@ public void testErrorResponseWhenAnonymousUserAccessPrivateApp() { when(mockProductAlertService.getSingleApplicableMessage()) .thenReturn(Mono.just(List.of(sampleProductAlertResponseDTO))); + doReturn(Mono.just(Boolean.TRUE)).when(featureFlagService).check(FeatureFlagEnum.release_static_url_enabled); + Mockito.doReturn(Mono.empty()) .when(mockNewPageRepository) .findPageByRefTypeAndRefNameAndBasePageId(any(), anyString(), anyString(), any(), any());