diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java index 57ad9d720..0dc3e3043 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java @@ -11,6 +11,8 @@ import lombok.extern.jackson.Jacksonized; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; +import org.lowcoder.domain.application.ApplicationUtil; +import org.lowcoder.domain.application.service.ApplicationRecordService; import org.lowcoder.domain.query.model.ApplicationQuery; import org.lowcoder.sdk.exception.BizError; import org.lowcoder.sdk.exception.BizException; @@ -19,6 +21,7 @@ import org.springframework.data.annotation.Transient; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; import java.time.Instant; import java.util.*; @@ -41,7 +44,6 @@ public class Application extends HasIdAndAuditing { private Integer applicationType; private ApplicationStatus applicationStatus; - private Map publishedApplicationDSL; private Map editingApplicationDSL; @Setter @@ -63,7 +65,6 @@ public Application( @JsonProperty("name") String name, @JsonProperty("applicationType") Integer applicationType, @JsonProperty("applicationStatus") ApplicationStatus applicationStatus, - @JsonProperty("publishedApplicationDSL") Map publishedApplicationDSL, @JsonProperty("editingApplicationDSL") Map editingApplicationDSL, @JsonProperty("publicToAll") Boolean publicToAll, @JsonProperty("publicToMarketplace") Boolean publicToMarketplace, @@ -76,7 +77,6 @@ public Application( this.name = name; this.applicationType = applicationType; this.applicationStatus = applicationStatus; - this.publishedApplicationDSL = publishedApplicationDSL; this.publicToAll = publicToAll; this.publicToMarketplace = publicToMarketplace; this.agencyProfile = agencyProfile; @@ -87,43 +87,28 @@ public Application( @Transient private final Supplier> editingQueries = - memoize(() -> Optional.ofNullable(editingApplicationDSL) + memoize(() -> ofNullable(editingApplicationDSL) .map(map -> map.get("queries")) .map(queries -> JsonUtils.fromJsonSet(JsonUtils.toJson(queries), ApplicationQuery.class)) .orElse(Collections.emptySet())); - @Transient - private final Supplier> liveQueries = - memoize(() -> JsonUtils.fromJsonSet(JsonUtils.toJson(getLiveApplicationDsl().get("queries")), ApplicationQuery.class)); - @Transient private final Supplier> editingModules = memoize(() -> getDependentModulesFromDsl(editingApplicationDSL)); - @Transient - private final Supplier> liveModules = memoize(() -> getDependentModulesFromDsl(getLiveApplicationDsl())); - - @Transient - private final Supplier liveContainerSize = memoize(() -> { - if (ApplicationType.APPLICATION.getValue() == getApplicationType()) { - return null; - } - return getContainerSizeFromDSL(getLiveApplicationDsl()); - }); - public Set getEditingQueries() { return editingQueries.get(); } - public Set getLiveQueries() { - return liveQueries.get(); + public Mono> getLiveQueries(ApplicationRecordService applicationRecordService) { + return getLiveApplicationDsl(applicationRecordService).mapNotNull(liveApplicationDSL -> JsonUtils.fromJsonSet(JsonUtils.toJson(liveApplicationDSL.get("queries")), ApplicationQuery.class)); } public Set getEditingModules() { return editingModules.get(); } - public Set getLiveModules() { - return liveModules.get(); + public Mono> getLiveModules(ApplicationRecordService applicationRecordService) { + return getLiveApplicationDsl(applicationRecordService).map(ApplicationUtil::getDependentModulesFromDsl); } public boolean isPublicToAll() { @@ -138,12 +123,12 @@ public boolean agencyProfile() { return BooleanUtils.toBooleanDefaultIfNull(agencyProfile, false); } - public ApplicationQuery getQueryByViewModeAndQueryId(boolean isViewMode, String queryId) { - return (isViewMode ? getLiveQueries() : getEditingQueries()) + public Mono getQueryByViewModeAndQueryId(boolean isViewMode, String queryId, ApplicationRecordService applicationRecordService) { + return getLiveQueries(applicationRecordService).map(liveQueries -> (isViewMode ? liveQueries : getEditingQueries()) .stream() .filter(query -> queryId.equals(query.getId()) || queryId.equals(query.getGid())) .findFirst() - .orElseThrow(() -> new BizException(BizError.QUERY_NOT_FOUND, "LIBRARY_QUERY_NOT_FOUND")); + .orElseThrow(() -> new BizException(BizError.QUERY_NOT_FOUND, "LIBRARY_QUERY_NOT_FOUND"))); } /** @@ -151,10 +136,10 @@ public ApplicationQuery getQueryByViewModeAndQueryId(boolean isViewMode, String */ @Transient @JsonIgnore - public Map getLiveApplicationDsl() { - var dsl = MapUtils.isEmpty(publishedApplicationDSL) ? editingApplicationDSL : publishedApplicationDSL; - if (dsl == null) dsl = new HashMap<>(); - return dsl; + public Mono> getLiveApplicationDsl(ApplicationRecordService applicationRecordService) { + return applicationRecordService.getLatestRecordByApplicationId(this.getId()) + .map(ApplicationRecord::getApplicationDSL) + .switchIfEmpty(Mono.just(editingApplicationDSL)); } public String getOrganizationId() { @@ -193,12 +178,17 @@ public String getCategory() { public Map getEditingApplicationDSLOrNull() {return editingApplicationDSL; } - public Object getLiveContainerSize() { - return liveContainerSize.get(); + public Mono getLiveContainerSize(ApplicationRecordService applicationRecordService) { + return getLiveApplicationDsl(applicationRecordService).flatMap(dsl -> { + if (ApplicationType.APPLICATION.getValue() == getApplicationType()) { + return Mono.empty(); + } + return Mono.just(getContainerSizeFromDSL(dsl)); + }); } - public Map getPublishedApplicationDSL() { - return publishedApplicationDSL; + public Mono> getPublishedApplicationDSL(ApplicationRecordService applicationRecordService) { + return applicationRecordService.getLatestRecordByApplicationId(this.getId()).map(ApplicationRecord::getApplicationDSL); } } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationCombineId.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationCombineId.java new file mode 100644 index 000000000..c07344f3a --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationCombineId.java @@ -0,0 +1,14 @@ +package org.lowcoder.domain.application.model; + +import org.apache.commons.lang.StringUtils; + +public record ApplicationCombineId(String applicationId, String applicationRecordId) { + + public boolean isUsingLiveRecord() { + return "latest".equals(applicationRecordId); + } + + public boolean isUsingEditingRecord() { + return StringUtils.isBlank(applicationRecordId) || "editing".equals(applicationRecordId); + } +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationRecord.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationRecord.java new file mode 100644 index 000000000..d6f57893f --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationRecord.java @@ -0,0 +1,27 @@ +package org.lowcoder.domain.application.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; +import org.lowcoder.sdk.models.HasIdAndAuditing; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Map; + +@Document +@Getter +@SuperBuilder +@Jacksonized +@NoArgsConstructor +public class ApplicationRecord extends HasIdAndAuditing { + + private String applicationId; + private String tag; + private String commitMessage; + private Map applicationDSL; + + public long getCreateTime() { + return createdAt.toEpochMilli(); + } +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationRecordRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationRecordRepository.java new file mode 100644 index 000000000..dc333f9ed --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationRecordRepository.java @@ -0,0 +1,23 @@ +package org.lowcoder.domain.application.repository; + + +import org.lowcoder.domain.application.model.ApplicationRecord; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Repository +public interface ApplicationRecordRepository extends ReactiveMongoRepository { + + Mono deleteByApplicationId(String applicationId); + + Flux findByApplicationId(String applicationId); + + Flux findByApplicationIdIn(List ids); + + Mono findTop1ByApplicationIdOrderByCreatedAtDesc(String applicationId); + +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationRecordService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationRecordService.java new file mode 100644 index 000000000..2afe65d8e --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationRecordService.java @@ -0,0 +1,23 @@ +package org.lowcoder.domain.application.service; + +import org.lowcoder.domain.application.model.ApplicationRecord; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; + +public interface ApplicationRecordService { + Mono insert(ApplicationRecord applicationRecord); + + Mono> getByApplicationId(String applicationId); + + Mono>> getByApplicationIdIn(List applicationIdList); + + Mono getById(String id); + + Mono getLatestRecordByApplicationId(String applicationId); + + Mono deleteAllApplicationTagByApplicationId(String applicationId); + + Mono deleteById(String id); +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationRecordServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationRecordServiceImpl.java new file mode 100644 index 000000000..9c7bcb482 --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationRecordServiceImpl.java @@ -0,0 +1,72 @@ +package org.lowcoder.domain.application.service; + +import lombok.RequiredArgsConstructor; +import org.lowcoder.domain.application.model.ApplicationRecord; +import org.lowcoder.domain.application.repository.ApplicationRecordRepository; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.lowcoder.sdk.exception.BizError.APPLICATION_NOT_FOUND; +import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; + +@RequiredArgsConstructor +@Service +public class ApplicationRecordServiceImpl implements ApplicationRecordService { + + private final ApplicationRecordRepository applicationRecordRepository; + + @Override + public Mono insert(ApplicationRecord applicationRecord) { + return applicationRecordRepository.save(applicationRecord); + } + + /** + * get all published versions + */ + @Override + public Mono> getByApplicationId(String applicationId) { + return applicationRecordRepository.findByApplicationId(applicationId) + .sort(Comparator.comparing(ApplicationRecord::getCreatedAt).reversed()) + .collectList(); + } + + @Override + public Mono>> getByApplicationIdIn(List applicationIdList) { + return applicationRecordRepository.findByApplicationIdIn(applicationIdList) + .sort(Comparator.comparing(ApplicationRecord::getCreatedAt).reversed()) + .collectList() + .map(applicationRecords -> applicationRecords.stream() + .collect(Collectors.groupingBy(ApplicationRecord::getApplicationId))); + } + + @Override + public Mono getById(String id) { + return applicationRecordRepository.findById(id) + .switchIfEmpty(deferredError(APPLICATION_NOT_FOUND, "APPLICATION_NOT_FOUND")); + } + + /** + * get the latest published version + */ + @Override + public Mono getLatestRecordByApplicationId(String applicationId) { + return applicationRecordRepository.findTop1ByApplicationIdOrderByCreatedAtDesc(applicationId); + } + + @Override + public Mono deleteAllApplicationTagByApplicationId(String applicationId) { + return applicationRecordRepository.deleteByApplicationId(applicationId); + } + + @Override + public Mono deleteById(String id) { + return applicationRecordRepository.deleteById(id); + } + + +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java index 85cf28dc5..f399a13d1 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java @@ -20,10 +20,6 @@ public interface ApplicationService { Mono updateById(String applicationId, Application application); - Mono updatePublishedApplicationDSL(String applicationId, Map applicationDSL); - - Mono publish(String applicationId); - Mono updateEditState(String applicationId, Boolean editingFinished); Mono create(Application newApplication, String visitorId); @@ -76,4 +72,5 @@ public interface ApplicationService { Flux findAll(); Mono updateLastEditedAt(String applicationId, Instant time, String visitorId); + Mono> getLiveDSLByApplicationId(String applicationId); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationServiceImpl.java index 50d6898dd..c65fef8e0 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationServiceImpl.java @@ -10,6 +10,7 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.lowcoder.domain.application.model.Application; +import org.lowcoder.domain.application.model.ApplicationRecord; import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.repository.ApplicationRepository; @@ -18,6 +19,9 @@ import org.lowcoder.domain.permission.model.ResourceRole; import org.lowcoder.domain.permission.model.ResourceType; import org.lowcoder.domain.permission.service.ResourcePermissionService; +import org.lowcoder.domain.query.model.LibraryQuery; +import org.lowcoder.domain.query.model.LibraryQueryRecord; +import org.lowcoder.domain.query.service.LibraryQueryRecordService; import org.lowcoder.domain.user.repository.UserRepository; import org.lowcoder.domain.user.service.UserService; import org.lowcoder.infra.annotation.NonEmptyMono; @@ -45,6 +49,7 @@ public class ApplicationServiceImpl implements ApplicationService { private final ResourcePermissionService resourcePermissionService; private final ApplicationRepository repository; private final UserRepository userRepository; + private final ApplicationRecordService applicationRecordService; @Override public Mono findById(String id) { @@ -77,23 +82,6 @@ public Mono updateById(String applicationId, Application application) { return mongoUpsertHelper.updateById(application, applicationId); } - - @Override - public Mono updatePublishedApplicationDSL(String applicationId, Map applicationDSL) { - Application application = Application.builder().publishedApplicationDSL(applicationDSL).build(); - return mongoUpsertHelper.updateById(application, applicationId); - } - - @Override - public Mono publish(String applicationId) { - return findById(applicationId) - .flatMap(newApplication -> { // copy editingApplicationDSL to publishedApplicationDSL - Map editingApplicationDSL = newApplication.getEditingApplicationDSL(); - return updatePublishedApplicationDSL(applicationId, editingApplicationDSL) - .thenReturn(newApplication); - }); - } - @Override public Mono updateEditState(String applicationId, Boolean editingFinished) { return findById(applicationId) @@ -153,8 +141,10 @@ public Mono> getAllDependentModulesFromApplicationId(String ap @Override public Mono> getAllDependentModulesFromApplication(Application application, boolean viewMode) { - Map dsl = viewMode ? application.getLiveApplicationDsl() : application.getEditingApplicationDSL(); - return getAllDependentModulesFromDsl(dsl); + return application.getLiveApplicationDsl(applicationRecordService).switchIfEmpty(Mono.just(new HashMap<>())).flatMap(liveApplicationDsl -> { + Map dsl = viewMode ? liveApplicationDsl : application.getEditingApplicationDSL(); + return getAllDependentModulesFromDsl(dsl); + }); } @Override @@ -169,12 +159,12 @@ public Mono> getAllDependentModulesFromDsl(Map } private Flux getDependentModules(Application module, Set circularDependencyCheckSet) { - return Flux.fromIterable(module.getLiveModules()) + return module.getLiveModules(applicationRecordService).flatMapMany(modules -> Flux.fromIterable(modules) .filter(moduleId -> !circularDependencyCheckSet.contains(moduleId)) .doOnNext(circularDependencyCheckSet::add) .collectList() .flatMapMany(this::findByIdIn) - .onErrorContinue((e, i) -> log.warn("get dependent modules on error continue , {}", e.getMessage())); + .onErrorContinue((e, i) -> log.warn("get dependent modules on error continue , {}", e.getMessage()))); } @Override @@ -355,4 +345,12 @@ public Mono updateLastEditedAt(String applicationId, Instant time, Stri .flatMap(repository::save) .hasElements(); } + + @Override + public Mono> getLiveDSLByApplicationId(String applicationId) { + return applicationRecordService.getLatestRecordByApplicationId(applicationId) + .map(ApplicationRecord::getApplicationDSL) + .switchIfEmpty(findById(applicationId) + .map(Application::getEditingApplicationDSL)); + } } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/solutions/TemplateSolutionServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/solutions/TemplateSolutionServiceImpl.java index d30e03fd2..e2d4d3db4 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/solutions/TemplateSolutionServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/solutions/TemplateSolutionServiceImpl.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.lowcoder.domain.application.model.Application; import org.lowcoder.domain.application.model.ApplicationStatus; +import org.lowcoder.domain.application.service.ApplicationRecordService; import org.lowcoder.domain.application.service.ApplicationService; import org.lowcoder.domain.datasource.model.Datasource; import org.lowcoder.domain.datasource.model.DatasourceCreationSource; @@ -44,6 +45,7 @@ public class TemplateSolutionServiceImpl implements TemplateSolutionService { private final DatasourceService datasourceService; @Lazy private final ApplicationService applicationService; + private final ApplicationRecordService applicationRecordService; @Override public Mono createFromTemplate(String templateId, String orgId, String visitorId) { @@ -59,20 +61,21 @@ public Mono createFromTemplate(String templateId, String orgId, Str String organizationId = tuple.getT2(); Application templateApplication = tuple.getT3(); List> datasourceIdMap = tuple.getT4(); - String dsl = JsonUtils.toJson(templateApplication.getLiveApplicationDsl()); - for (Pair stringStringPair : datasourceIdMap) { - dsl = dsl.replace(stringStringPair.getLeft(), stringStringPair.getRight()); - } - Map applicationDSL = JsonUtils.fromJsonMap(dsl); - Application application = Application.builder() - .applicationStatus(ApplicationStatus.NORMAL) - .gid(UuidCreator.getTimeOrderedEpoch().toString()) - .organizationId(organizationId) - .name(template.getName()) - .editingApplicationDSL(applicationDSL) - .publishedApplicationDSL(applicationDSL) - .build(); - return applicationService.create(application, visitorId); + return templateApplication.getLiveApplicationDsl(applicationRecordService).flatMap(liveApplicationDsl -> { + String dsl = JsonUtils.toJson(liveApplicationDsl); + for (Pair stringStringPair : datasourceIdMap) { + dsl = dsl.replace(stringStringPair.getLeft(), stringStringPair.getRight()); + } + Map applicationDSL = JsonUtils.fromJsonMap(dsl); + Application application = Application.builder() + .applicationStatus(ApplicationStatus.NORMAL) + .gid(UuidCreator.getTimeOrderedEpoch().toString()) + .organizationId(organizationId) + .name(template.getName()) + .editingApplicationDSL(applicationDSL) + .build(); + return applicationService.create(application, visitorId); + }); }); } @@ -91,17 +94,18 @@ public Mono> getTemplateApplicationIds(Collection applicatio */ private Mono>> copyDatasourceFromTemplateToCurrentOrganization(String currentOrganizationId, Application application, String visitorId) { - Set queries = application.getLiveQueries(); - if (isNull(queries)) { - return ofError(TEMPLATE_NOT_CORRECT, "TEMPLATE_NOT_CORRECT"); - } - Set datasourceIds = queries.stream() - .map(query -> query.getBaseQuery().getDatasourceId()) - .collect(Collectors.toSet()); - return Flux.fromIterable(datasourceIds) - .flatMap(datasourceId -> doCopyDatasource(currentOrganizationId, datasourceId, visitorId) - .map(copiedDatasourceId -> Pair.of(datasourceId, copiedDatasourceId))) - .collectList(); + return application.getLiveQueries(applicationRecordService).flatMap(queries -> { + if (isNull(queries)) { + return ofError(TEMPLATE_NOT_CORRECT, "TEMPLATE_NOT_CORRECT"); + } + Set datasourceIds = queries.stream() + .map(query -> query.getBaseQuery().getDatasourceId()) + .collect(Collectors.toSet()); + return Flux.fromIterable(datasourceIds) + .flatMap(datasourceId -> doCopyDatasource(currentOrganizationId, datasourceId, visitorId) + .map(copiedDatasourceId -> Pair.of(datasourceId, copiedDatasourceId))) + .collectList(); + }); } /** diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/constant/NewUrl.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/constant/NewUrl.java index 01eb05ccf..ba5d39975 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/constant/NewUrl.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/constant/NewUrl.java @@ -18,6 +18,7 @@ private NewUrl() { public static final String CUSTOM_AUTH = PREFIX + "/auth"; public static final String INVITATION_URL = PREFIX + "/invitation"; public static final String APPLICATION_URL = PREFIX + "/applications"; + public static final String APPLICATION_RECORD_URL = PREFIX + "/application-records"; public static final String APPLICATION_HISTORY_URL = PREFIX + "/application/history-snapshots"; public static final String QUERY_URL = PREFIX + "/query"; diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java index fa280173d..3b92146aa 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java @@ -63,6 +63,7 @@ public enum BizError { INVALID_HISTORY_SNAPSHOT(500, 5307), NO_PERMISSION_TO_REQUEST_APP(403, 5308), + APPLICATION_AND_ORG_NOT_MATCH(400, 5309), // datasource related, code range 5500 - 5600 DATASOURCE_NOT_FOUND(500, 5500), diff --git a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties index 78601b9a7..1c24e43d8 100644 --- a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties +++ b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties @@ -60,6 +60,7 @@ TEMPLATE_NOT_CORRECT=Sorry, the template has some errors. EXCEED_QUERY_REQUEST_SIZE=Sorry, it exceeds query request limit size, please contact administrator. EXCEED_QUERY_RESPONSE_SIZE=Sorry, it exceeds query response limit size, please contact administrator. LIBRARY_QUERY_AND_ORG_NOT_MATCH=Query library does not match the workspace. +APPLICATION_AND_ORG_NOT_MATCH=Application does not match the workspace. LIBRARY_QUERY_NOT_FOUND=Sorry, query library has no such query, please check again. INVALID_USER_STATUS=Sorry, the user status is illegal: {0}. FOLDER_OPERATE_NO_PERMISSION=Oops! It appears you don''t have operation permissions of the folder. diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java index e0990e134..0bc50ffc2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java @@ -3,6 +3,7 @@ import jakarta.annotation.Nonnull; import org.lowcoder.api.application.view.ApplicationInfoView; import org.lowcoder.api.application.view.ApplicationPermissionView; +import org.lowcoder.api.application.view.ApplicationPublishRequest; import org.lowcoder.api.application.view.ApplicationView; import org.lowcoder.domain.application.model.Application; import org.lowcoder.domain.application.model.ApplicationRequestType; @@ -33,7 +34,7 @@ public interface ApplicationApiService { Mono update(String applicationId, Application application); - Mono publish(String applicationId); + Mono publish(String applicationId, ApplicationPublishRequest applicationPublishRequest); Mono updateEditState(String applicationId, ApplicationEndpoints.UpdateEditStateRequest updateEditStateRequest); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java index 3724cc69b..8f26aec99 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java @@ -13,6 +13,7 @@ import org.lowcoder.api.application.ApplicationEndpoints.CreateApplicationRequest; import org.lowcoder.api.application.view.ApplicationInfoView; import org.lowcoder.api.application.view.ApplicationPermissionView; +import org.lowcoder.api.application.view.ApplicationPublishRequest; import org.lowcoder.api.application.view.ApplicationView; import org.lowcoder.api.bizthreshold.AbstractBizThresholdChecker; import org.lowcoder.api.home.FolderApiService; @@ -23,6 +24,7 @@ import org.lowcoder.api.usermanagement.OrgDevChecker; import org.lowcoder.domain.application.model.*; import org.lowcoder.domain.application.service.ApplicationHistorySnapshotService; +import org.lowcoder.domain.application.service.ApplicationRecordService; import org.lowcoder.domain.application.service.ApplicationService; import org.lowcoder.domain.datasource.model.Datasource; import org.lowcoder.domain.datasource.service.DatasourceService; @@ -91,6 +93,7 @@ public class ApplicationApiServiceImpl implements ApplicationApiService { private final PermissionHelper permissionHelper; private final DatasourceService datasourceService; private final ApplicationHistorySnapshotService applicationHistorySnapshotService; + private final ApplicationRecordService applicationRecordService; @Override public Mono create(CreateApplicationRequest createApplicationRequest) { @@ -100,7 +103,6 @@ public Mono create(CreateApplicationRequest createApplicationRe createApplicationRequest.name(), createApplicationRequest.applicationType(), NORMAL, - createApplicationRequest.publishedApplicationDSL(), createApplicationRequest.editingApplicationDSL(), false, false, false, "", Instant.now()); @@ -262,15 +264,18 @@ public Mono getEditingApplication(String applicationId) { List dependentModules = tuple.getT3(); Map commonSettings = tuple.getT4(); - Map> dependentModuleDsl = dependentModules.stream() - .collect(Collectors.toMap(Application::getId, Application::getLiveApplicationDsl, (a, b) -> b)); - return applicationService.updateById(applicationId, application).map(__ -> - ApplicationView.builder() - .applicationInfoView(buildView(application, permission.getResourceRole().getValue())) - .applicationDSL(application.getEditingApplicationDSL()) - .moduleDSL(dependentModuleDsl) - .orgCommonSettings(commonSettings) - .build()); + return Flux.fromIterable(dependentModules) + .flatMap(app -> app.getLiveApplicationDsl(applicationRecordService) + .map(dsl -> Map.entry(app.getId(), sanitizeDsl(dsl)))) + .collectMap(Map.Entry::getKey, Map.Entry::getValue) + .flatMap(dependentModuleDsl -> + applicationService.updateById(applicationId, application).map(__ -> + ApplicationView.builder() + .applicationInfoView(buildView(application, permission.getResourceRole().getValue())) + .applicationDSL(application.getEditingApplicationDSL()) + .moduleDSL(dependentModuleDsl) + .orgCommonSettings(commonSettings) + .build())); }); } @@ -283,21 +288,26 @@ public Mono getPublishedApplication(String applicationId, Appli .zipWhen(tuple -> applicationService.getAllDependentModulesFromApplication(tuple.getT2(), true), TupleUtils::merge) .zipWhen(tuple -> organizationService.getOrgCommonSettings(tuple.getT2().getOrganizationId()), TupleUtils::merge) .zipWith(getTemplateIdFromApplicationId(applicationId), TupleUtils::merge) - .map(tuple -> { + .flatMap(tuple -> { ResourcePermission permission = tuple.getT1(); Application application = tuple.getT2(); List dependentModules = tuple.getT3(); Map commonSettings = tuple.getT4(); String templateId = tuple.getT5(); - Map> dependentModuleDsl = dependentModules.stream() - .collect(Collectors.toMap(Application::getId, app -> sanitizeDsl(app.getLiveApplicationDsl()), (a, b) -> b)); - return ApplicationView.builder() - .applicationInfoView(buildView(application, permission.getResourceRole().getValue())) - .applicationDSL(sanitizeDsl(application.getLiveApplicationDsl())) - .moduleDSL(dependentModuleDsl) - .orgCommonSettings(commonSettings) - .templateId(templateId) - .build(); + return Flux.fromIterable(dependentModules) + .flatMap(app -> app.getLiveApplicationDsl(applicationRecordService) + .map(dsl -> Map.entry(app.getId(), sanitizeDsl(dsl)))) + .collectMap(Map.Entry::getKey, Map.Entry::getValue) + .flatMap(dependentModuleDsl -> + application.getLiveApplicationDsl(applicationRecordService).map(liveDsl -> + ApplicationView.builder() + .applicationInfoView(buildView(application, permission.getResourceRole().getValue())) + .applicationDSL(sanitizeDsl(liveDsl)) + .moduleDSL(dependentModuleDsl) + .orgCommonSettings(commonSettings) + .templateId(templateId) + .build()) + ); }) .delayUntil(applicationView -> { if (applicationView.getApplicationInfoView().getApplicationType() == ApplicationType.NAV_LAYOUT.getValue()) { @@ -352,15 +362,23 @@ private Mono doUpdateApplication(String applicationId, Application } @Override - public Mono publish(String applicationId) { + public Mono publish(String applicationId, ApplicationPublishRequest applicationPublishRequest) { return checkApplicationStatus(applicationId, NORMAL) .then(sessionUserService.getVisitorId()) .flatMap(userId -> resourcePermissionService.checkAndReturnMaxPermission(userId, applicationId, PUBLISH_APPLICATIONS)) - .flatMap(permission -> applicationService.publish(applicationId) + .delayUntil(__ -> applicationService.findById(applicationId) + .map(application -> ApplicationRecord.builder() + .tag(applicationPublishRequest.tag()) + .commitMessage(applicationPublishRequest.commitMessage()) + .applicationId(application.getId()) + .applicationDSL(application.getEditingApplicationDSL()) + .build()) + .flatMap(applicationRecordService::insert)) + .flatMap(permission -> applicationService.findById(applicationId) .map(applicationUpdated -> ApplicationView.builder() .applicationInfoView(buildView(applicationUpdated, permission.getResourceRole().getValue())) - .applicationDSL(applicationUpdated.getLiveApplicationDsl()) + .applicationDSL(applicationUpdated.getEditingApplicationDSL()) .build())); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index 55a23edb6..6edddcb2d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java @@ -1,10 +1,8 @@ package org.lowcoder.api.application; +import io.sentry.protocol.App; import lombok.RequiredArgsConstructor; -import org.lowcoder.api.application.view.ApplicationInfoView; -import org.lowcoder.api.application.view.ApplicationPermissionView; -import org.lowcoder.api.application.view.ApplicationView; -import org.lowcoder.api.application.view.MarketplaceApplicationInfoView; +import org.lowcoder.api.application.view.*; import org.lowcoder.api.framework.view.PageResponseView; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.home.SessionUserService; @@ -16,6 +14,7 @@ import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.model.ApplicationType; +import org.lowcoder.domain.application.service.ApplicationRecordService; import org.lowcoder.domain.folder.service.FolderElementRelationService; import org.lowcoder.domain.permission.model.ResourceRole; import org.springframework.web.bind.annotation.PathVariable; @@ -25,6 +24,7 @@ import reactor.core.publisher.Mono; import java.util.List; +import java.util.Objects; import static org.apache.commons.collections4.SetUtils.emptyIfNull; import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.*; @@ -41,6 +41,7 @@ public class ApplicationController implements ApplicationEndpoints { private final SessionUserService sessionUserService; private final GidService gidService; private final FolderElementRelationService folderElementRelationService; + private final ApplicationRecordService applicationRecordService; @Override public Mono> create(@RequestBody CreateApplicationRequest createApplicationRequest) { @@ -132,9 +133,28 @@ public Mono> update(@PathVariable String applicati } @Override - public Mono> publish(@PathVariable String applicationId) { + public Mono> publish(@PathVariable String applicationId, + @RequestBody(required = false) ApplicationPublishRequest applicationPublishRequest) { return gidService.convertApplicationIdToObjectId(applicationId).flatMap(appId -> - applicationApiService.publish(appId) + applicationRecordService.getLatestRecordByApplicationId(applicationId) + .map(applicationRecord -> { + String tag = applicationRecord.getTag(); // Assuming format is 1.0.0 + String newtag = "1.0.0"; + + if (tag != null && tag.matches("\\d+\\.\\d+\\.\\d+")) { // Validate tag format + String[] parts = tag.split("\\."); // Split by "." + int major = Integer.parseInt(parts[0]); + int minor = Integer.parseInt(parts[1]); + int patch = Integer.parseInt(parts[2]); + + patch++; // Increment the patch version + newtag = String.format("%d.%d.%d", major, minor, patch); + } + + return newtag; + }) + .switchIfEmpty(Mono.just("1.0.0")) + .flatMap(newtag -> applicationApiService.publish(appId, Objects.requireNonNullElse(applicationPublishRequest, new ApplicationPublishRequest("", newtag)))) .map(ResponseView::success)); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java index 78121eec4..a7f09da7d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java @@ -4,12 +4,10 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Nullable; import org.apache.commons.lang3.BooleanUtils; -import org.lowcoder.api.application.view.ApplicationInfoView; -import org.lowcoder.api.application.view.ApplicationPermissionView; -import org.lowcoder.api.application.view.ApplicationView; -import org.lowcoder.api.application.view.MarketplaceApplicationInfoView; +import org.lowcoder.api.application.view.*; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.home.UserHomepageView; +import org.lowcoder.api.query.view.LibraryQueryPublishRequest; import org.lowcoder.domain.application.model.Application; import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.infra.constant.NewUrl; @@ -135,7 +133,8 @@ public Mono> update(@PathVariable String applicati description = "Set a Lowcoder Application identified by its ID as available to all selected Users or User-Groups. This is similar to the classic deployment. The Lowcoder Apps gets published in production mode." ) @PostMapping("/{applicationId}/publish") - public Mono> publish(@PathVariable String applicationId); + public Mono> publish(@PathVariable String applicationId, + @RequestBody(required = false) ApplicationPublishRequest applicationPublishRequest); @Operation( tags = TAG_APPLICATION_MANAGEMENT, @@ -295,7 +294,6 @@ public record UpdatePermissionRequest(String role) { public record CreateApplicationRequest(@JsonProperty("orgId") String organizationId, String name, Integer applicationType, - Map publishedApplicationDSL, Map editingApplicationDSL, @Nullable String folderId) { } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java index b5a6381d7..b637f576e 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java @@ -10,6 +10,7 @@ import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.lowcoder.domain.application.service.ApplicationHistorySnapshotService; +import org.lowcoder.domain.application.service.ApplicationRecordService; import org.lowcoder.domain.application.service.ApplicationService; import org.lowcoder.domain.permission.model.ResourceAction; import org.lowcoder.domain.permission.service.ResourcePermissionService; @@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.Instant; @@ -38,6 +40,7 @@ public class ApplicationHistorySnapshotController implements ApplicationHistoryS private final SessionUserService sessionUserService; private final UserService userService; private final ApplicationService applicationService; + private final ApplicationRecordService applicationRecordService; @Override public Mono> create(@RequestBody ApplicationHistorySnapshotRequest request) { @@ -136,15 +139,18 @@ public Mono> getHistorySnapshotDsl(@PathVar .flatMap(__ -> applicationHistorySnapshotService.getHistorySnapshotDetail(snapshotId)) .map(ApplicationHistorySnapshot::getDsl) .zipWhen(applicationService::getAllDependentModulesFromDsl) - .map(tuple -> { + .flatMap(tuple -> { Map applicationDsl = tuple.getT1(); List dependentModules = tuple.getT2(); - Map> dependentModuleDsl = dependentModules.stream() - .collect(Collectors.toMap(Application::getId, Application::getLiveApplicationDsl, (a, b) -> b)); - return HistorySnapshotDslView.builder() - .applicationsDsl(applicationDsl) - .moduleDSL(dependentModuleDsl) - .build(); + return Flux.fromIterable(dependentModules) + .flatMap(app -> app.getLiveApplicationDsl(applicationRecordService) + .map(dsl -> Map.entry(app.getId(), dsl))) + .collectMap(Map.Entry::getKey, Map.Entry::getValue) + .map(dependentModuleDsl -> + HistorySnapshotDslView.builder() + .applicationsDsl(applicationDsl) + .moduleDSL(dependentModuleDsl) + .build()); }) .map(ResponseView::success); } @@ -158,15 +164,18 @@ public Mono> getHistorySnapshotDslArchived( .flatMap(__ -> applicationHistorySnapshotService.getHistorySnapshotDetailArchived(snapshotId)) .map(ApplicationHistorySnapshotTS::getDsl) .zipWhen(applicationService::getAllDependentModulesFromDsl) - .map(tuple -> { + .flatMap(tuple -> { Map applicationDsl = tuple.getT1(); List dependentModules = tuple.getT2(); - Map> dependentModuleDsl = dependentModules.stream() - .collect(Collectors.toMap(Application::getId, Application::getLiveApplicationDsl, (a, b) -> b)); - return HistorySnapshotDslView.builder() - .applicationsDsl(applicationDsl) - .moduleDSL(dependentModuleDsl) - .build(); + return Flux.fromIterable(dependentModules) + .flatMap(app -> app.getLiveApplicationDsl(applicationRecordService) + .map(dsl -> Map.entry(app.getId(), dsl))) + .collectMap(Map.Entry::getKey, Map.Entry::getValue) + .map(dependentModuleDsl -> + HistorySnapshotDslView.builder() + .applicationsDsl(applicationDsl) + .moduleDSL(dependentModuleDsl) + .build()); }) .map(ResponseView::success); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordApiService.java new file mode 100644 index 000000000..9ea29e9c8 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordApiService.java @@ -0,0 +1,16 @@ +package org.lowcoder.api.application; + +import org.lowcoder.api.application.view.ApplicationRecordMetaView; +import org.lowcoder.domain.application.model.ApplicationCombineId; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; + +public interface ApplicationRecordApiService { + Mono> getRecordDSLFromApplicationCombineId(ApplicationCombineId applicationCombineId); + + Mono delete(String id); + + Mono> getByApplicationId(String applicationId); +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordApiServiceImpl.java new file mode 100644 index 000000000..8d40e0e5d --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordApiServiceImpl.java @@ -0,0 +1,101 @@ +package org.lowcoder.api.application; + +import lombok.RequiredArgsConstructor; +import org.lowcoder.api.home.SessionUserService; +import org.lowcoder.api.application.ApplicationApiServiceImpl; +import org.lowcoder.api.application.view.ApplicationRecordMetaView; +import org.lowcoder.api.usermanagement.OrgDevChecker; +import org.lowcoder.domain.organization.model.OrgMember; +import org.lowcoder.domain.application.model.Application; +import org.lowcoder.domain.application.model.ApplicationCombineId; +import org.lowcoder.domain.application.model.ApplicationRecord; +import org.lowcoder.domain.application.service.ApplicationRecordService; +import org.lowcoder.domain.application.service.ApplicationService; +import org.lowcoder.domain.user.service.UserService; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; + +import static org.lowcoder.api.util.ViewBuilder.multiBuild; +import static org.lowcoder.sdk.exception.BizError.APPLICATION_AND_ORG_NOT_MATCH; +import static org.lowcoder.sdk.util.ExceptionUtils.ofError; + +@RequiredArgsConstructor +@Service +public class ApplicationRecordApiServiceImpl implements ApplicationRecordApiService { + + private final ApplicationService applicationService; + private final ApplicationRecordService applicationRecordService; + private final ApplicationApiServiceImpl applicationApiService; + private final SessionUserService sessionUserService; + private final OrgDevChecker orgDevChecker; + private final UserService userService; + + @Override + public Mono> getRecordDSLFromApplicationCombineId(ApplicationCombineId applicationCombineId) { + return checkApplicationRecordViewPermission(applicationCombineId) + .then(Mono.defer(() -> { + if (applicationCombineId.isUsingLiveRecord()) { + return applicationService.getLiveDSLByApplicationId(applicationCombineId.applicationId()); + } + return applicationRecordService.getById(applicationCombineId.applicationRecordId()) + .map(ApplicationRecord::getApplicationDSL); + })); + } + + @Override + public Mono delete(String id) { + return checkApplicationRecordManagementPermission(id) + .then(applicationRecordService.deleteById(id)); + } + + @Override + public Mono> getByApplicationId(String applicationId) { + return applicationRecordService.getByApplicationId(applicationId) + .flatMap(applicationRecords -> multiBuild(applicationRecords, + ApplicationRecord::getCreatedBy, + userService::getByIds, + ApplicationRecordMetaView::from + )); + } + + + Mono checkApplicationRecordManagementPermission(String applicationRecordId) { + return orgDevChecker.checkCurrentOrgDev() + .then(sessionUserService.getVisitorOrgMemberCache()) + .zipWith(applicationRecordService.getById(applicationRecordId) + .flatMap(applicationRecord -> applicationService.findById(applicationRecord.getApplicationId()))) + .flatMap(tuple2 -> { + OrgMember orgMember = tuple2.getT1(); + Application application = tuple2.getT2(); + if (!orgMember.getOrgId().equals(application.getOrganizationId())) { + return ofError(APPLICATION_AND_ORG_NOT_MATCH, "APPLICATION_AND_ORG_NOT_MATCH"); + } + return Mono.empty(); + }); + } + + Mono checkApplicationRecordViewPermission(ApplicationCombineId applicationCombineId) { + return sessionUserService.getVisitorOrgMemberCache() + .zipWith(Mono.defer(() -> { + if (applicationCombineId.isUsingLiveRecord()) { + return applicationService.findById(applicationCombineId.applicationId()); + } + return applicationRecordService.getById(applicationCombineId.applicationRecordId()) + .flatMap(applicationRecord -> applicationService.findById(applicationRecord.getApplicationId())); + + })) + .flatMap(tuple2 -> { + OrgMember orgMember = tuple2.getT1(); + Application application = tuple2.getT2(); + if (!orgMember.getOrgId().equals(application.getOrganizationId())) { + return ofError(APPLICATION_AND_ORG_NOT_MATCH, "APPLICATION_AND_ORG_NOT_MATCH"); + } + return Mono.empty(); + }); + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordController.java new file mode 100644 index 000000000..5addbe8d1 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordController.java @@ -0,0 +1,41 @@ +package org.lowcoder.api.application; + +import lombok.RequiredArgsConstructor; +import org.lowcoder.api.framework.view.ResponseView; +import org.lowcoder.api.application.ApplicationRecordApiService; +import org.lowcoder.api.application.view.ApplicationRecordMetaView; +import org.lowcoder.domain.application.model.ApplicationCombineId; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@RestController +public class ApplicationRecordController implements ApplicationRecordEndpoints +{ + private final ApplicationRecordApiService applicationRecordApiService; + + @Override + public Mono delete(@PathVariable String applicationRecordId) { + return applicationRecordApiService.delete(applicationRecordId); + } + + @Override + public Mono>> getByApplicationId(@RequestParam(name = "applicationId") String applicationId) { + return applicationRecordApiService.getByApplicationId(applicationId) + .map(ResponseView::success); + } + + @Override + public Mono>> dslById(@RequestParam(name = "applicationId") String applicationId, + @RequestParam(name = "applicationRecordId") String applicationRecordId) { + ApplicationCombineId applicationCombineId = new ApplicationCombineId(applicationId, applicationRecordId); + return applicationRecordApiService.getRecordDSLFromApplicationCombineId(applicationCombineId) + .map(ResponseView::success); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordEndpoints.java new file mode 100644 index 000000000..d1c760341 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationRecordEndpoints.java @@ -0,0 +1,48 @@ +package org.lowcoder.api.application; + +import io.swagger.v3.oas.annotations.Operation; +import org.lowcoder.api.framework.view.ResponseView; +import org.lowcoder.api.application.view.ApplicationRecordMetaView; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; + +import static org.lowcoder.infra.constant.NewUrl.APPLICATION_RECORD_URL; + +@RestController +@RequestMapping(value = APPLICATION_RECORD_URL) +public interface ApplicationRecordEndpoints +{ + public static final String TAG_APPLICATION_RECORDS = "Application Record APIs"; + + @Operation( + tags = TAG_APPLICATION_RECORDS, + operationId = "deleteApplicationRecord", + summary = "Delete Application Record", + description = "Permanently remove a specific Application Record from Lowcoder using its unique record ID." + ) + @DeleteMapping("/{applicationRecordId}") + public Mono delete(@PathVariable String applicationRecordId); + + @Operation( + tags = TAG_APPLICATION_RECORDS, + operationId = "getApplicationRecord", + summary = "Get Application Record", + description = "Retrieve a specific Application Record within Lowcoder using the associated application ID." + ) + @GetMapping("/listByApplicationId") + public Mono>> getByApplicationId(@RequestParam(name = "applicationId") String applicationId); + + @Operation( + tags = TAG_APPLICATION_RECORDS, + operationId = "listApplicationRecords", + summary = "Get Application Records", + description = "Retrieve a list of Application Records, which store information related to executed queries within Lowcoder and the current Organization / Workspace by the impersonated User" + ) + @GetMapping + public Mono>> dslById(@RequestParam(name = "applicationId") String applicationId, + @RequestParam(name = "applicationRecordId") String applicationRecordId); + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationPublishRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationPublishRequest.java new file mode 100644 index 000000000..f69cf3bb9 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationPublishRequest.java @@ -0,0 +1,5 @@ +package org.lowcoder.api.application.view; + +public record ApplicationPublishRequest(String commitMessage, String tag) { + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationRecordMetaView.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationRecordMetaView.java new file mode 100644 index 000000000..8b6ab4369 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationRecordMetaView.java @@ -0,0 +1,30 @@ +package org.lowcoder.api.application.view; + +import org.lowcoder.domain.application.model.ApplicationRecord; +import org.lowcoder.domain.user.model.User; + +public record ApplicationRecordMetaView(String id, + String applicationId, + String tag, + String commitMessage, + long createTime, + String creatorName) { + + public static ApplicationRecordMetaView from(ApplicationRecord applicationRecord) { + return new ApplicationRecordMetaView(applicationRecord.getId(), + applicationRecord.getApplicationId(), + applicationRecord.getTag(), + applicationRecord.getCommitMessage(), + applicationRecord.getCreatedAt().toEpochMilli(), + null); + } + + public static ApplicationRecordMetaView from(ApplicationRecord applicationRecord, User applicationRecordCreator) { + return new ApplicationRecordMetaView(applicationRecord.getId(), + applicationRecord.getApplicationId(), + applicationRecord.getTag(), + applicationRecord.getCommitMessage(), + applicationRecord.getCreatedAt().toEpochMilli(), + applicationRecordCreator.getName()); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/AdvancedMapUtils.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/AdvancedMapUtils.java index 7f2bf4c36..6fe769f73 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/AdvancedMapUtils.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/AdvancedMapUtils.java @@ -1,5 +1,8 @@ package org.lowcoder.api.authentication.util; +import org.bson.Document; + +import java.util.HashMap; import java.util.Map; public class AdvancedMapUtils { @@ -52,4 +55,23 @@ public static String getString(Map map, String key) { return current!=null?current.toString():null; } + + public static Map documentToMap(Document document) { + if (document == null) { + return new HashMap<>(); + } + + Map map = new HashMap<>(); + for (Map.Entry entry : document.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Document) { + // Recursively convert nested Document + map.put(entry.getKey(), documentToMap((Document) value)); + } else { + map.put(entry.getKey(), value); + } + } + return map; + } + } \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java index 4fe04695f..afa08b036 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java @@ -14,6 +14,7 @@ import org.lowcoder.domain.application.model.Application; import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.model.ApplicationType; +import org.lowcoder.domain.application.service.ApplicationRecordService; import org.lowcoder.domain.application.service.ApplicationService; import org.lowcoder.domain.bundle.model.Bundle; import org.lowcoder.domain.bundle.model.BundleElement; @@ -69,6 +70,7 @@ public class UserHomeApiServiceImpl implements UserHomeApiService { private final CommonConfig config; private final BundleElementRelationServiceImpl bundleElementRelationServiceImpl; private final BundleService bundleService; + private final ApplicationRecordService applicationRecordService; @Override public Mono buildUserProfileView(User user, ServerWebExchange exchange) { @@ -252,8 +254,8 @@ public Flux getAllAuthorisedApplications4CurrentOrgMember(@ .flatMap(positions -> { long position = positions.isEmpty() ? 0 : positions.get(0); ResourceRole resourceRole = resourcePermissionMap.get(application.getId()).getResourceRole(); - return Mono.just(buildView(application, resourceRole, userMap, applicationLastViewTimeMap.get(application.getId()), - position, withContainerSize)); + return buildView(application, resourceRole, userMap, applicationLastViewTimeMap.get(application.getId()), + position, withContainerSize); }); }); }); @@ -359,7 +361,7 @@ public Flux getAllMarketplaceApplications(@Nulla return applicationFlux .flatMap(application -> Mono.zip(Mono.just(application), userMapMono, orgMapMono)) - .map(tuple2 -> { + .flatMap(tuple2 -> { // build view Application application = tuple2.getT1(); Map userMap = tuple2.getT2(); @@ -379,19 +381,17 @@ public Flux getAllMarketplaceApplications(@Nulla .build(); // marketplace specific fields - Map settings = new HashMap<>(); - if (application.getPublishedApplicationDSL() != null) - { - settings.putAll((Map)application.getPublishedApplicationDSL().getOrDefault("settings", new HashMap<>())); - } - - marketplaceApplicationInfoView.setTitle((String)settings.getOrDefault("title", application.getName())); - marketplaceApplicationInfoView.setCategory((String)settings.get("category")); - marketplaceApplicationInfoView.setDescription((String)settings.get("description")); - marketplaceApplicationInfoView.setImage((String)settings.get("icon")); - - return marketplaceApplicationInfoView; - + return application.getPublishedApplicationDSL(applicationRecordService) + .map(pubishedApplicationDSL -> + (Map) new HashMap((Map) pubishedApplicationDSL.getOrDefault("settings", new HashMap<>()))) + .switchIfEmpty(Mono.just(new HashMap<>())) + .map(settings -> { + marketplaceApplicationInfoView.setTitle((String)settings.getOrDefault("title", application.getName())); + marketplaceApplicationInfoView.setCategory((String)settings.get("category")); + marketplaceApplicationInfoView.setDescription((String)settings.get("description")); + marketplaceApplicationInfoView.setImage((String)settings.get("icon")); + return marketplaceApplicationInfoView; + }); }); }); @@ -561,7 +561,7 @@ public Flux getAllAgencyProfileBundles() { }); } - private ApplicationInfoView buildView(Application application, ResourceRole maxRole, Map userMap, @Nullable Instant lastViewTime, + private Mono buildView(Application application, ResourceRole maxRole, Map userMap, @Nullable Instant lastViewTime, Long bundlePosition, boolean withContainerSize) { ApplicationInfoViewBuilder applicationInfoViewBuilder = ApplicationInfoView.builder() .applicationId(application.getId()) @@ -582,11 +582,14 @@ private ApplicationInfoView buildView(Application application, ResourceRole maxR .publicToMarketplace(application.isPublicToMarketplace()) .agencyProfile(application.agencyProfile()); if (withContainerSize) { - return applicationInfoViewBuilder - .containerSize(application.getLiveContainerSize()) - .build(); + return application.getLiveContainerSize(applicationRecordService).map(size -> applicationInfoViewBuilder + .containerSize(size) + .build()) + .switchIfEmpty(Mono.just(applicationInfoViewBuilder + .containerSize(null) + .build())); } - return applicationInfoViewBuilder.build(); + return Mono.just(applicationInfoViewBuilder.build()); } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/ApplicationQueryApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/ApplicationQueryApiServiceImpl.java index 8bc3270c4..773ebe1e2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/ApplicationQueryApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/ApplicationQueryApiServiceImpl.java @@ -7,6 +7,7 @@ import org.lowcoder.api.home.SessionUserService; import org.lowcoder.api.query.view.QueryExecutionRequest; import org.lowcoder.domain.application.model.Application; +import org.lowcoder.domain.application.service.ApplicationRecordService; import org.lowcoder.domain.application.service.ApplicationService; import org.lowcoder.domain.datasource.model.Datasource; import org.lowcoder.domain.datasource.service.DatasourceService; @@ -62,6 +63,7 @@ public class ApplicationQueryApiServiceImpl implements ApplicationQueryApiServic private final DatasourceService datasourceService; private final QueryExecutionService queryExecutionService; private final CommonConfig commonConfig; + private final ApplicationRecordService applicationRecordService; @Value("${server.port}") private int port; @@ -79,7 +81,7 @@ public Mono executeApplicationQuery(ServerWebExchange exch String queryId = queryExecutionRequest.getQueryId(); Mono appMono = applicationService.findById(appId).cache(); Mono appQueryMono = appMono - .map(app -> app.getQueryByViewModeAndQueryId(viewMode, queryId)) + .flatMap(app -> app.getQueryByViewModeAndQueryId(viewMode, queryId, applicationRecordService)) .cache(); Mono baseQueryMono = appQueryMono.flatMap(this::getBaseQuery).cache(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java index ff7e33e25..e502dbedc 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java @@ -9,9 +9,11 @@ import com.mongodb.client.result.DeleteResult; import lombok.extern.slf4j.Slf4j; import org.bson.Document; +import org.bson.types.ObjectId; import org.lowcoder.domain.application.model.Application; import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; +import org.lowcoder.domain.application.model.ApplicationRecord; import org.lowcoder.domain.bundle.model.Bundle; import org.lowcoder.domain.datasource.model.Datasource; import org.lowcoder.domain.datasource.model.DatasourceStructureDO; @@ -49,8 +51,10 @@ import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Set; +import static org.lowcoder.api.authentication.util.AdvancedMapUtils.documentToMap; import static org.lowcoder.domain.util.QueryDslUtils.fieldName; import static org.lowcoder.sdk.util.IDUtils.generate; @@ -422,6 +426,31 @@ public void populateEmailInUserConnections(MongockTemplate mongoTemplate, Common } + @ChangeSet(order = "028", id = "published-to-record", author = "Thomas") + public void publishedToRecord(MongockTemplate mongoTemplate, CommonConfig commonConfig) { + Query query = new Query(Criteria.where("publishedApplicationDSL").exists(true)); + + MongoCursor cursor = mongoTemplate.getCollection("application").find(query.getQueryObject()).iterator(); + + while (cursor.hasNext()) { + Document document = cursor.next(); + Document dsl = (Document) document.get("publishedApplicationDSL"); + ObjectId id = document.getObjectId("_id"); + String createdBy = document.getString("createdBy"); + Map dslMap = documentToMap(dsl); + ApplicationRecord record = ApplicationRecord.builder() + .applicationId(id.toHexString()) + .applicationDSL(dslMap) + .commitMessage("") + .tag("1.0.0") + .createdBy(createdBy) + .modifiedBy(createdBy) + .createdAt(Instant.now()) + .updatedAt(Instant.now()) + .build(); + mongoTemplate.insert(record); + } + } private void addGidField(MongockTemplate mongoTemplate, String collectionName) { // Create a query to match all documents diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceIntegrationTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceIntegrationTest.java index dc18765df..cb4d81808 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceIntegrationTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceIntegrationTest.java @@ -73,7 +73,6 @@ public void testCreateApplicationSuccess() { "org01", "app05", ApplicationType.APPLICATION.getValue(), - Map.of("comp", "table"), Map.of("comp", "list", "queries", Set.of(Map.of("datasourceId", datasource.getId()))), null)) .delayUntil(__ -> deleteMono) @@ -108,7 +107,6 @@ public void testUpdateApplicationFailedDueToLackOfDatasourcePermissions() { "org01", "app03", ApplicationType.APPLICATION.getValue(), - Map.of("comp", "table"), Map.of("comp", "list", "queries", Set.of(Map.of("datasourceId", datasource.getId()))), null)) .delayUntil(__ -> deleteMono) @@ -131,7 +129,7 @@ public void testUpdateApplicationFailedDueToLackOfDatasourcePermissions() { @Test @WithMockUser public void testUpdateEditingStateSuccess() { - Mono applicationViewMono = applicationApiService.create(new CreateApplicationRequest("org01", "app1", ApplicationType.APPLICATION.getValue(), Map.of("comp", "table"), Map.of("comp", "list"), null)); + Mono applicationViewMono = applicationApiService.create(new CreateApplicationRequest("org01", "app1", ApplicationType.APPLICATION.getValue(), Map.of("comp", "list"), null)); Mono updateEditStateMono = applicationViewMono.delayUntil(app -> applicationApiService.updateEditState(app.getApplicationInfoView().getApplicationId(), new ApplicationEndpoints.UpdateEditStateRequest(true))); Mono app = updateEditStateMono.flatMap(applicationView -> applicationApiService.getEditingApplication(applicationView.getApplicationInfoView().getApplicationId())); StepVerifier.create(app) diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceTest.java index f08b60759..ede7ed95a 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.TestInstance; import org.lowcoder.api.application.ApplicationEndpoints.CreateApplicationRequest; import org.lowcoder.api.application.view.ApplicationPermissionView; +import org.lowcoder.api.application.view.ApplicationPublishRequest; import org.lowcoder.api.application.view.ApplicationView; import org.lowcoder.api.common.InitData; import org.lowcoder.api.common.mockuser.WithMockUser; @@ -129,7 +130,7 @@ public void testDeleteNormalApplicationWithError() { private Mono createApplication(String name, String folderId) { CreateApplicationRequest createApplicationRequest = new CreateApplicationRequest("org01", name, ApplicationType.APPLICATION.getValue(), - Map.of("comp", "table"), Map.of("comp", "list"), folderId); + Map.of("comp", "list"), folderId); return applicationApiService.create(createApplicationRequest); } @@ -147,12 +148,12 @@ public void testPublishApplication() { // published dsl before publish StepVerifier.create(applicationIdMono.flatMap(id -> applicationApiService.getPublishedApplication(id, ApplicationRequestType.PUBLIC_TO_ALL))) - .assertNext(applicationView -> Assertions.assertEquals(Map.of("comp", "table"), applicationView.getApplicationDSL())) + .assertNext(applicationView -> Assertions.assertEquals(Map.of("comp", "list"), applicationView.getApplicationDSL())) .verifyComplete(); // publish applicationIdMono = applicationIdMono - .delayUntil(id -> applicationApiService.publish(id)); + .delayUntil(id -> applicationApiService.publish(id, new ApplicationPublishRequest("Test Publish", "1.0.0"))).cache(); // edit dsl after publish StepVerifier.create(applicationIdMono.flatMap(id -> applicationApiService.getEditingApplication(id))) @@ -163,6 +164,34 @@ public void testPublishApplication() { StepVerifier.create(applicationIdMono.flatMap(id -> applicationApiService.getPublishedApplication(id, ApplicationRequestType.PUBLIC_TO_ALL))) .assertNext(applicationView -> Assertions.assertEquals(Map.of("comp", "list"), applicationView.getApplicationDSL())) .verifyComplete(); + + // update + applicationIdMono = applicationIdMono + .delayUntil(id -> applicationApiService.update(id, Application.builder().editingApplicationDSL(Map.of("comp", "table")).build())).cache(); + + // edit dsl after publish + StepVerifier.create(applicationIdMono.flatMap(id -> applicationApiService.getEditingApplication(id))) + .assertNext(applicationView -> Assertions.assertEquals(Map.of("comp", "table"), applicationView.getApplicationDSL())) + .verifyComplete(); + + // published dsl after publish + StepVerifier.create(applicationIdMono.flatMap(id -> applicationApiService.getPublishedApplication(id, ApplicationRequestType.PUBLIC_TO_ALL))) + .assertNext(applicationView -> Assertions.assertEquals(Map.of("comp", "list"), applicationView.getApplicationDSL())) + .verifyComplete(); + + // publish + applicationIdMono = applicationIdMono + .delayUntil(id -> applicationApiService.publish(id, new ApplicationPublishRequest("Test Publish 2", "2.0.0"))).cache(); + + // edit dsl after publish + StepVerifier.create(applicationIdMono.flatMap(id -> applicationApiService.getEditingApplication(id))) + .assertNext(applicationView -> Assertions.assertEquals(Map.of("comp", "table"), applicationView.getApplicationDSL())) + .verifyComplete(); + + // published dsl after publish + StepVerifier.create(applicationIdMono.flatMap(id -> applicationApiService.getPublishedApplication(id, ApplicationRequestType.PUBLIC_TO_ALL))) + .assertNext(applicationView -> Assertions.assertEquals(Map.of("comp", "table"), applicationView.getApplicationDSL())) + .verifyComplete(); } @Test diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java index 09fa8a2b9..38bd7b044 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java @@ -28,7 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest -@ActiveProfiles("test") +@ActiveProfiles("testFolder") //@RunWith(SpringRunner.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class FolderApiServiceTest {