Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: API for search entities #29203

Merged
merged 23 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4a27a58
feat: API to fetch workspaces for homepage
abhvsn Nov 16, 2023
169fc5c
test: Add testcases
abhvsn Nov 17, 2023
b64713c
Merge branch 'release' into feat/add-workspece-homepage-api
abhvsn Nov 20, 2023
98bfa7b
Add testcases for domain sorter class
abhvsn Nov 20, 2023
88eae0e
Create basic skeleton with API contract
abhvsn Nov 21, 2023
257bd61
feat(RecentlyUsedEntityDTO): Introduce RecentlyUsedEntityDTO and impl…
abhvsn Nov 24, 2023
40f175e
test: Add testcases
abhvsn Nov 27, 2023
2a267ef
Merge branch 'release' into feat/add-get-applications-for-homepage-api
abhvsn Nov 28, 2023
3beeae6
Add testcases for updating the entities
abhvsn Nov 28, 2023
4962ab9
Merge branch 'release' into feat/add-get-applications-for-homepage-api
abhvsn Nov 28, 2023
042df3d
feat: Add migration to track recently used entities for user data
abhvsn Nov 28, 2023
c62ee13
feat: Add search entity functionality for homepage
abhvsn Nov 29, 2023
4eb0876
feat: Add entity selector option to enhance search functionality
abhvsn Nov 29, 2023
fbedec8
chore: Add indexing comment for search field method
abhvsn Dec 4, 2023
8d8dd6e
Merge branch 'release' into feat/search-api-homepage
abhvsn Dec 4, 2023
0a80de2
feat: Add SearchEntityHelper and its test cases
abhvsn Dec 4, 2023
2f9ea02
test: Add testcases and fix get default application when connected to…
abhvsn Dec 5, 2023
34ff3cf
feat: Trim the search string before fetching results from DB
abhvsn Dec 5, 2023
86c6462
Merge branch 'release' into feat/search-api-homepage
abhvsn Dec 5, 2023
5a6c1bb
chore: Resolve merge conflicts with release
abhvsn Dec 5, 2023
f86074a
Merge branch 'release' into feat/search-api-homepage
abhvsn Dec 6, 2023
fb606c2
chore: Resolve merge conflicts with release
abhvsn Dec 6, 2023
aadb679
refactor method name
abhvsn Dec 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.appsmith.server.dtos.GitAuthDTO;
import com.appsmith.server.services.CrudService;
import com.mongodb.client.result.UpdateResult;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.codec.multipart.Part;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -105,4 +107,11 @@ Mono<UpdateResult> setAppTheme(
Mono<Boolean> isApplicationConnectedToGit(String applicationId);

Mono<Void> updateProtectedBranches(String applicationId, List<String> protectedBranches);

Flux<Application> filterByEntityFields(
List<String> searchableEntityFields,
String searchString,
Pageable pageable,
Sort sort,
AclPermission permission);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.exceptions.util.DuplicateKeyExceptionUtils;
import com.appsmith.server.helpers.GitDeployKeyGenerator;
import com.appsmith.server.helpers.GitUtils;
import com.appsmith.server.helpers.ResponseUtils;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.migrations.ApplicationVersion;
Expand Down Expand Up @@ -225,14 +226,8 @@ public Flux<Application> findByWorkspaceIdAndDefaultApplicationsInRecentlyUsedOr
* - Applications that are not connected to Git.
* - Applications that, when connected, revert with default branch only.
*/
GitApplicationMetadata metadata = application.getGitApplicationMetadata();
return metadata == null
// When the ssh key is generated by user and then the connect app fails
|| (!StringUtils.hasLength(metadata.getDefaultBranchName())
&& !StringUtils.hasLength(metadata.getBranchName()))
// Default branched application
|| (StringUtils.hasLength(metadata.getBranchName())
&& metadata.getBranchName().equals(metadata.getDefaultBranchName()));
return !GitUtils.isApplicationConnectedToGit(application)
|| GitUtils.isDefaultBranchedApplication(application);
})
.map(responseUtils::updateApplicationWithDefaultResources));
}
Expand Down Expand Up @@ -1049,10 +1044,7 @@ public Mono<Boolean> isApplicationConnectedToGit(String applicationId) {
if (!StringUtils.hasLength(applicationId)) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ID));
}
return this.getById(applicationId)
.map(application -> application.getGitApplicationMetadata() != null
&& StringUtils.hasLength(
application.getGitApplicationMetadata().getRemoteUrl()));
return this.getById(applicationId).map(GitUtils::isApplicationConnectedToGit);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public class UrlCE {
public static final String USAGE_PULSE_URL = BASE_URL + VERSION + "/usage-pulse";
public static final String TENANT_URL = BASE_URL + VERSION + "/tenants";
public static final String CUSTOM_JS_LIB_URL = BASE_URL + VERSION + "/libraries";

public static final String PRODUCT_ALERT = BASE_URL + VERSION + "/product-alert";
public static final String SEARCH_ENTITY_URL = BASE_URL + VERSION + "/search-entities";

// Sub-paths
public static final String MOCKS = "/mocks";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.appsmith.server.controllers;

import com.appsmith.server.constants.Url;
import com.appsmith.server.controllers.ce.SearchEntityControllerCE;
import com.appsmith.server.searchentities.SearchEntitySolution;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping(Url.SEARCH_ENTITY_URL)
@RestController
public class SearchEntityController extends SearchEntityControllerCE {

public SearchEntityController(SearchEntitySolution searchEntitySolution) {
super(searchEntitySolution);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.appsmith.server.controllers.ce;

import com.appsmith.external.views.Views;
import com.appsmith.server.constants.Url;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.dtos.SearchEntityDTO;
import com.appsmith.server.searchentities.SearchEntitySolution;
import com.fasterxml.jackson.annotation.JsonView;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import reactor.core.publisher.Mono;

@RequestMapping(Url.SEARCH_ENTITY_URL)
@Slf4j
public class SearchEntityControllerCE {

private final SearchEntitySolution searchEntitySolution;

public SearchEntityControllerCE(SearchEntitySolution searchEntitySolution) {
this.searchEntitySolution = searchEntitySolution;
}

@JsonView(Views.Public.class)
@GetMapping("")
public Mono<ResponseDTO<SearchEntityDTO>> getAllUnpublishedActionCollections(
@RequestParam(required = false) String[] entities,
@RequestParam(required = false, defaultValue = "") String keyword,
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "20") int size) {
log.debug("Going to search for entities with search string: {}", keyword);
return searchEntitySolution
.searchEntity(entities, keyword, page, size, Boolean.TRUE)
abhvsn marked this conversation as resolved.
Show resolved Hide resolved
.map(resources -> new ResponseDTO<>(HttpStatus.OK.value(), resources, null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.appsmith.server.dtos;

import com.appsmith.server.dtos.ce.SearchEntityCE_DTO;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class SearchEntityDTO extends SearchEntityCE_DTO {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.appsmith.server.dtos.ce;

import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Workspace;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public class SearchEntityCE_DTO {
List<Application> applications;
List<Workspace> workspaces;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.appsmith.server.helpers;

import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.GitApplicationMetadata;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
Expand Down Expand Up @@ -102,4 +103,29 @@ public static String getDefaultBranchName(GitApplicationMetadata gitApplicationM
? gitApplicationMetadata.getBranchName()
: gitApplicationMetadata.getDefaultBranchName();
}

/**
* This method checks if the application is connected to git and is the default branched.
*
* @param application application to be checked
* @return true if the application is default branched, false otherwise
*/
public static boolean isDefaultBranchedApplication(Application application) {
GitApplicationMetadata metadata = application.getGitApplicationMetadata();
return isApplicationConnectedToGit(application)
&& !StringUtils.isEmptyOrNull(metadata.getBranchName())
&& metadata.getBranchName().equals(metadata.getDefaultBranchName());
}

/**
* This method checks if the application is connected to Git or not.
* @param application application to be checked
* @return true if the application is connected to Git, false otherwise
*/
public static boolean isApplicationConnectedToGit(Application application) {
GitApplicationMetadata metadata = application.getGitApplicationMetadata();
return metadata != null
&& !StringUtils.isEmptyOrNull(metadata.getDefaultApplicationId())
&& !StringUtils.isEmptyOrNull(metadata.getRemoteUrl());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.migrations.JsonSchemaVersions;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;

@RequiredArgsConstructor
@Slf4j
@Component
public class ResponseUtils {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.appsmith.server.migrations.db.ce;

import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Workspace;
import io.mongock.api.annotations.ChangeUnit;
import io.mongock.api.annotations.Execution;
import io.mongock.api.annotations.RollbackExecution;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.index.Index;

import static com.appsmith.server.migrations.DatabaseChangelog1.dropIndexIfExists;
import static com.appsmith.server.migrations.DatabaseChangelog1.ensureIndexes;
import static com.appsmith.server.migrations.DatabaseChangelog1.makeIndex;

@Slf4j
@ChangeUnit(order = "037", id = "add-compound-index-name-deleted", author = " ")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The author field in the @ChangeUnit annotation is empty. It should be filled with the author's name or identifier for tracking and accountability purposes.

public class Migration037AddCompoundIndexForNameAndDeletedAt {

private final MongoTemplate mongoTemplate;

private static final String NAME_DELETED_COMPOUND_INDEX = "name_deleted_compound_index";

public Migration037AddCompoundIndexForNameAndDeletedAt(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}

/**
* mandatory to declare, but we don't have a use-case for this yet.
*/
@RollbackExecution
public void rollbackExecution() {}

@Execution
public void addIndexInWorkspaceAndApplicationsCollection() {

Index namedDeletedAtIndex = makeIndex(FieldName.NAME, FieldName.DELETED, FieldName.DELETED_AT)
.named(NAME_DELETED_COMPOUND_INDEX);

try {
dropIndexIfExists(mongoTemplate, Workspace.class, NAME_DELETED_COMPOUND_INDEX);
ensureIndexes(mongoTemplate, Workspace.class, namedDeletedAtIndex);

dropIndexIfExists(mongoTemplate, Application.class, NAME_DELETED_COMPOUND_INDEX);
ensureIndexes(mongoTemplate, Application.class, namedDeletedAtIndex);
} catch (UncategorizedMongoDbException mongockException) {
log.error(
"An error occurred while creating the index : {}, skipping the addition of index because of {}.",
NAME_DELETED_COMPOUND_INDEX,
mongockException.getMessage());
} catch (Exception exception) {
log.error("An error occurred while creating the index : {}", NAME_DELETED_COMPOUND_INDEX, exception);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.appsmith.server.searchentities;

public interface SearchEntitySolution extends SearchEntitySolutionCE {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.appsmith.server.searchentities;

import com.appsmith.server.dtos.SearchEntityDTO;
import reactor.core.publisher.Mono;

public interface SearchEntitySolutionCE {
Mono<SearchEntityDTO> searchEntity(
String[] entities, String searchString, int page, int size, Boolean isRequestedForHomepage);
abhvsn marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.appsmith.server.searchentities;

import com.appsmith.server.applications.base.ApplicationService;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.SearchEntityDTO;
import com.appsmith.server.helpers.GitUtils;
import com.appsmith.server.helpers.ResponseUtils;
import com.appsmith.server.services.WorkspaceService;
import com.appsmith.server.solutions.ApplicationPermission;
import com.appsmith.server.solutions.WorkspacePermission;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

import static com.appsmith.server.searchentities.helpers.SearchEntityHelper.shouldSearchEntity;

@RequiredArgsConstructor
public class SearchEntitySolutionCEImpl implements SearchEntitySolutionCE {

private final WorkspaceService workspaceService;

private final ApplicationService applicationService;

private final WorkspacePermission workspacePermission;

private final ApplicationPermission applicationPermission;

private final ResponseUtils responseUtils;

/**
* This method searches for workspaces and applications based on the searchString provided.
* The search is performed with contains operator on the name field of the entities and is case-insensitive.
* The search results are sorted by the updated_at field in descending order.
* searchString = "test" will return all entities with name containing "test".
* e.g. "test_app", "test_workspace", "appTest", "wsTest_random" etc.
*
* @param entities The list of entities to search for. If null or empty, all entities are searched.
* @param searchString The string to search for in the name field of the entities.
* @param page The page number of the results to return.
* @param size Max number of results to return within each entity.
* @param isRequestedForHomepage Whether the search is requested for the homepage or not.
*
* @return A Mono of SearchEntityDTO containing the list of workspaces and applications.
*/
@Override
public Mono<SearchEntityDTO> searchEntity(
String[] entities, String searchString, int page, int size, Boolean isRequestedForHomepage) {
if (size == 0) {
return Mono.just(new SearchEntityDTO());
}
Pageable pageable = Pageable.ofSize(size).withPage(page);
Sort sort = Sort.by(Sort.Direction.DESC, FieldName.UPDATED_AT);
searchString = StringUtils.hasLength(searchString) ? searchString.trim() : "";
// If no entities are specified, search for all entities.
Mono<List<Workspace>> workspacesMono = Mono.just(new ArrayList<>());
if (shouldSearchEntity(Workspace.class, entities)) {
workspacesMono = workspaceService
.filterByEntityFields(
List.of(FieldName.NAME),
searchString,
pageable,
sort,
workspacePermission.getReadPermission())
.collectList();
}

Mono<List<Application>> applicationsMono = Mono.just(new ArrayList<>());
if (shouldSearchEntity(Application.class, entities)) {
applicationsMono = applicationService
.filterByEntityFields(
List.of(FieldName.NAME),
searchString,
pageable,
sort,
applicationPermission.getReadPermission())
.filter(application -> {
if (Boolean.FALSE.equals(isRequestedForHomepage)) {
return true;
}
/*
* As the applications are requested on homepage filter applications based on the following
* criteria:
* - Applications that are not connected to Git.
* OR
* - Applications that, when connected, revert with default branch only.
*/
return !GitUtils.isApplicationConnectedToGit(application)
|| GitUtils.isDefaultBranchedApplication(application);
})
.map(responseUtils::updateApplicationWithDefaultResources)
.collectList();
}

return Mono.zip(workspacesMono, applicationsMono).map(tuple2 -> {
SearchEntityDTO searchEntityDTO = new SearchEntityDTO();
searchEntityDTO.setWorkspaces(tuple2.getT1());
searchEntityDTO.setApplications(tuple2.getT2());
return searchEntityDTO;
});
}
}
Loading
Loading