Skip to content

Commit

Permalink
SITES-22111 - Extend core CIF graphQL resolver with new function/API …
Browse files Browse the repository at this point in the history
…to expose Errors (#1006)

* added getErrors() in AbstractRetriever
* provided implementation for getErrors() in existing retrievers
* added hasErrors() in AbstractRetriever
* added hasErrors() and getErrors()  in SearchResultSet
* updated unit tests
* added Javadoc
* improved error message in the Error response based on runtime exception
* fixed typo
  • Loading branch information
LSantha authored May 27, 2024
1 parent c2356a0 commit f8ba743
Show file tree
Hide file tree
Showing 19 changed files with 300 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,15 @@ private static GraphqlClient adaptToGraphqlClient(Resource resource) {
private static GraphqlResponse<Query, Error> newErrorResponse(Throwable throwable) {
GraphqlResponse<Query, Error> response = new GraphqlResponse<>();
Error error = new Error();
error.setMessage(throwable.getMessage());

StringBuilder sb = new StringBuilder();
sb.append(String.format("[%s: \"%s\"]", throwable.getClass().getName(), throwable.getMessage()));
while (throwable.getCause() != null) {
throwable = throwable.getCause();
sb.append(String.format(" Caused by: [%s: \"%s\"]", throwable.getClass().getName(), throwable.getMessage()));
}

error.setMessage(sb.toString());
error.setCategory(MagentoGraphqlClient.RUNTIME_ERROR_CATEGORY);
response.setErrors(Collections.singletonList(error));
return response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ protected void populate() {

GraphqlResponse<Query, Error> response = executeQuery();

if (CollectionUtils.isNotEmpty(response.getErrors())) {
errors = response.getErrors();
if (CollectionUtils.isNotEmpty(errors)) {
categories = Collections.emptyList();
product = Optional.empty();
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
@Version("5.0.0")
@Version("5.1.0")
package com.adobe.cq.commerce.core.components.models.productlist;

import org.osgi.annotation.versioning.Version;
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ protected GraphqlResponse<Query, Error> executeQuery() {
@Override
protected void populate() {
GraphqlResponse<Query, Error> response = executeQuery();
if (CollectionUtils.isEmpty(response.getErrors())) {
errors = response.getErrors();
if (CollectionUtils.isEmpty(errors)) {
Query rootQuery = response.getData();
categories = rootQuery.getCategoryList();
categories.sort(Comparator.comparing(c -> identifiers.indexOf(c.getUid().toString())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@ protected GraphqlResponse<Query, Error> executeQuery() {
@Override
protected void populate() {
GraphqlResponse<Query, Error> response = executeQuery();
if (CollectionUtils.isEmpty(response.getErrors())) {
errors = response.getErrors();
if (CollectionUtils.isEmpty(errors)) {
Query rootQuery = response.getData();
if (rootQuery.getCategoryList() != null && !rootQuery.getCategoryList().isEmpty()) {
category = Optional.of(rootQuery.getCategoryList().get(0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ protected ProductPriceQueryDefinition generatePriceQuery() {
protected void populate() {
// Get product list from response
GraphqlResponse<Query, Error> response = executeQuery();
if (CollectionUtils.isEmpty(response.getErrors())) {
errors = response.getErrors();
if (CollectionUtils.isEmpty(errors)) {
Query rootQuery = response.getData();
List<ProductInterface> products = rootQuery.getProducts().getItems();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ protected GraphqlResponse<Query, Error> executeQuery() {
protected void populate() {
// Get product list from response
GraphqlResponse<Query, Error> response = executeQuery();
if (CollectionUtils.isEmpty(response.getErrors())) {
errors = response.getErrors();
if (CollectionUtils.isEmpty(errors)) {
Query rootQuery = response.getData();
products = rootQuery.getProducts().getItems();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package com.adobe.cq.commerce.core.components.models.retriever;

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

import com.adobe.cq.commerce.core.components.client.MagentoGraphqlClient;
import com.adobe.cq.commerce.graphql.client.GraphqlResponse;
import com.adobe.cq.commerce.magento.graphql.Query;
Expand All @@ -24,6 +28,7 @@
* Abstract implementation of retriever that fetches data using GraphQL.
*/
public abstract class AbstractRetriever {
private static final List<Error> INITIAL_ERRORS = Collections.unmodifiableList(new ArrayList<>());

/**
* Generated or fully customized query.
Expand All @@ -34,6 +39,7 @@ public abstract class AbstractRetriever {
* Instance of the Magento GraphQL client.
*/
protected MagentoGraphqlClient client;
protected List<Error> errors = INITIAL_ERRORS;

public AbstractRetriever(MagentoGraphqlClient client) {
if (client == null) {
Expand All @@ -51,8 +57,36 @@ public void setQuery(String query) {
this.query = query;
}

/**
* Returns the errors encountered during the retrieval of GraphQL results.
*
* @return list of errors
*
* @throws IllegalStateException if this method is invoked earlier than {@link #populate()} on this instance
*/
public final List<Error> getErrors() {
if (errors == INITIAL_ERRORS) {
throw new IllegalStateException("The populate() method must be called and it must populate 'errors' before getErrors().");
}

return errors == null ? Collections.emptyList() : errors;
}

/**
* Checks whether there were any errors encountered during the retrieval of GraphQL results.
*
* @return {@code true} if there where any errors, {@code false} otherwise
*
* @throws IllegalStateException if this method is invoked earlier than {@link #populate()} on this instance
*/
public final boolean hasErrors() {
return !getErrors().isEmpty();
}

/**
* Executes the query and parses the response.
* Implementors should initialize the {@link #errors} field with the errors
* in the GraphQL response returned by {@link #executeQuery()}.
*/
abstract protected void populate();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
@Version("2.1.0")
@Version("2.2.0")
package com.adobe.cq.commerce.core.components.models.retriever;

import org.osgi.annotation.versioning.Version;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.adobe.cq.commerce.core.search.internal.models;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -29,6 +30,7 @@
import com.adobe.cq.commerce.core.search.models.SearchOptions;
import com.adobe.cq.commerce.core.search.models.SearchResultsSet;
import com.adobe.cq.commerce.core.search.models.SorterKey;
import com.adobe.cq.commerce.magento.graphql.gson.Error;

public class SearchResultsSetImpl implements SearchResultsSet {

Expand All @@ -38,6 +40,7 @@ public class SearchResultsSetImpl implements SearchResultsSet {
private List<SearchAggregation> searchAggregations = new ArrayList<>();
private Pager pager;
private SorterImpl sorter = new SorterImpl();
private List<Error> errors;

@Nonnull
@Override
Expand Down Expand Up @@ -144,4 +147,13 @@ public boolean hasSorting() {
List<SorterKey> keys = getSorter().getKeys();
return keys != null && !keys.isEmpty();
}

@Override
public List<Error> getErrors() {
return errors == null ? Collections.emptyList() : errors;
}

public void setErrors(List<Error> errors) {
this.errors = errors;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package com.adobe.cq.commerce.core.search.internal.services;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.SyntheticResource;
Expand Down Expand Up @@ -85,41 +87,58 @@ public List<FilterAttributeMetadata> retrieveCurrentlyAvailableCommerceFilters(f
return Optional.ofNullable(page.adaptTo(Resource.class))
.map(r -> new SyntheticResource(r.getResourceResolver(), r.getPath(), SearchFilterService.class.getName()))
.map(r -> r.adaptTo(MagentoGraphqlClient.class))
.map(this::retrieveCurrentlyAvailableCommerceFilters)
.map(magentoGraphqlClient -> retrieveCurrentlyAvailableCommerceFiltersInfo(magentoGraphqlClient).getLeft())
.orElseGet(Collections::emptyList);
}

public List<FilterAttributeMetadata> retrieveCurrentlyAvailableCommerceFilters(final SlingHttpServletRequest request, Page page) {
public Pair<List<FilterAttributeMetadata>, List<Error>> retrieveCurrentlyAvailableCommerceFiltersInfo(
final SlingHttpServletRequest request, Page page) {
// This is used to configure the cache in the GraphqlClient with a cache name of
// --> com.adobe.cq.commerce.core.search.services.SearchFilterService
return Optional.ofNullable(page.adaptTo(Resource.class))
.map(r -> new SyntheticResource(r.getResourceResolver(), r.getPath(), SearchFilterService.class.getName()))
.map(r -> new SlingHttpServletRequestWrapper(request) {
Resource r = page.adaptTo(Resource.class);
if (r != null) {
SyntheticResource syntheticResource = new SyntheticResource(r.getResourceResolver(), r.getPath(), SearchFilterService.class
.getName());
SlingHttpServletRequestWrapper slingHttpServletRequestWrapper = new SlingHttpServletRequestWrapper(request) {
@Override
public Resource getResource() {
return r;
return syntheticResource;
}
})
.map(r -> r.adaptTo(MagentoGraphqlClient.class))
.map(this::retrieveCurrentlyAvailableCommerceFilters)
.orElseGet(Collections::emptyList);
};
MagentoGraphqlClient magentoGraphqlClient = slingHttpServletRequestWrapper.adaptTo(MagentoGraphqlClient.class);
if (magentoGraphqlClient != null) {
Pair<List<FilterAttributeMetadata>, List<Error>> filterAttributeMetadataInfo = retrieveCurrentlyAvailableCommerceFiltersInfo(
magentoGraphqlClient);
return filterAttributeMetadataInfo;
}
}
return Pair.of(Collections.emptyList(), Collections.emptyList());
}

private List<FilterAttributeMetadata> retrieveCurrentlyAvailableCommerceFilters(MagentoGraphqlClient magentoGraphqlClient) {
private Pair<List<FilterAttributeMetadata>, List<Error>> retrieveCurrentlyAvailableCommerceFiltersInfo(
MagentoGraphqlClient magentoGraphqlClient) {
// First we query Magento for the required attribute and filter information
final List<__InputValue> availableFilters = fetchAvailableSearchFilters(magentoGraphqlClient);
final List<Attribute> attributes = fetchAttributeMetadata(magentoGraphqlClient, availableFilters);
final Pair<List<__InputValue>, List<Error>> availableFiltersInfo = fetchAvailableSearchFilters(magentoGraphqlClient);
final Pair<List<Attribute>, List<Error>> attributesInfo = fetchAttributeMetadata(magentoGraphqlClient, availableFiltersInfo
.getLeft());
final List<Error> errors = new ArrayList<>();
if (availableFiltersInfo.getRight() != null) {
errors.addAll(availableFiltersInfo.getRight());
}
if (attributesInfo.getRight() != null) {
errors.addAll(attributesInfo.getRight());
}
// Then we combine this data into a useful set of data usable by other systems
FilterAttributeMetadataConverter converter = new FilterAttributeMetadataConverter(attributes);
return availableFilters.stream().map(converter).collect(Collectors.toList());
FilterAttributeMetadataConverter converter = new FilterAttributeMetadataConverter(attributesInfo.getLeft());
return Pair.of(availableFiltersInfo.getLeft().stream().map(converter).collect(Collectors.toList()), errors);
}

private List<Attribute> fetchAttributeMetadata(final MagentoGraphqlClient magentoGraphqlClient,
private Pair<List<Attribute>, List<Error>> fetchAttributeMetadata(final MagentoGraphqlClient magentoGraphqlClient,
final List<__InputValue> availableFilters) {

if (magentoGraphqlClient == null) {
LOGGER.error("MagentoGraphQL client is null, unable to make query to fetch attribute metadata.");
return Collections.emptyList();
return Pair.of(Collections.emptyList(), Collections.emptyList());
}

List<AttributeInput> attributeInputs = availableFilters.stream().map(inputField -> {
Expand All @@ -144,11 +163,12 @@ private List<Attribute> fetchAttributeMetadata(final MagentoGraphqlClient magent
if (CollectionUtils.isNotEmpty(response.getErrors())) {
response.getErrors()
.forEach(err -> LOGGER.error("An error has occurred: {} ({})", err.getMessage(), err.getCategory()));
return Collections.emptyList();
return Pair.of(Collections.emptyList(), response.getErrors());
}

CustomAttributeMetadata cam = response.getData().getCustomAttributeMetadata();
return cam != null ? response.getData().getCustomAttributeMetadata().getItems() : Collections.emptyList();
return Pair.of(cam != null ? response.getData().getCustomAttributeMetadata().getItems() : Collections.emptyList(),
Collections.emptyList());
}

/**
Expand All @@ -157,11 +177,11 @@ private List<Attribute> fetchAttributeMetadata(final MagentoGraphqlClient magent
* @param magentoGraphqlClient client for making Magento GraphQL requests
* @return key value pair of the attribute code or identifier and filter type for that attribute
*/
private List<__InputValue> fetchAvailableSearchFilters(final MagentoGraphqlClient magentoGraphqlClient) {
private Pair<List<__InputValue>, List<Error>> fetchAvailableSearchFilters(final MagentoGraphqlClient magentoGraphqlClient) {

if (magentoGraphqlClient == null) {
LOGGER.error("MagentoGraphQL client is null, unable to make introspection call to fetch available filter attributes.");
return Collections.emptyList();
return Pair.of(Collections.emptyList(), Collections.emptyList());
}

__TypeQueryDefinition typeQuery = q -> q
Expand All @@ -179,10 +199,10 @@ private List<__InputValue> fetchAvailableSearchFilters(final MagentoGraphqlClien
if (CollectionUtils.isNotEmpty(response.getErrors())) {
response.getErrors()
.forEach(err -> LOGGER.error("An error has occurred: {} ({})", err.getMessage(), err.getCategory()));
return Collections.emptyList();
return Pair.of(Collections.emptyList(), response.getErrors());
}

__Type type = response.getData().__getType();
return type != null ? type.getInputFields() : Collections.emptyList();
return Pair.of(type != null ? type.getInputFields() : Collections.emptyList(), Collections.emptyList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,16 @@ public Pair<QueryQuery.CategoryListArgumentsDefinition, CategoryTreeQueryDefinit
category = categoryRetriever.fetchCategory();
}

List<Error> errors = new ArrayList<>();
// We will use the search filter service to retrieve all of the potential available filters the commerce system
// has available for querying against
List<FilterAttributeMetadata> availableFilters = searchFilterService.retrieveCurrentlyAvailableCommerceFilters(request, page);
Pair<List<FilterAttributeMetadata>, List<Error>> filterAttributeInfo = searchFilterService
.retrieveCurrentlyAvailableCommerceFiltersInfo(request, page);
List<FilterAttributeMetadata> availableFilters = filterAttributeInfo.getLeft();
SorterKey currentSorterKey = findSortKey(mutableSearchOptions);
if (filterAttributeInfo.getRight() != null) {
errors.addAll(filterAttributeInfo.getRight());
}

String productsQueryString = generateProductsQueryString(mutableSearchOptions, availableFilters, productQueryHook,
productAttributeFilterHook,
Expand All @@ -221,6 +227,7 @@ public Pair<QueryQuery.CategoryListArgumentsDefinition, CategoryTreeQueryDefinit

// If we have any errors returned we'll log them and return an empty search result
if (CollectionUtils.isNotEmpty(response.getErrors())) {
errors.addAll(response.getErrors());
response.getErrors()
.forEach(err -> LOGGER.error("An error has occurred: {} ({})", err.getMessage(), err.getCategory()));

Expand All @@ -240,6 +247,7 @@ public Pair<QueryQuery.CategoryListArgumentsDefinition, CategoryTreeQueryDefinit
searchResultsSet.setTotalResults(products.getTotalCount());
searchResultsSet.setProductListItems(productListItems);
searchResultsSet.setSearchAggregations(searchAggregations);
searchResultsSet.setErrors(errors);

return new ImmutablePair<>(category, searchResultsSet);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package com.adobe.cq.commerce.core.search.models;

import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand All @@ -23,6 +24,7 @@
import org.osgi.annotation.versioning.ConsumerType;

import com.adobe.cq.commerce.core.components.models.common.ProductListItem;
import com.adobe.cq.commerce.magento.graphql.gson.Error;

/**
* Represents a set of search results from a backend service. This would generally contain the actual {@link ProductListItem}s
Expand Down Expand Up @@ -113,4 +115,22 @@ public interface SearchResultsSet {
* @return {@code true} if the result set provides support for sorting of the results, {@code false} otherwise
*/
boolean hasSorting();

/**
* Get the list of errors that occurred during the search.
*
* @return the list of errors
*/
default List<Error> getErrors() {
return Collections.emptyList();
}

/**
* Check if there were errors during the search.
*
* @return {@code true} if the result set contains errors, {@code false} otherwise
*/
default boolean hasErrors() {
return getErrors() != null && !getErrors().isEmpty();
}
}
Loading

0 comments on commit f8ba743

Please sign in to comment.