From a36b45de60d77ab00a8d74c956907d1ae6548628 Mon Sep 17 00:00:00 2001 From: Manish Kumar <107841575+sondermanish@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:01:51 +0530 Subject: [PATCH] chore: complimentary pr for CD migration changes (#35876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description > CE changes for CD migration on EE pr: https://github.com/appsmithorg/appsmith-ee/pull/4927 ## Automation /ok-to-test tags="@tag.Git" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: ef94bff3b0d12db1a5003a78ce8ddfeabbb3c87d > Cypress dashboard. > Tags: `@tag.Git` > Spec: >
Wed, 28 Aug 2024 15:23:46 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced a new field for tracking the continuous deployment migration version in application metadata. - Enhanced Git artifact management with additional metadata for improved deployment processes. - **Bug Fixes** - Resolved issues related to application versioning and deployment tracking. - **Refactor** - Substantial restructuring of the Application class for improved maintainability and extensibility. - Simplified structure of GitArtifactMetadata, enhancing functionality and clarity. - **Tests** - Expanded equality tests for new entities related to the Application class, ensuring proper validation of equality logic. --- .../appsmith/server/domains/Application.java | 487 +--------------- .../server/domains/GitArtifactMetadata.java | 119 +--- .../server/domains/ce/ApplicationCE.java | 525 ++++++++++++++++++ .../domains/ce/GitArtifactMetadataCE.java | 122 ++++ .../appsmith/server/domains/EqualityTest.java | 57 +- 5 files changed, 729 insertions(+), 581 deletions(-) create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationCE.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/GitArtifactMetadataCE.java 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 d34f79fb93..4e3aecd74f 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 @@ -1,36 +1,16 @@ package com.appsmith.server.domains; -import com.appsmith.external.models.BaseDomain; -import com.appsmith.external.views.Git; -import com.appsmith.external.views.Views; -import com.appsmith.server.constants.ArtifactType; -import com.appsmith.server.dtos.CustomJSLibContextDTO; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonView; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; +import com.appsmith.server.domains.ce.ApplicationCE; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import lombok.experimental.FieldNameConstants; -import org.springframework.data.annotation.Transient; import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.util.StringUtils; import java.io.Serializable; -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static com.appsmith.external.helpers.StringUtils.dotted; -import static com.appsmith.server.constants.ResourceModes.EDIT; -import static com.appsmith.server.constants.ResourceModes.VIEW; -import static com.appsmith.server.helpers.DateUtils.ISO_FORMATTER; @Getter @Setter @@ -38,349 +18,20 @@ import static com.appsmith.server.helpers.DateUtils.ISO_FORMATTER; @NoArgsConstructor @Document @FieldNameConstants -public class Application extends BaseDomain implements Artifact { - - @NotNull @JsonView(Views.Public.class) - String name; - - @JsonView(Views.Public.class) - String workspaceId; - - // TODO: remove default values from application - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - @Deprecated(forRemoval = true) - @JsonView(Views.Public.class) - Boolean isPublic = false; - - @JsonView({Views.Public.class, Git.class}) - List pages; - - @JsonView(Views.Internal.class) - List publishedPages; - - @JsonView(Views.Internal.class) - @Transient - Boolean viewMode = false; - - @Transient - @JsonView({Views.Public.class, Git.class}) - boolean appIsExample = false; - - @Transient - @JsonView(Views.Public.class) - long unreadCommentThreads; - - @JsonView(Views.Internal.class) - String clonedFromApplicationId; - - @JsonView({Views.Internal.class, Git.class}) - ApplicationDetail unpublishedApplicationDetail; - - @JsonView(Views.Internal.class) - ApplicationDetail publishedApplicationDetail; - - @JsonView({Views.Public.class, Git.class}) - String color; - - @JsonView({Views.Public.class, Git.class}) - String icon; - - @JsonView(Views.Public.class) - private String slug; - - @JsonView({Views.Internal.class, Git.class}) - AppLayout unpublishedAppLayout; - - @JsonView(Views.Internal.class) - AppLayout publishedAppLayout; - - @JsonView(Views.Public.class) - Set unpublishedCustomJSLibs; - - @JsonView(Views.Public.class) - Set publishedCustomJSLibs; - - @JsonView(Views.Public.class) - GitArtifactMetadata gitApplicationMetadata; - - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - @JsonView(Views.Public.class) - Instant lastDeployedAt; // when this application was last deployed - - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - @JsonView({Views.Public.class, Git.class}) - Integer evaluationVersion; - - /** - * applicationVersion will be used when we've a breaking change in application, and it's not possible to write a - * migration. User need to update the application manually. - * In such cases, we can use this field to determine whether we need to notify user about that breaking change - * so that they can update their application. - * Once updated, we should set applicationVersion to latest version as well. - */ - @JsonView({Views.Public.class, Git.class}) - Integer applicationVersion; - - /** - * Changing name, change in pages, widgets and datasources will set lastEditedAt. - * Other activities e.g. changing policy will not change this property. - * We're adding JsonIgnore here because it'll be exposed as modifiedAt to keep it backward compatible - */ - @JsonView(Views.Internal.class) - Instant lastEditedAt; - - @JsonView({Views.Public.class, Git.class}) - EmbedSetting embedSetting; - - @JsonView({Views.Public.class, Git.class}) - Boolean collapseInvisibleWidgets; - - /** - * Earlier this was returning value of the updatedAt property in the base domain. - * As this property is modified by the framework when there is any change in domain, - * a new property lastEditedAt has been added to track the edit actions from users. - * This method exposes that property. - * - * @return updated time as a string - */ - @JsonProperty(value = "modifiedAt", access = JsonProperty.Access.READ_ONLY) - @JsonView(Views.Public.class) - public String getLastUpdateTime() { - if (lastEditedAt != null) { - return ISO_FORMATTER.format(lastEditedAt); - } - return null; - } - - @JsonView(Views.Public.class) - public String getLastDeployedAt() { - if (lastDeployedAt != null) { - return ISO_FORMATTER.format(lastDeployedAt); - } - return null; - } - - @JsonView(Views.Public.class) - Boolean forkingEnabled; - - // Field to convey if the application is updated by the user - @JsonView(Views.Public.class) - Boolean isManualUpdate; - - // Field to convey if the application is modified from the DB migration - @Transient - @JsonView(Views.Public.class) - Boolean isAutoUpdate; - - // To convey current schema version for client and server. This will be used to check if we run the migration - // between 2 commits if the application is connected to git - @JsonView({Views.Internal.class, Git.class}) - Integer clientSchemaVersion; - - @JsonView({Views.Internal.class, Git.class}) - Integer serverSchemaVersion; - - @JsonView(Views.Internal.class) - String publishedModeThemeId; - - @JsonView(Views.Internal.class) - String editModeThemeId; - - // TODO Temporary provision for exporting the application with datasource configuration for the sample/template apps - @JsonView(Views.Public.class) - Boolean exportWithConfiguration; - - // forkWithConfiguration represents whether credentials are shared or not while forking an app - @JsonView(Views.Public.class) - Boolean forkWithConfiguration; - - // isCommunityTemplate represents whether this application has been published as a community template - @JsonView(Views.Public.class) - Boolean isCommunityTemplate; - - /* Template title of the template from which this app was forked, if any */ - @JsonView(Views.Public.class) - String forkedFromTemplateTitle; +public class Application extends ApplicationCE implements Artifact { // This constructor is used during clone application. It only deeply copies selected fields. The rest are either // initialized newly or is left up to the calling function to set. public Application(Application application) { - super(); - this.workspaceId = application.getWorkspaceId(); - this.pages = new ArrayList<>(); - this.publishedPages = new ArrayList<>(); - this.clonedFromApplicationId = application.getId(); - this.color = application.getColor(); - this.icon = application.getIcon(); - this.unpublishedAppLayout = application.getUnpublishedAppLayout() == null - ? null - : new AppLayout(application.getUnpublishedAppLayout().type); - this.publishedAppLayout = application.getPublishedAppLayout() == null - ? null - : new AppLayout(application.getPublishedAppLayout().type); - this.setUnpublishedApplicationDetail(new ApplicationDetail()); - this.setPublishedApplicationDetail(new ApplicationDetail()); - if (application.getUnpublishedApplicationDetail() == null) { - application.setUnpublishedApplicationDetail(new ApplicationDetail()); - } - if (application.getPublishedApplicationDetail() == null) { - application.setPublishedApplicationDetail(new ApplicationDetail()); - } - AppPositioning unpublishedAppPositioning = - application.getUnpublishedApplicationDetail().getAppPositioning() == null - ? null - : new AppPositioning( - application.getUnpublishedApplicationDetail().getAppPositioning().type); - this.getUnpublishedApplicationDetail().setAppPositioning(unpublishedAppPositioning); - AppPositioning publishedAppPositioning = - application.getPublishedApplicationDetail().getAppPositioning() == null - ? null - : new AppPositioning( - application.getPublishedApplicationDetail().getAppPositioning().type); - this.getPublishedApplicationDetail().setAppPositioning(publishedAppPositioning); - this.getUnpublishedApplicationDetail() - .setNavigationSetting( - application.getUnpublishedApplicationDetail().getNavigationSetting() == null - ? null - : new NavigationSetting()); - this.getPublishedApplicationDetail() - .setNavigationSetting( - application.getPublishedApplicationDetail().getNavigationSetting() == null - ? null - : new NavigationSetting()); - this.getUnpublishedApplicationDetail() - .setThemeSetting( - application.getUnpublishedApplicationDetail().getThemeSetting() == null - ? null - : new ThemeSetting()); - this.getPublishedApplicationDetail() - .setThemeSetting( - application.getPublishedApplicationDetail().getThemeSetting() == null - ? null - : new ThemeSetting()); - this.unpublishedCustomJSLibs = application.getUnpublishedCustomJSLibs(); - this.collapseInvisibleWidgets = application.getCollapseInvisibleWidgets(); - } - - public void exportApplicationPages(final Map pageIdToNameMap) { - for (ApplicationPage applicationPage : this.getPages()) { - applicationPage.setId(pageIdToNameMap.get(applicationPage.getId() + EDIT)); - applicationPage.setDefaultPageId(null); - } - for (ApplicationPage applicationPage : this.getPublishedPages()) { - applicationPage.setId(pageIdToNameMap.get(applicationPage.getId() + VIEW)); - applicationPage.setDefaultPageId(null); - } - } - - @Override - public String getBaseId() { - if (this.getGitArtifactMetadata() != null - && StringUtils.hasLength(this.getGitArtifactMetadata().getDefaultArtifactId())) { - return this.getGitArtifactMetadata().getDefaultArtifactId(); - } - return Artifact.super.getBaseId(); - } - - @JsonView(Views.Internal.class) - @Override - public GitArtifactMetadata getGitArtifactMetadata() { - return this.gitApplicationMetadata; - } - - @JsonView(Views.Internal.class) - @Override - public void setGitArtifactMetadata(GitArtifactMetadata gitArtifactMetadata) { - this.gitApplicationMetadata = gitArtifactMetadata; - } - - @Override - public String getUnpublishedThemeId() { - return this.getEditModeThemeId(); - } - - @Override - public void setUnpublishedThemeId(String themeId) { - this.setEditModeThemeId(themeId); - } - - @Override - public String getPublishedThemeId() { - return this.getPublishedModeThemeId(); - } - - @Override - public void setPublishedThemeId(String themeId) { - this.setPublishedModeThemeId(themeId); - } - - @Override - public void sanitiseToExportDBObject() { - this.setWorkspaceId(null); - this.setModifiedBy(null); - this.setCreatedBy(null); - this.setLastDeployedAt(null); - this.setLastEditedAt(null); - this.setGitApplicationMetadata(null); - this.setEditModeThemeId(null); - this.setPublishedModeThemeId(null); - this.setClientSchemaVersion(null); - this.setServerSchemaVersion(null); - this.setIsManualUpdate(false); - this.setPublishedCustomJSLibs(new HashSet<>()); - this.setExportWithConfiguration(null); - this.setForkWithConfiguration(null); - this.setForkingEnabled(null); - super.sanitiseToExportDBObject(); - } - - public List getPages() { - return Boolean.TRUE.equals(viewMode) ? publishedPages : pages; - } - - public AppLayout getAppLayout() { - return Boolean.TRUE.equals(viewMode) ? publishedAppLayout : unpublishedAppLayout; - } - - public void setAppLayout(AppLayout appLayout) { - if (Boolean.TRUE.equals(viewMode)) { - publishedAppLayout = appLayout; - } else { - unpublishedAppLayout = appLayout; - } - } - - public ApplicationDetail getApplicationDetail() { - return Boolean.TRUE.equals(viewMode) ? publishedApplicationDetail : unpublishedApplicationDetail; - } - - public void setApplicationDetail(ApplicationDetail applicationDetail) { - if (Boolean.TRUE.equals(viewMode)) { - publishedApplicationDetail = applicationDetail; - } else { - unpublishedApplicationDetail = applicationDetail; - } - } - - @Override - @JsonView({Views.Internal.class}) - public ArtifactType getArtifactType() { - return ArtifactType.APPLICATION; + super(application); } @Data + @EqualsAndHashCode(callSuper = true) @NoArgsConstructor - @AllArgsConstructor - public static class AppLayout implements Serializable { - @JsonView({Views.Public.class, Git.class}) - Type type; - - public enum Type { - DESKTOP, - TABLET_LARGE, - TABLET, - MOBILE, - FLUID, + public static class AppLayout extends AppLayoutCE implements Serializable { + public AppLayout(AppLayout.Type type) { + super(type); } } @@ -388,136 +39,40 @@ public class Application extends BaseDomain implements Artifact { * EmbedSetting is used for embedding Appsmith apps on other platforms */ @Data - public static class EmbedSetting { - - @JsonView({Views.Public.class, Git.class}) - private String height; - - @JsonView({Views.Public.class, Git.class}) - private String width; - - @JsonView({Views.Public.class, Git.class}) - private Boolean showNavigationBar; - } + @EqualsAndHashCode(callSuper = true) + public static class EmbedSetting extends EmbedSettingCE {} /** * NavigationSetting stores the navigation configuration for the app */ @Data - public static class NavigationSetting { - @JsonView({Views.Public.class, Git.class}) - private Boolean showNavbar; - - @JsonView({Views.Public.class, Git.class}) - private String orientation; - - @JsonView({Views.Public.class, Git.class}) - private String navStyle; - - @JsonView({Views.Public.class, Git.class}) - private String position; - - @JsonView({Views.Public.class, Git.class}) - private String itemStyle; - - @JsonView({Views.Public.class, Git.class}) - private String colorStyle; - - @JsonView({Views.Public.class, Git.class}) - private String logoAssetId; - - @JsonView({Views.Public.class, Git.class}) - private String logoConfiguration; - - @JsonView({Views.Public.class, Git.class}) - private Boolean showSignIn; - } + @EqualsAndHashCode(callSuper = true) + public static class NavigationSetting extends NavigationSettingCE {} /** * AppPositioning captures widget positioning Mode of the application */ @Data + @EqualsAndHashCode(callSuper = true) @NoArgsConstructor - @AllArgsConstructor - public static class AppPositioning { - @JsonView({Views.Public.class, Git.class}) - Type type; - + public static class AppPositioning extends AppPositioningCE { public AppPositioning(String type) { - setType(Type.valueOf(type)); + super(type); } - public enum Type { - FIXED, - AUTO, - ANVIL + public AppPositioning(AppPositioning.Type type) { + super(type); } } @Data + @EqualsAndHashCode(callSuper = true) @NoArgsConstructor - public static class ThemeSetting { - - @JsonView({Views.Public.class, Git.class}) - private String accentColor; - - @JsonView({Views.Public.class, Git.class}) - private String borderRadius; - - @JsonView({Views.Public.class, Git.class}) - private float sizing = 1; - - @JsonView({Views.Public.class, Git.class}) - private float density = 1; - - @JsonView({Views.Public.class, Git.class}) - private String fontFamily; - - @JsonView({Views.Public.class, Git.class}) - Type colorMode; - - @JsonView({Views.Public.class, Git.class}) - IconStyle iconStyle; - - @JsonView({Views.Public.class, Git.class}) - AppMaxWidth appMaxWidth = AppMaxWidth.LARGE; - + public static class ThemeSetting extends ThemeSettingCE { public ThemeSetting(Type colorMode) { - this.colorMode = colorMode; - } - - public enum Type { - LIGHT, - DARK - } - - public enum IconStyle { - OUTLINED, - FILLED - } - - public enum AppMaxWidth { - UNLIMITED, - LARGE, - MEDIUM, + super(colorMode); } } - public static class Fields extends BaseDomain.Fields { - public static final String gitApplicationMetadata_gitAuth = - dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.gitAuth); - public static final String gitApplicationMetadata_defaultApplicationId = - dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.defaultApplicationId); - - public static final String gitApplicationMetadata_defaultArtifactId = - dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.defaultArtifactId); - public static final String gitApplicationMetadata_isAutoDeploymentEnabled = - dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.isAutoDeploymentEnabled); - public static final String gitApplicationMetadata_branchName = - dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.branchName); - public static final String gitApplicationMetadata_isRepoPrivate = - dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.isRepoPrivate); - public static final String gitApplicationMetadata_isProtectedBranch = - dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.isProtectedBranch); - } + public static class Fields extends ApplicationCE.Fields {} } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/GitArtifactMetadata.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/GitArtifactMetadata.java index 1e20e973b6..b416e3c666 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/GitArtifactMetadata.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/GitArtifactMetadata.java @@ -1,124 +1,15 @@ package com.appsmith.server.domains; import com.appsmith.external.models.AppsmithDomain; -import com.appsmith.external.views.Views; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonView; +import com.appsmith.server.domains.ce.GitArtifactMetadataCE; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.experimental.FieldNameConstants; -import org.springframework.data.annotation.Transient; -import org.springframework.util.StringUtils; - -import java.time.Instant; -import java.util.List; -import java.util.Map; // This class will be used for one-to-one mapping for the DB application and the application present in the git repo. @Data @FieldNameConstants -public class GitArtifactMetadata implements AppsmithDomain { - // Git branch corresponding to this application, we have one to one mapping for application in DB with git-branch - @JsonView(Views.Public.class) - String branchName; - - // Git default branch corresponding to the remote git repo to which the application is connected to - @JsonView(Views.Public.class) - String defaultBranchName; - - // Git remote url will be used while pushing and pulling changes - @JsonView(Views.Public.class) - String remoteUrl; - - // Git remote https url will be used while checking if the repo is public or private - @JsonView(Views.Public.class) - String browserSupportedRemoteUrl; - - // If remote repo is private and will be stored only with default application - @JsonView(Views.Public.class) - Boolean isRepoPrivate; - - // The name of git repo - @JsonView(Views.Public.class) - String repoName; - - // Default application id used for storing the application files in local volume : - // container-volumes/git_repo/workspaceId/defaultApplicationId/branchName/applicationDirectoryStructure... - @JsonView(Views.Public.class) - String defaultApplicationId; - - // We are maintaining this attribute separately from defaultApplicationId to maintain backward compatibility with - // the directory structure that folks might on their file systems - // Default artifact id used for storing the artifact files in local volume : - // container-volumes/git_repo/workspaceId/artifactType/defaultArtifactId/branchName/artifactDirectoryStructure... - @JsonView(Views.Public.class) - String defaultArtifactId; - - // Git credentials used to push changes to remote repo and will be stored with default application only to optimise - // space requirement and update operation - @JsonView(Views.Internal.class) - GitAuth gitAuth; - - @Transient - @JsonView(Views.Public.class) - Map gitProfiles; - - @Transient - @JsonView(Views.Public.class) - String publicKey; - - // Deploy key documentation url - @Transient - @JsonView(Views.Public.class) - String docUrl; - - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssX", timezone = "UTC") - @JsonView(Views.Public.class) - Instant lastCommittedAt; - - @JsonView(Views.Metadata.class) - List branchProtectionRules; - - /** - * This field is no more used and will be removed in future version. - * Please use the branchProtectionRules field to know whether a branch is protected or not. - */ - @Deprecated - @JsonView(Views.Internal.class) - Boolean isProtectedBranch; - - @JsonView(Views.Metadata.class) - AutoCommitConfig autoCommitConfig; - - /** - * Boolean flag to store whether auto deployment is enabled for any branch of this application. - * If true, any branch of this application can be automatically deployed using git web hook. - */ - @JsonView(Views.Metadata.class) - boolean isAutoDeploymentEnabled; - - public AutoCommitConfig getAutoCommitConfig() { - // by default, the auto commit should be enabled. - // new AutoCommitConfig will have enabled=true so we're returning a new object when field is null - if (autoCommitConfig == null) { - autoCommitConfig = new AutoCommitConfig(); - } - return autoCommitConfig; - } - - @JsonView(Views.Public.class) - public String getDefaultArtifactId() { - if (StringUtils.hasText(defaultArtifactId)) { - return defaultArtifactId; - } else return defaultApplicationId; - } - - // TODO : Set to private to prevent direct access unless migration is performed - private void setDefaultArtifactId(String defaultArtifactId) { - this.defaultArtifactId = defaultArtifactId; - } - - public void setDefaultApplicationId(String defaultApplicationId) { - this.defaultApplicationId = defaultApplicationId; - this.defaultArtifactId = defaultApplicationId; - } +@EqualsAndHashCode(callSuper = true) +public class GitArtifactMetadata extends GitArtifactMetadataCE implements AppsmithDomain { + public static class Fields extends GitArtifactMetadataCE.Fields {} } 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 new file mode 100644 index 0000000000..e00580ef88 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/ApplicationCE.java @@ -0,0 +1,525 @@ +package com.appsmith.server.domains.ce; + +import com.appsmith.external.models.BaseDomain; +import com.appsmith.external.views.Git; +import com.appsmith.external.views.Views; +import com.appsmith.server.constants.ArtifactType; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationDetail; +import com.appsmith.server.domains.ApplicationPage; +import com.appsmith.server.domains.GitArtifactMetadata; +import com.appsmith.server.dtos.CustomJSLibContextDTO; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.FieldNameConstants; +import org.springframework.data.annotation.Transient; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.util.StringUtils; + +import java.io.Serializable; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.appsmith.external.helpers.StringUtils.dotted; +import static com.appsmith.server.constants.ResourceModes.EDIT; +import static com.appsmith.server.constants.ResourceModes.VIEW; +import static com.appsmith.server.helpers.DateUtils.ISO_FORMATTER; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@Document +@FieldNameConstants +public class ApplicationCE extends BaseDomain implements ArtifactCE { + + @NotNull @JsonView(Views.Public.class) + String name; + + @JsonView(Views.Public.class) + String workspaceId; + + // TODO: remove default values from application + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @Deprecated(forRemoval = true) + @JsonView(Views.Public.class) + Boolean isPublic = false; + + @JsonView({Views.Public.class, Git.class}) + List pages; + + @JsonView(Views.Internal.class) + List publishedPages; + + @JsonView(Views.Internal.class) + @Transient + Boolean viewMode = false; + + @Transient + @JsonView({Views.Public.class, Git.class}) + boolean appIsExample = false; + + @Transient + @JsonView(Views.Public.class) + long unreadCommentThreads; + + @JsonView(Views.Internal.class) + String clonedFromApplicationId; + + @JsonView({Views.Internal.class, Git.class}) + ApplicationDetail unpublishedApplicationDetail; + + @JsonView(Views.Internal.class) + ApplicationDetail publishedApplicationDetail; + + @JsonView({Views.Public.class, Git.class}) + String color; + + @JsonView({Views.Public.class, Git.class}) + String icon; + + @JsonView(Views.Public.class) + private String slug; + + @JsonView({Views.Internal.class, Git.class}) + Application.AppLayout unpublishedAppLayout; + + @JsonView(Views.Internal.class) + Application.AppLayout publishedAppLayout; + + @JsonView(Views.Public.class) + Set unpublishedCustomJSLibs; + + @JsonView(Views.Public.class) + Set publishedCustomJSLibs; + + @JsonView(Views.Public.class) + GitArtifactMetadata gitApplicationMetadata; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @JsonView(Views.Public.class) + Instant lastDeployedAt; // when this application was last deployed + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @JsonView({Views.Public.class, Git.class}) + Integer evaluationVersion; + + /** + * applicationVersion will be used when we've a breaking change in application, and it's not possible to write a + * migration. User need to update the application manually. + * In such cases, we can use this field to determine whether we need to notify user about that breaking change + * so that they can update their application. + * Once updated, we should set applicationVersion to latest version as well. + */ + @JsonView({Views.Public.class, Git.class}) + Integer applicationVersion; + + /** + * Changing name, change in pages, widgets and datasources will set lastEditedAt. + * Other activities e.g. changing policy will not change this property. + * We're adding JsonIgnore here because it'll be exposed as modifiedAt to keep it backward compatible + */ + @JsonView(Views.Internal.class) + Instant lastEditedAt; + + @JsonView({Views.Public.class, Git.class}) + Application.EmbedSetting embedSetting; + + @JsonView({Views.Public.class, Git.class}) + Boolean collapseInvisibleWidgets; + + /** + * Earlier this was returning value of the updatedAt property in the base domain. + * As this property is modified by the framework when there is any change in domain, + * a new property lastEditedAt has been added to track the edit actions from users. + * This method exposes that property. + * + * @return updated time as a string + */ + @JsonProperty(value = "modifiedAt", access = JsonProperty.Access.READ_ONLY) + @JsonView(Views.Public.class) + public String getLastUpdateTime() { + if (lastEditedAt != null) { + return ISO_FORMATTER.format(lastEditedAt); + } + return null; + } + + @JsonView(Views.Public.class) + public String getLastDeployedAt() { + if (lastDeployedAt != null) { + return ISO_FORMATTER.format(lastDeployedAt); + } + return null; + } + + @JsonView(Views.Public.class) + Boolean forkingEnabled; + + // Field to convey if the application is updated by the user + @JsonView(Views.Public.class) + Boolean isManualUpdate; + + // Field to convey if the application is modified from the DB migration + @Transient + @JsonView(Views.Public.class) + Boolean isAutoUpdate; + + // To convey current schema version for client and server. This will be used to check if we run the migration + // between 2 commits if the application is connected to git + @JsonView({Views.Internal.class, Git.class}) + Integer clientSchemaVersion; + + @JsonView({Views.Internal.class, Git.class}) + Integer serverSchemaVersion; + + @JsonView(Views.Internal.class) + String publishedModeThemeId; + + @JsonView(Views.Internal.class) + String editModeThemeId; + + // TODO Temporary provision for exporting the application with datasource configuration for the sample/template apps + @JsonView(Views.Public.class) + Boolean exportWithConfiguration; + + // forkWithConfiguration represents whether credentials are shared or not while forking an app + @JsonView(Views.Public.class) + Boolean forkWithConfiguration; + + // isCommunityTemplate represents whether this application has been published as a community template + @JsonView(Views.Public.class) + Boolean isCommunityTemplate; + + /* Template title of the template from which this app was forked, if any */ + @JsonView(Views.Public.class) + String forkedFromTemplateTitle; + + // This constructor is used during clone application. It only deeply copies selected fields. The rest are either + // initialized newly or is left up to the calling function to set. + public ApplicationCE(ApplicationCE application) { + super(); + this.workspaceId = application.getWorkspaceId(); + this.pages = new ArrayList<>(); + this.publishedPages = new ArrayList<>(); + this.clonedFromApplicationId = application.getId(); + this.color = application.getColor(); + this.icon = application.getIcon(); + this.unpublishedAppLayout = application.getUnpublishedAppLayout() == null + ? null + : new Application.AppLayout(application.getUnpublishedAppLayout().type); + this.publishedAppLayout = application.getPublishedAppLayout() == null + ? null + : new Application.AppLayout(application.getPublishedAppLayout().type); + this.setUnpublishedApplicationDetail(new ApplicationDetail()); + this.setPublishedApplicationDetail(new ApplicationDetail()); + if (application.getUnpublishedApplicationDetail() == null) { + application.setUnpublishedApplicationDetail(new ApplicationDetail()); + } + if (application.getPublishedApplicationDetail() == null) { + application.setPublishedApplicationDetail(new ApplicationDetail()); + } + + Application.AppPositioning unpublishedAppPositioning = + application.getUnpublishedApplicationDetail().getAppPositioning() == null + ? null + : new Application.AppPositioning( + application.getUnpublishedApplicationDetail().getAppPositioning().type); + this.getUnpublishedApplicationDetail().setAppPositioning(unpublishedAppPositioning); + Application.AppPositioning publishedAppPositioning = + application.getPublishedApplicationDetail().getAppPositioning() == null + ? null + : new Application.AppPositioning( + application.getPublishedApplicationDetail().getAppPositioning().type); + this.getPublishedApplicationDetail().setAppPositioning(publishedAppPositioning); + this.getUnpublishedApplicationDetail() + .setNavigationSetting( + application.getUnpublishedApplicationDetail().getNavigationSetting() == null + ? null + : new Application.NavigationSetting()); + this.getPublishedApplicationDetail() + .setNavigationSetting( + application.getPublishedApplicationDetail().getNavigationSetting() == null + ? null + : new Application.NavigationSetting()); + this.getUnpublishedApplicationDetail() + .setThemeSetting( + application.getUnpublishedApplicationDetail().getThemeSetting() == null + ? null + : new Application.ThemeSetting()); + this.getPublishedApplicationDetail() + .setThemeSetting( + application.getPublishedApplicationDetail().getThemeSetting() == null + ? null + : new Application.ThemeSetting()); + this.unpublishedCustomJSLibs = application.getUnpublishedCustomJSLibs(); + this.collapseInvisibleWidgets = application.getCollapseInvisibleWidgets(); + } + + public void exportApplicationPages(final Map pageIdToNameMap) { + for (ApplicationPage applicationPage : this.getPages()) { + applicationPage.setId(pageIdToNameMap.get(applicationPage.getId() + EDIT)); + applicationPage.setDefaultPageId(null); + } + for (ApplicationPage applicationPage : this.getPublishedPages()) { + applicationPage.setId(pageIdToNameMap.get(applicationPage.getId() + VIEW)); + applicationPage.setDefaultPageId(null); + } + } + + @Override + public String getBaseId() { + if (this.getGitArtifactMetadata() != null + && StringUtils.hasLength(this.getGitArtifactMetadata().getDefaultArtifactId())) { + return this.getGitArtifactMetadata().getDefaultArtifactId(); + } + return ArtifactCE.super.getBaseId(); + } + + @JsonView(Views.Internal.class) + @Override + public GitArtifactMetadata getGitArtifactMetadata() { + return this.gitApplicationMetadata; + } + + @JsonView(Views.Internal.class) + @Override + public void setGitArtifactMetadata(GitArtifactMetadata gitArtifactMetadata) { + this.gitApplicationMetadata = gitArtifactMetadata; + } + + @Override + public String getUnpublishedThemeId() { + return this.getEditModeThemeId(); + } + + @Override + public void setUnpublishedThemeId(String themeId) { + this.setEditModeThemeId(themeId); + } + + @Override + public String getPublishedThemeId() { + return this.getPublishedModeThemeId(); + } + + @Override + public void setPublishedThemeId(String themeId) { + this.setPublishedModeThemeId(themeId); + } + + @Override + public void sanitiseToExportDBObject() { + this.setWorkspaceId(null); + this.setModifiedBy(null); + this.setCreatedBy(null); + this.setLastDeployedAt(null); + this.setLastEditedAt(null); + this.setGitApplicationMetadata(null); + this.setEditModeThemeId(null); + this.setPublishedModeThemeId(null); + this.setClientSchemaVersion(null); + this.setServerSchemaVersion(null); + this.setIsManualUpdate(false); + this.setPublishedCustomJSLibs(new HashSet<>()); + this.setExportWithConfiguration(null); + this.setForkWithConfiguration(null); + this.setForkingEnabled(null); + super.sanitiseToExportDBObject(); + } + + public List getPages() { + return Boolean.TRUE.equals(viewMode) ? publishedPages : pages; + } + + public Application.AppLayout getAppLayout() { + return Boolean.TRUE.equals(viewMode) ? publishedAppLayout : unpublishedAppLayout; + } + + public void setAppLayout(Application.AppLayout appLayout) { + if (Boolean.TRUE.equals(viewMode)) { + publishedAppLayout = appLayout; + } else { + unpublishedAppLayout = appLayout; + } + } + + public ApplicationDetail getApplicationDetail() { + return Boolean.TRUE.equals(viewMode) ? publishedApplicationDetail : unpublishedApplicationDetail; + } + + public void setApplicationDetail(ApplicationDetail applicationDetail) { + if (Boolean.TRUE.equals(viewMode)) { + publishedApplicationDetail = applicationDetail; + } else { + unpublishedApplicationDetail = applicationDetail; + } + } + + @Override + @JsonView({Views.Internal.class}) + public ArtifactType getArtifactType() { + return ArtifactType.APPLICATION; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class AppLayoutCE implements Serializable { + @JsonView({Views.Public.class, Git.class}) + protected Type type; + + public enum Type { + DESKTOP, + TABLET_LARGE, + TABLET, + MOBILE, + FLUID, + } + } + + /** + * EmbedSetting is used for embedding Appsmith apps on other platforms + */ + @Data + public static class EmbedSettingCE { + + @JsonView({Views.Public.class, Git.class}) + private String height; + + @JsonView({Views.Public.class, Git.class}) + private String width; + + @JsonView({Views.Public.class, Git.class}) + private Boolean showNavigationBar; + } + + /** + * NavigationSetting stores the navigation configuration for the app + */ + @Data + public static class NavigationSettingCE { + @JsonView({Views.Public.class, Git.class}) + private Boolean showNavbar; + + @JsonView({Views.Public.class, Git.class}) + private String orientation; + + @JsonView({Views.Public.class, Git.class}) + private String navStyle; + + @JsonView({Views.Public.class, Git.class}) + private String position; + + @JsonView({Views.Public.class, Git.class}) + private String itemStyle; + + @JsonView({Views.Public.class, Git.class}) + private String colorStyle; + + @JsonView({Views.Public.class, Git.class}) + private String logoAssetId; + + @JsonView({Views.Public.class, Git.class}) + private String logoConfiguration; + + @JsonView({Views.Public.class, Git.class}) + private Boolean showSignIn; + } + + /** + * AppPositioning captures widget positioning Mode of the application + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class AppPositioningCE { + @JsonView({Views.Public.class, Git.class}) + protected Type type; + + public AppPositioningCE(String type) { + setType(Type.valueOf(type)); + } + + public enum Type { + FIXED, + AUTO, + ANVIL + } + } + + @Data + @NoArgsConstructor + public static class ThemeSettingCE { + + @JsonView({Views.Public.class, Git.class}) + private String accentColor; + + @JsonView({Views.Public.class, Git.class}) + private String borderRadius; + + @JsonView({Views.Public.class, Git.class}) + private float sizing = 1; + + @JsonView({Views.Public.class, Git.class}) + private float density = 1; + + @JsonView({Views.Public.class, Git.class}) + private String fontFamily; + + @JsonView({Views.Public.class, Git.class}) + protected Type colorMode; + + @JsonView({Views.Public.class, Git.class}) + IconStyle iconStyle; + + @JsonView({Views.Public.class, Git.class}) + AppMaxWidth appMaxWidth = AppMaxWidth.LARGE; + + public ThemeSettingCE(Type colorMode) { + this.colorMode = colorMode; + } + + public enum Type { + LIGHT, + DARK + } + + public enum IconStyle { + OUTLINED, + FILLED + } + + public enum AppMaxWidth { + UNLIMITED, + LARGE, + MEDIUM, + } + } + + public static class Fields extends BaseDomain.Fields { + public static final String gitApplicationMetadata_gitAuth = + dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.gitAuth); + public static final String gitApplicationMetadata_defaultApplicationId = + dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.defaultApplicationId); + public static final String gitApplicationMetadata_defaultArtifactId = + dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.defaultArtifactId); + public static final String gitApplicationMetadata_branchName = + dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.branchName); + public static final String gitApplicationMetadata_isRepoPrivate = + dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.isRepoPrivate); + public static final String gitApplicationMetadata_isProtectedBranch = + dotted(gitApplicationMetadata, GitArtifactMetadata.Fields.isProtectedBranch); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/GitArtifactMetadataCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/GitArtifactMetadataCE.java new file mode 100644 index 0000000000..ce46a144fc --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/GitArtifactMetadataCE.java @@ -0,0 +1,122 @@ +package com.appsmith.server.domains.ce; + +import com.appsmith.external.models.AppsmithDomain; +import com.appsmith.external.views.Views; +import com.appsmith.server.domains.AutoCommitConfig; +import com.appsmith.server.domains.GitAuth; +import com.appsmith.server.domains.GitProfile; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonView; +import lombok.Data; +import lombok.experimental.FieldNameConstants; +import org.springframework.data.annotation.Transient; +import org.springframework.util.StringUtils; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +// This class will be used for one-to-one mapping for the DB application and the application present in the git repo. +@Data +@FieldNameConstants +public class GitArtifactMetadataCE implements AppsmithDomain { + // Git branch corresponding to this application, we have one to one mapping for application in DB with git-branch + @JsonView(Views.Public.class) + String branchName; + + // Git default branch corresponding to the remote git repo to which the application is connected to + @JsonView(Views.Public.class) + String defaultBranchName; + + // Git remote url will be used while pushing and pulling changes + @JsonView(Views.Public.class) + String remoteUrl; + + // Git remote https url will be used while checking if the repo is public or private + @JsonView(Views.Public.class) + String browserSupportedRemoteUrl; + + // If remote repo is private and will be stored only with default application + @JsonView(Views.Public.class) + Boolean isRepoPrivate; + + // The name of git repo + @JsonView(Views.Public.class) + String repoName; + + // Default application id used for storing the application files in local volume : + // container-volumes/git_repo/workspaceId/defaultApplicationId/branchName/applicationDirectoryStructure... + @JsonView(Views.Public.class) + String defaultApplicationId; + + // We are maintaining this attribute separately from defaultApplicationId to maintain backward compatibility with + // the directory structure that folks might on their file systems + // Default artifact id used for storing the artifact files in local volume : + // container-volumes/git_repo/workspaceId/artifactType/defaultArtifactId/branchName/artifactDirectoryStructure... + @JsonView(Views.Public.class) + String defaultArtifactId; + + // Git credentials used to push changes to remote repo and will be stored with default application only to optimise + // space requirement and update operation + @JsonView(Views.Internal.class) + GitAuth gitAuth; + + @Transient + @JsonView(Views.Public.class) + Map gitProfiles; + + @Transient + @JsonView(Views.Public.class) + String publicKey; + + // Deploy key documentation url + @Transient + @JsonView(Views.Public.class) + String docUrl; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssX", timezone = "UTC") + @JsonView(Views.Public.class) + Instant lastCommittedAt; + + @JsonView(Views.Metadata.class) + List branchProtectionRules; + + /** + * This field is no more used and will be removed in future version. + * Please use the branchProtectionRules field to know whether a branch is protected or not. + */ + @Deprecated + @JsonView(Views.Internal.class) + Boolean isProtectedBranch; + + @JsonView(Views.Metadata.class) + AutoCommitConfig autoCommitConfig; + + public AutoCommitConfig getAutoCommitConfig() { + // by default, the auto commit should be enabled. + // new AutoCommitConfig will have enabled=true so we're returning a new object when field is null + if (autoCommitConfig == null) { + autoCommitConfig = new AutoCommitConfig(); + } + return autoCommitConfig; + } + + @JsonView(Views.Public.class) + public String getDefaultArtifactId() { + if (StringUtils.hasText(defaultArtifactId)) { + return defaultArtifactId; + } else return defaultApplicationId; + } + + // TODO : Set to private to prevent direct access unless migration is performed + private void setDefaultArtifactId(String defaultArtifactId) { + this.defaultArtifactId = defaultArtifactId; + } + + public void setDefaultApplicationId(String defaultApplicationId) { + this.defaultApplicationId = defaultApplicationId; + this.defaultArtifactId = defaultApplicationId; + } + + public static class Fields {} +} 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 e15667024b..ff0476e4cb 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 @@ -18,7 +18,11 @@ public class EqualityTest { private final Set> TESTED_CLASSES = Set.of( // Note: Adding a class here means that we have a test for its equality in this file. - ApplicationDetail.class, TenantConfiguration.class); + ApplicationDetail.class, + TenantConfiguration.class, + Application.AppLayout.class, + Application.EmbedSetting.class, + GitArtifactMetadata.class); @SneakyThrows @Test @@ -72,4 +76,55 @@ public class EqualityTest { assertThat(d1).isEqualTo(d2); assertThat(d1).isNotEqualTo(d3); } + + @Test + void testAppLayout() { + Application.AppLayout a1 = new Application.AppLayout(Application.AppLayout.Type.DESKTOP); + Application.AppLayout a2 = new Application.AppLayout(Application.AppLayout.Type.DESKTOP); + Application.AppLayout a3 = new Application.AppLayout(Application.AppLayout.Type.MOBILE); + assertThat(a1).isEqualTo(a2).isNotEqualTo(a3); + } + + @Test + void testAppEmbedSetting() { + Application.EmbedSetting a1 = new Application.EmbedSetting(); + a1.setHeight("5"); + a1.setWidth("5"); + a1.setShowNavigationBar(Boolean.TRUE); + Application.EmbedSetting a2 = new Application.EmbedSetting(); + a2.setHeight("5"); + a2.setWidth("5"); + a2.setShowNavigationBar(Boolean.TRUE); + Application.EmbedSetting a3 = new Application.EmbedSetting(); + a3.setHeight("5"); + a3.setWidth("5"); + a3.setShowNavigationBar(Boolean.FALSE); + assertThat(a1).isEqualTo(a2).isNotEqualTo(a3); + } + + @Test + void testArtifactEquality() { + String remoteUrl1 = "protocol://domain.superdomain"; + String remoteUrl2 = "protocol://domain.superdomain2"; + + GitArtifactMetadata a1 = new GitArtifactMetadata(); + a1.setRemoteUrl(remoteUrl1); + GitArtifactMetadata a2 = new GitArtifactMetadata(); + a2.setRemoteUrl(remoteUrl1); + GitArtifactMetadata a3 = new GitArtifactMetadata(); + a3.setRemoteUrl(remoteUrl2); + + assertThat(a1).isEqualTo(a2).isNotEqualTo(a3); + + a1.setAutoCommitConfig(new AutoCommitConfig()); + a2.setAutoCommitConfig(new AutoCommitConfig()); + a3.setAutoCommitConfig(new AutoCommitConfig()); + + assertThat(a1).isEqualTo(a2).isNotEqualTo(a3); + + a1.getAutoCommitConfig().setEnabled(Boolean.TRUE); + a2.getAutoCommitConfig().setEnabled(Boolean.FALSE); + + assertThat(a1).isNotEqualTo(a2).isNotEqualTo(a3); + } }