diff --git a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogDataApiGraphQLSchemaBuilder.java b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogDataApiGraphQLSchemaBuilder.java index b8d03d87e..c65cd508e 100644 --- a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogDataApiGraphQLSchemaBuilder.java +++ b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogDataApiGraphQLSchemaBuilder.java @@ -287,6 +287,14 @@ private BuiltFieldDescriptor buildGetUnknownEntityField() { .build()) .forEach(getUnknownEntityFieldBuilder::argument); + final boolean localeArgumentNeeded = globalAttributes.stream() + .anyMatch(GlobalAttributeSchemaContract::isUniqueGloballyWithinLocale); + if (localeArgumentNeeded) { + getUnknownEntityFieldBuilder.argument(UnknownEntityHeaderDescriptor.LOCALE + .to(argumentBuilderTransformer) + .type(typeRef(LOCALE_ENUM.name()))); + } + getUnknownEntityFieldBuilder.argument(UnknownEntityHeaderDescriptor.JOIN.to(argumentBuilderTransformer)); return new BuiltFieldDescriptor( @@ -330,6 +338,14 @@ private BuiltFieldDescriptor buildListUnknownEntityField() { .build()) .forEach(listUnknownEntityFieldBuilder::argument); + final boolean localeArgumentNeeded = globalAttributes.stream() + .anyMatch(GlobalAttributeSchemaContract::isUniqueGloballyWithinLocale); + if (localeArgumentNeeded) { + listUnknownEntityFieldBuilder.argument(ListUnknownEntitiesHeaderDescriptor.LOCALE + .to(argumentBuilderTransformer) + .type(typeRef(LOCALE_ENUM.name()))); + } + listUnknownEntityFieldBuilder.argument(ListUnknownEntitiesHeaderDescriptor.JOIN.to(argumentBuilderTransformer)); return new BuiltFieldDescriptor( diff --git a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/model/UnknownEntityHeaderDescriptor.java b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/model/UnknownEntityHeaderDescriptor.java index c9e196cac..30d47e66e 100644 --- a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/model/UnknownEntityHeaderDescriptor.java +++ b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/model/UnknownEntityHeaderDescriptor.java @@ -34,6 +34,13 @@ */ public interface UnknownEntityHeaderDescriptor { + PropertyDescriptor LOCALE = PropertyDescriptor.builder() + .name("locale") + .description(""" + Parameter specifying desired locale of queried entity and its inner datasets + """) + // type is expected to be a locale enum + .build(); PropertyDescriptor JOIN = PropertyDescriptor.builder() .name("join") .description(""" diff --git a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/GetUnknownEntityDataFetcher.java b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/GetUnknownEntityDataFetcher.java index 3f07a234f..4345e37be 100644 --- a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/GetUnknownEntityDataFetcher.java +++ b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/GetUnknownEntityDataFetcher.java @@ -52,12 +52,14 @@ import lombok.extern.slf4j.Slf4j; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.Serializable; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @@ -145,6 +147,8 @@ private > FilterBy buildFilterBy(@Nonnull filterConstraints.add(attributeEquals(attribute.getKey().getName(), (A) attribute.getValue())); } + Optional.ofNullable(arguments.locale()).ifPresent(locale -> filterConstraints.add(entityLocaleEquals(locale))); + if (arguments.join() == QueryHeaderArgumentsJoinType.AND) { return filterBy(and(filterConstraints.toArray(FilterConstraint[]::new))); } else if (arguments.join() == QueryHeaderArgumentsJoinType.OR) { @@ -170,28 +174,32 @@ private Require buildRequire(@Nonnull DataFetchingEnvironment environment) { /** * Holds parsed GraphQL query arguments relevant for single entity query */ - private record Arguments(@Nonnull QueryHeaderArgumentsJoinType join, + private record Arguments(@Nullable Locale locale, + @Nonnull QueryHeaderArgumentsJoinType join, @Nonnull Map globallyUniqueAttributes) { private static Arguments from(@Nonnull DataFetchingEnvironment environment, @Nonnull CatalogSchemaContract catalogSchema) { - final HashMap arguments = new HashMap<>(environment.getArguments()); - // left over arguments are globally unique attribute filters as defined by schema - final Map globallyUniqueAttributes = extractUniqueAttributesFromArguments(arguments, catalogSchema); + final Map globallyUniqueAttributes = extractUniqueAttributesFromArguments(environment.getArguments(), catalogSchema); // validate that arguments contain at least one entity identifier if (globallyUniqueAttributes.isEmpty()) { throw new GraphQLInvalidArgumentException("Missing globally unique attribute to identify entity."); } + final Locale locale = environment.getArgument(UnknownEntityHeaderDescriptor.LOCALE.name()); + if (locale == null && + globallyUniqueAttributes.keySet().stream().anyMatch(GlobalAttributeSchemaContract::isUniqueGloballyWithinLocale)) { + throw new GraphQLInvalidArgumentException("Globally unique within locale attribute used but no locale was passed."); + } final QueryHeaderArgumentsJoinType join = environment.getArgument(UnknownEntityHeaderDescriptor.JOIN.name()); - return new Arguments(join, globallyUniqueAttributes); + return new Arguments(locale, join, globallyUniqueAttributes); } } private static Map extractUniqueAttributesFromArguments( - @Nonnull HashMap arguments, + @Nonnull Map arguments, @Nonnull CatalogSchemaContract catalogSchema ) { final Map uniqueAttributes = createHashMap(arguments.size()); diff --git a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/ListUnknownEntitiesDataFetcher.java b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/ListUnknownEntitiesDataFetcher.java index b64b9a16c..a02aa9f24 100644 --- a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/ListUnknownEntitiesDataFetcher.java +++ b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/ListUnknownEntitiesDataFetcher.java @@ -40,6 +40,7 @@ import io.evitadb.externalApi.graphql.api.catalog.GraphQLContextKey; import io.evitadb.externalApi.graphql.api.catalog.dataApi.model.ListUnknownEntitiesHeaderDescriptor; import io.evitadb.externalApi.graphql.api.catalog.dataApi.model.QueryHeaderArgumentsJoinType; +import io.evitadb.externalApi.graphql.api.catalog.dataApi.model.UnknownEntityHeaderDescriptor; import io.evitadb.externalApi.graphql.api.catalog.dataApi.resolver.constraint.EntityFetchRequireResolver; import io.evitadb.externalApi.graphql.api.catalog.dataApi.resolver.constraint.FilterConstraintResolver; import io.evitadb.externalApi.graphql.api.catalog.dataApi.resolver.constraint.OrderConstraintResolver; @@ -60,6 +61,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @@ -151,6 +153,8 @@ private > FilterBy buildFilterBy(@Nonnull filterConstraints.add(attributeInSet(attributeSchema.getName(), attributeValues)); } + Optional.ofNullable(arguments.locale()).ifPresent(locale -> filterConstraints.add(entityLocaleEquals(locale))); + if (arguments.join() == QueryHeaderArgumentsJoinType.AND) { return filterBy(and(filterConstraints.toArray(FilterConstraint[]::new))); } else if (arguments.join() == QueryHeaderArgumentsJoinType.OR) { @@ -184,29 +188,34 @@ private Require buildRequire(@Nonnull DataFetchingEnvironment environment, @Nonn /** * Holds parsed GraphQL query arguments relevant for single entity query */ - private record Arguments(@Nullable Integer limit, + private record Arguments(@Nullable Locale locale, + @Nullable Integer limit, @Nonnull QueryHeaderArgumentsJoinType join, @Nonnull Map> globallyUniqueAttributes) { private static Arguments from(@Nonnull DataFetchingEnvironment environment, @Nonnull CatalogSchemaContract catalogSchema) { - final HashMap arguments = new HashMap<>(environment.getArguments()); - - final Integer limit = (Integer) arguments.remove(ListUnknownEntitiesHeaderDescriptor.LIMIT.name()); - final QueryHeaderArgumentsJoinType join = (QueryHeaderArgumentsJoinType) arguments.get(ListUnknownEntitiesHeaderDescriptor.JOIN.name()); - // left over arguments are globally unique attribute filters as defined by schema - final Map> globallyUniqueAttributes = extractUniqueAttributesFromArguments(arguments, catalogSchema); + final Map> globallyUniqueAttributes = extractUniqueAttributesFromArguments(environment.getArguments(), catalogSchema); // validate that arguments contain at least one entity identifier if (globallyUniqueAttributes.isEmpty()) { throw new GraphQLInvalidArgumentException("Missing globally unique attribute to identify entity."); } - return new Arguments(limit, join, globallyUniqueAttributes); + final Locale locale = environment.getArgument(UnknownEntityHeaderDescriptor.LOCALE.name()); + if (locale == null && + globallyUniqueAttributes.keySet().stream().anyMatch(GlobalAttributeSchemaContract::isUniqueGloballyWithinLocale)) { + throw new GraphQLInvalidArgumentException("Globally unique within locale attribute used but no locale was passed."); + } + final Integer limit = environment.getArgument(ListUnknownEntitiesHeaderDescriptor.LIMIT.name()); + final QueryHeaderArgumentsJoinType join = environment.getArgument(ListUnknownEntitiesHeaderDescriptor.JOIN.name()); + + + return new Arguments(locale, limit, join, globallyUniqueAttributes); } private static Map> extractUniqueAttributesFromArguments( - @Nonnull HashMap arguments, + @Nonnull Map arguments, @Nonnull CatalogSchemaContract catalogSchema ) { final Map> uniqueAttributes = createHashMap(arguments.size()); diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/builder/DataApiEndpointBuilder.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/builder/DataApiEndpointBuilder.java index 309563d08..373d89fff 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/builder/DataApiEndpointBuilder.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/builder/DataApiEndpointBuilder.java @@ -192,7 +192,9 @@ public Optional buildGetUnknownEntityEndpoint(@Nonnull C queryParameters.add(UnknownEntityEndpointHeaderDescriptor.FILTER_JOIN .to(operationQueryParameterBuilderTransformer) .build()); - queryParameters.addAll(buildFetchQueryParametersForUnknownEntity(localized)); + + final boolean localeArgumentNeeded = globallyUniqueAttributes.stream().anyMatch(GlobalAttributeSchemaContract::isUniqueGloballyWithinLocale); + queryParameters.addAll(buildFetchQueryParametersForUnknownEntity(!localized || localeArgumentNeeded)); return Optional.of( newCatalogEndpoint(buildingContext.getSchema()) @@ -234,7 +236,9 @@ public Optional buildListUnknownEntityEndpoint(@Nonnull queryParameters.add(ListUnknownEntitiesEndpointHeaderDescriptor.FILTER_JOIN .to(operationQueryParameterBuilderTransformer) .build()); - queryParameters.addAll(buildFetchQueryParametersForUnknownEntity(localized)); + + final boolean localeArgumentNeeded = globallyUniqueAttributes.stream().anyMatch(GlobalAttributeSchemaContract::isUniqueGloballyWithinLocale); + queryParameters.addAll(buildFetchQueryParametersForUnknownEntity(!localized || localeArgumentNeeded)); return Optional.of( newCatalogEndpoint(buildingContext.getSchema()) @@ -302,11 +306,11 @@ public OpenApiCollectionEndpoint buildDeleteEntitiesByQueryEndpoint(@Nonnull Ent } @Nonnull - private List buildFetchQueryParametersForUnknownEntity(boolean localized) { + private List buildFetchQueryParametersForUnknownEntity(boolean needsLocale) { final List queryParameters = new ArrayList<>(8); //build fetch params - if (!localized) { + if (needsLocale) { queryParameters.add(FetchEntityEndpointHeaderDescriptor.LOCALE.to(operationQueryParameterBuilderTransformer).build()); } queryParameters.add(FetchEntityEndpointHeaderDescriptor.DATA_IN_LOCALES.to(operationQueryParameterBuilderTransformer).build()); diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/model/header/ListUnknownEntitiesEndpointHeaderDescriptor.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/model/header/ListUnknownEntitiesEndpointHeaderDescriptor.java index e28a886ed..dd24fb8f6 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/model/header/ListUnknownEntitiesEndpointHeaderDescriptor.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/model/header/ListUnknownEntitiesEndpointHeaderDescriptor.java @@ -32,7 +32,7 @@ * * @author Lukáš Hornych, FG Forrest a.s. (c) 2023 */ -public interface ListUnknownEntitiesEndpointHeaderDescriptor extends UnknownEntityEndpointHeaderDescriptor, FetchEntityEndpointHeaderDescriptor { +public interface ListUnknownEntitiesEndpointHeaderDescriptor extends UnknownEntityEndpointHeaderDescriptor { PropertyDescriptor LIMIT = PropertyDescriptor.builder() .name("limit") diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/model/header/UnknownEntityEndpointHeaderDescriptor.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/model/header/UnknownEntityEndpointHeaderDescriptor.java index 7b691017b..3975bbac2 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/model/header/UnknownEntityEndpointHeaderDescriptor.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/model/header/UnknownEntityEndpointHeaderDescriptor.java @@ -32,7 +32,7 @@ * * @author Lukáš Hornych, FG Forrest a.s. (c) 2023 */ -public interface UnknownEntityEndpointHeaderDescriptor { +public interface UnknownEntityEndpointHeaderDescriptor extends FetchEntityEndpointHeaderDescriptor { PropertyDescriptor FILTER_JOIN = PropertyDescriptor.builder() .name("filterJoin") diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/constraint/FilterByConstraintFromRequestQueryBuilder.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/constraint/FilterByConstraintFromRequestQueryBuilder.java index 564e9b602..ff9c7c5e0 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/constraint/FilterByConstraintFromRequestQueryBuilder.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/constraint/FilterByConstraintFromRequestQueryBuilder.java @@ -36,6 +36,9 @@ import io.evitadb.externalApi.rest.api.catalog.dataApi.model.header.QueryHeaderFilterArgumentsJoinType; import io.evitadb.externalApi.rest.api.catalog.dataApi.model.header.UnknownEntityEndpointHeaderDescriptor; import io.evitadb.externalApi.rest.exception.RestInternalError; +import io.evitadb.externalApi.rest.exception.RestInvalidArgumentException; +import io.evitadb.externalApi.rest.exception.RestQueryResolvingInternalError; +import io.evitadb.utils.Assert; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -49,12 +52,12 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; -import static io.evitadb.api.query.QueryConstraints.and; -import static io.evitadb.api.query.QueryConstraints.entityPrimaryKeyInSet; -import static io.evitadb.api.query.QueryConstraints.filterBy; -import static io.evitadb.api.query.QueryConstraints.or; +import static io.evitadb.api.query.QueryConstraints.*; import static io.evitadb.externalApi.api.ExternalApiNamingConventions.ARGUMENT_NAME_NAMING_CONVENTION; +import static io.evitadb.utils.CollectionUtils.createHashMap; /** * Creates {@link io.evitadb.api.query.filter.FilterBy} constraint for Evita query from request parameters. @@ -123,17 +126,18 @@ public static > FilterBy buildFilterByFor @Nonnull CatalogSchemaContract catalogSchema) { final List filterConstraints = new LinkedList<>(); - if (parameters.containsKey(FetchEntityEndpointHeaderDescriptor.LOCALE.name())) { - filterConstraints.add(QueryConstraints.entityLocaleEquals((Locale) parameters.get(FetchEntityEndpointHeaderDescriptor.LOCALE.name()))); - } + final Map uniqueAttributes = getGloballyUniqueAttributesFromParameters(parameters, catalogSchema); + uniqueAttributes.forEach((attributeSchema, attributeValue) -> { + final String name = attributeSchema.getNameVariant(ARGUMENT_NAME_NAMING_CONVENTION); + filterConstraints.add(QueryConstraints.attributeEquals(name, (A) attributeValue)); + }); - getGloballyUniqueAttributes(catalogSchema).stream() - .map(arg -> arg.getNameVariant(ARGUMENT_NAME_NAMING_CONVENTION)) - .forEach(name -> { - if (parameters.containsKey(name)) { - filterConstraints.add(QueryConstraints.attributeEquals(name, (A) parameters.get(name))); - } - }); + final Locale locale = (Locale) parameters.get(FetchEntityEndpointHeaderDescriptor.LOCALE.name()); + if (locale == null && + uniqueAttributes.keySet().stream().anyMatch(GlobalAttributeSchemaContract::isUniqueGloballyWithinLocale)) { + throw new RestInvalidArgumentException("Globally unique within locale attribute used but no locale was passed."); + } + Optional.ofNullable(locale).ifPresent(it -> filterConstraints.add(entityLocaleEquals(it))); final QueryHeaderFilterArgumentsJoinType filterJoin = (QueryHeaderFilterArgumentsJoinType) parameters.getOrDefault( UnknownEntityEndpointHeaderDescriptor.FILTER_JOIN.name(), @@ -162,22 +166,22 @@ public static > FilterBy buildFilterByFor @Nonnull CatalogSchemaContract catalogSchema) { final List filterConstraints = new LinkedList<>(); - if (parameters.containsKey(FetchEntityEndpointHeaderDescriptor.LOCALE.name())) { - filterConstraints.add(QueryConstraints.entityLocaleEquals((Locale) parameters.get(FetchEntityEndpointHeaderDescriptor.LOCALE.name()))); - } + final Map uniqueAttributes = getGloballyUniqueAttributesFromParameters(parameters, catalogSchema); + uniqueAttributes.forEach((attributeSchema, attributeValue) -> { + final String name = attributeSchema.getNameVariant(ARGUMENT_NAME_NAMING_CONVENTION); + if(attributeValue instanceof Object[] array) { + filterConstraints.add(QueryConstraints.attributeInSet(name, convertObjectArrayToSpecificArray(attributeSchema.getType(), array))); + } else { + filterConstraints.add(QueryConstraints.attributeEquals(name, (A) attributeValue)); + } + }); - getGloballyUniqueAttributes(catalogSchema) - .forEach(arg -> { - final String name = arg.getNameVariant(ARGUMENT_NAME_NAMING_CONVENTION); - if (parameters.containsKey(name)) { - final Object parameter = parameters.get(name); - if(parameter instanceof Object[] array) { - filterConstraints.add(QueryConstraints.attributeInSet(name, convertObjectArrayToSpecificArray(arg.getType(), array))); - } else { - filterConstraints.add(QueryConstraints.attributeEquals(name, (A) parameter)); - } - } - }); + final Locale locale = (Locale) parameters.get(FetchEntityEndpointHeaderDescriptor.LOCALE.name()); + if (locale == null && + uniqueAttributes.keySet().stream().anyMatch(GlobalAttributeSchemaContract::isUniqueGloballyWithinLocale)) { + throw new RestInvalidArgumentException("Globally unique within locale attribute used but no locale was passed."); + } + Optional.ofNullable(locale).ifPresent(it -> filterConstraints.add(entityLocaleEquals(it))); final QueryHeaderFilterArgumentsJoinType filterJoin = (QueryHeaderFilterArgumentsJoinType) parameters.getOrDefault( ListUnknownEntitiesEndpointHeaderDescriptor.FILTER_JOIN.name(), @@ -198,13 +202,36 @@ public static > FilterBy buildFilterByFor } @Nonnull - private static List getGloballyUniqueAttributes(CatalogSchemaContract catalogSchema) { - return catalogSchema - .getAttributes() - .values() - .stream() - .filter(GlobalAttributeSchemaContract::isUniqueGlobally) - .toList(); + private static Map getGloballyUniqueAttributesFromParameters(@Nonnull Map parameters, + @Nonnull CatalogSchemaContract catalogSchema) { + final Map uniqueAttributes = createHashMap(parameters.size()); + + for (Entry parameter : parameters.entrySet()) { + final String attributeName = parameter.getKey(); + final GlobalAttributeSchemaContract attributeSchema = catalogSchema + .getAttributeByName(attributeName, ARGUMENT_NAME_NAMING_CONVENTION) + .orElse(null); + if (attributeSchema == null) { + // not a attribute argument + continue; + } + Assert.isPremiseValid( + attributeSchema.isUniqueGlobally(), + () -> new RestQueryResolvingInternalError( + "Cannot find entity by non-unique attribute `" + attributeName + "`." + ) + ); + + final Object attributeValue = parameter.getValue(); + if (attributeValue == null) { + // ignore empty argument attributes + continue; + } + + uniqueAttributes.put(attributeSchema, attributeValue); + } + + return uniqueAttributes; } @SuppressWarnings("unchecked") diff --git a/evita_functional_tests/src/test/java/io/evitadb/externalApi/ExternalApiFunctionTestsSupport.java b/evita_functional_tests/src/test/java/io/evitadb/externalApi/ExternalApiFunctionTestsSupport.java index 525ea946a..b47bb392c 100644 --- a/evita_functional_tests/src/test/java/io/evitadb/externalApi/ExternalApiFunctionTestsSupport.java +++ b/evita_functional_tests/src/test/java/io/evitadb/externalApi/ExternalApiFunctionTestsSupport.java @@ -25,6 +25,7 @@ import io.evitadb.api.query.Query; import io.evitadb.api.requestResponse.EvitaResponse; +import io.evitadb.api.requestResponse.data.AttributesContract.AttributeValue; import io.evitadb.api.requestResponse.data.EntityClassifier; import io.evitadb.api.requestResponse.data.EntityContract; import io.evitadb.api.requestResponse.data.SealedEntity; @@ -59,6 +60,26 @@ */ public interface ExternalApiFunctionTestsSupport { + /** + * Returns value of "random" value in the dataset. + */ + default AttributeValue getRandomAttributeValueObject(@Nonnull List originalProductEntities, @Nonnull String attributeName) { + return getRandomAttributeValueObject(originalProductEntities, attributeName, 10); + } + + /** + * Returns value of "random" value in the dataset. + */ + default AttributeValue getRandomAttributeValueObject(@Nonnull List originalProductEntities, @Nonnull String attributeName, int order) { + return originalProductEntities + .stream() + .flatMap(it -> it.getAttributeValues(attributeName).stream()) + .filter(Objects::nonNull) + .skip(order) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Failed to localize `" + attributeName + "` attribute!")); + } + /** * Returns value of "random" value in the dataset. */ diff --git a/evita_functional_tests/src/test/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogGraphQLGetUnknownEntityQueryFunctionalTest.java b/evita_functional_tests/src/test/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogGraphQLGetUnknownEntityQueryFunctionalTest.java index 939c303bf..e106866a2 100644 --- a/evita_functional_tests/src/test/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogGraphQLGetUnknownEntityQueryFunctionalTest.java +++ b/evita_functional_tests/src/test/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogGraphQLGetUnknownEntityQueryFunctionalTest.java @@ -23,38 +23,34 @@ package io.evitadb.externalApi.graphql.api.catalog.dataApi; +import io.evitadb.api.requestResponse.data.AttributesContract.AttributeValue; import io.evitadb.api.requestResponse.data.EntityClassifier; -import io.evitadb.api.requestResponse.data.ReferenceContract; import io.evitadb.api.requestResponse.data.SealedEntity; import io.evitadb.core.Evita; -import io.evitadb.exception.EvitaInternalError; import io.evitadb.externalApi.api.catalog.dataApi.model.AttributesDescriptor; import io.evitadb.externalApi.api.catalog.dataApi.model.EntityDescriptor; -import io.evitadb.externalApi.api.catalog.dataApi.model.PriceDescriptor; -import io.evitadb.externalApi.api.catalog.dataApi.model.ReferenceDescriptor; import io.evitadb.test.Entities; import io.evitadb.test.annotation.UseDataSet; import io.evitadb.test.tester.GraphQLTester; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import javax.annotation.Nonnull; -import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.function.Predicate; import static io.evitadb.api.query.Query.query; import static io.evitadb.api.query.QueryConstraints.*; -import static io.evitadb.externalApi.graphql.api.testSuite.TestDataGenerator.ATTRIBUTE_MARKET_SHARE; +import static io.evitadb.externalApi.graphql.api.testSuite.TestDataGenerator.ATTRIBUTE_RELATIVE_URL; import static io.evitadb.externalApi.graphql.api.testSuite.TestDataGenerator.GRAPHQL_THOUSAND_PRODUCTS; import static io.evitadb.test.TestConstants.TEST_CATALOG; import static io.evitadb.test.builder.MapBuilder.map; -import static io.evitadb.test.generator.DataGenerator.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static io.evitadb.test.generator.DataGenerator.ATTRIBUTE_CODE; +import static io.evitadb.test.generator.DataGenerator.ATTRIBUTE_URL; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.nullValue; /** * Tests for GraphQL catalog unknown single entity query. @@ -106,6 +102,75 @@ void shouldReturnUnknownEntityByGloballyUniqueAttribute(Evita evita, GraphQLTest ); } + @Test + @UseDataSet(GRAPHQL_THOUSAND_PRODUCTS) + @DisplayName("Should return unknown entity by globally unique locale specific attribute without specifying collection") + void shouldReturnUnknownEntityByGloballyUniqueLocaleSpecificAttributeWithoutSpecifyingCollection(Evita evita, GraphQLTester tester, List originalProductEntities) { + final AttributeValue relativeUrl = getRandomAttributeValueObject(originalProductEntities, ATTRIBUTE_RELATIVE_URL); + + final EntityClassifier entity = getEntity( + evita, + query( + filterBy( + attributeEquals(ATTRIBUTE_RELATIVE_URL, relativeUrl.value()), + entityLocaleEquals(relativeUrl.key().locale()) + ) + ) + ); + + tester.test(TEST_CATALOG) + .document( + """ + query { + getEntity(relativeUrl: "%s", locale: %s) { + __typename + primaryKey + type + } + } + """, + relativeUrl.value(), + relativeUrl.key().locale().toString() + ) + .executeAndThen() + .statusCode(200) + .body(ERRORS_PATH, nullValue()) + .body( + GET_ENTITY_PATH, + equalTo( + map() + .e(TYPENAME_FIELD, EntityDescriptor.THIS_GLOBAL.name()) + .e(EntityDescriptor.PRIMARY_KEY.name(), entity.getPrimaryKey()) + .e(EntityDescriptor.TYPE.name(), Entities.PRODUCT) + .build() + ) + ); + } + + @Test + @UseDataSet(GRAPHQL_THOUSAND_PRODUCTS) + @DisplayName("Should return error when filtering by globally unique attribute without locale") + void shouldReturnErrorWhenFilteringByGloballyUniqueLocalSpecificAttributeWithoutLocale(Evita evita, GraphQLTester tester, List originalProductEntities) { + final AttributeValue relativeUrl = getRandomAttributeValueObject(originalProductEntities, ATTRIBUTE_RELATIVE_URL); + + tester.test(TEST_CATALOG) + .document( + """ + query { + getEntity(relativeUrl: "%s") { + __typename + primaryKey + type + } + } + """, + relativeUrl.value() + ) + .executeAndThen() + .statusCode(200) + .body(ERRORS_PATH, hasSize(greaterThan(0))); + } + @Test @UseDataSet(GRAPHQL_THOUSAND_PRODUCTS) @DisplayName("Should return version with entity") diff --git a/evita_functional_tests/src/test/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogGraphQLListUnknownEntitiesQueryFunctionalTest.java b/evita_functional_tests/src/test/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogGraphQLListUnknownEntitiesQueryFunctionalTest.java index 30d85c398..975c6ce8f 100644 --- a/evita_functional_tests/src/test/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogGraphQLListUnknownEntitiesQueryFunctionalTest.java +++ b/evita_functional_tests/src/test/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogGraphQLListUnknownEntitiesQueryFunctionalTest.java @@ -23,6 +23,8 @@ package io.evitadb.externalApi.graphql.api.catalog.dataApi; +import io.evitadb.api.requestResponse.data.AttributesContract.AttributeValue; +import io.evitadb.api.requestResponse.data.EntityClassifier; import io.evitadb.api.requestResponse.data.SealedEntity; import io.evitadb.core.Evita; import io.evitadb.exception.EvitaInternalError; @@ -43,6 +45,7 @@ import static io.evitadb.api.query.Query.query; import static io.evitadb.api.query.QueryConstraints.*; +import static io.evitadb.externalApi.graphql.api.testSuite.TestDataGenerator.ATTRIBUTE_RELATIVE_URL; import static io.evitadb.externalApi.graphql.api.testSuite.TestDataGenerator.GRAPHQL_THOUSAND_PRODUCTS; import static io.evitadb.test.TestConstants.TEST_CATALOG; import static io.evitadb.test.builder.MapBuilder.map; @@ -59,7 +62,7 @@ */ public class CatalogGraphQLListUnknownEntitiesQueryFunctionalTest extends CatalogGraphQLDataEndpointFunctionalTest { - private static final String ENTITY_LIST_PATH = "data.listEntity"; + private static final String LIST_ENTITY_PATH = "data.listEntity"; @Test @UseDataSet(GRAPHQL_THOUSAND_PRODUCTS) @@ -94,7 +97,7 @@ void shouldReturnUnknownEntityListByMultipleGloballyUniqueAttribute(GraphQLTeste .statusCode(200) .body(ERRORS_PATH, nullValue()) .body( - ENTITY_LIST_PATH, + LIST_ENTITY_PATH, equalTo( List.of( map() @@ -112,6 +115,79 @@ void shouldReturnUnknownEntityListByMultipleGloballyUniqueAttribute(GraphQLTeste ); } + + @Test + @UseDataSet(GRAPHQL_THOUSAND_PRODUCTS) + @DisplayName("Should return unknown entity by globally unique attribute") + void shouldReturnUnknownEntityByGloballyUniqueLocaleSpecificCodeWithoutSpecifyingCollection(Evita evita, GraphQLTester tester, List originalProductEntities) { + final AttributeValue relativeUrl = getRandomAttributeValueObject(originalProductEntities, ATTRIBUTE_RELATIVE_URL); + + final EntityClassifier entity = getEntity( + evita, + query( + filterBy( + attributeEquals(ATTRIBUTE_RELATIVE_URL, relativeUrl.value()), + entityLocaleEquals(relativeUrl.key().locale()) + ) + ) + ); + + tester.test(TEST_CATALOG) + .document( + """ + query { + listEntity(relativeUrl: ["%s"], locale: %s) { + __typename + primaryKey + type + } + } + """, + relativeUrl.value(), + relativeUrl.key().locale().toString() + ) + .executeAndThen() + .statusCode(200) + .body(ERRORS_PATH, nullValue()) + .body( + LIST_ENTITY_PATH, + equalTo( + List.of( + map() + .e(TYPENAME_FIELD, EntityDescriptor.THIS_GLOBAL.name()) + .e(EntityDescriptor.PRIMARY_KEY.name(), entity.getPrimaryKey()) + .e(EntityDescriptor.TYPE.name(), Entities.PRODUCT) + .build() + ) + ) + ); + } + + @Test + @UseDataSet(GRAPHQL_THOUSAND_PRODUCTS) + @DisplayName("Should return unknown entity by globally unique attribute") + void shouldReturnErrorWhenFilteringByGloballyUniqueLocalSpecificAttributeWithoutLocale(Evita evita, GraphQLTester tester, List originalProductEntities) { + final AttributeValue relativeUrl = getRandomAttributeValueObject(originalProductEntities, ATTRIBUTE_RELATIVE_URL); + + tester.test(TEST_CATALOG) + .document( + """ + query { + listEntity(relativeUrl: ["%s"]) { + __typename + primaryKey + type + } + } + """, + relativeUrl.value() + ) + .executeAndThen() + .statusCode(200) + .body(ERRORS_PATH, hasSize(greaterThan(0))); + } + + @Test @UseDataSet(GRAPHQL_THOUSAND_PRODUCTS) @DisplayName("Should return entity versions") @@ -156,7 +232,7 @@ void shouldReturnEntityVersions(Evita evita, GraphQLTester tester, List whichIs.sortable().uniqueGlobally()) .withAttribute(ATTRIBUTE_URL, String.class, whichIs -> whichIs.localized().uniqueGlobally()) + .withAttribute(ATTRIBUTE_RELATIVE_URL, String.class, whichIs -> whichIs.localized().uniqueGloballyWithinLocale().nullable()) .updateVia(session); final DataGenerator dataGenerator = new DataGenerator(); @@ -181,6 +183,7 @@ public static DataCarrier generateMainCatalogEntities(@Nonnull Evita evita, int schemaBuilder -> { schemaBuilder .withDescription("This is a description") + .withGlobalAttribute(ATTRIBUTE_RELATIVE_URL) .withAttribute(ATTRIBUTE_QUANTITY, BigDecimal.class, whichIs -> whichIs .withDescription("This is a description") .filterable() diff --git a/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/catalog/dataApi/CatalogRestGetUnknownEntityQueryFunctionalTest.java b/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/catalog/dataApi/CatalogRestGetUnknownEntityQueryFunctionalTest.java index 5ff50343b..8ce61b14c 100644 --- a/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/catalog/dataApi/CatalogRestGetUnknownEntityQueryFunctionalTest.java +++ b/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/catalog/dataApi/CatalogRestGetUnknownEntityQueryFunctionalTest.java @@ -24,6 +24,7 @@ package io.evitadb.externalApi.rest.api.catalog.dataApi; import io.evitadb.api.query.require.PriceContentMode; +import io.evitadb.api.requestResponse.data.AttributesContract.AttributeValue; import io.evitadb.api.requestResponse.data.EntityClassifier; import io.evitadb.api.requestResponse.data.SealedEntity; import io.evitadb.core.Evita; @@ -42,6 +43,7 @@ import static io.evitadb.api.query.Query.query; import static io.evitadb.api.query.QueryConstraints.*; +import static io.evitadb.externalApi.rest.api.testSuite.TestDataGenerator.ATTRIBUTE_RELATIVE_URL; import static io.evitadb.externalApi.rest.api.testSuite.TestDataGenerator.REST_THOUSAND_PRODUCTS; import static io.evitadb.test.TestConstants.TEST_CATALOG; import static io.evitadb.test.builder.MapBuilder.map; @@ -85,6 +87,52 @@ void shouldReturnUnknownEntityByGloballyUniqueAttribute(Evita evita, List originalProductEntities, RestTester tester) { + final AttributeValue relativeUrl = getRandomAttributeValueObject(originalProductEntities, ATTRIBUTE_RELATIVE_URL); + + final EntityClassifier entity = getEntity( + evita, + query( + filterBy( + attributeEquals(ATTRIBUTE_RELATIVE_URL, relativeUrl.value()), + entityLocaleEquals(relativeUrl.key().locale()) + ) + ) + ); + + tester.test(TEST_CATALOG) + .urlPathSuffix("/entity/get") + .httpMethod(Request.METHOD_GET) + .requestParams(map() + .e(ATTRIBUTE_RELATIVE_URL, relativeUrl.value()) + .e(FetchEntityEndpointHeaderDescriptor.LOCALE.name(), relativeUrl.key().locale().toLanguageTag()) + .e(FetchEntityEndpointHeaderDescriptor.BODY_FETCH.name(), false) + .build()) + .executeAndThen() + .statusCode(200) + .body("", equalTo(createEntityDto(entity))); + } + + @Test + @UseDataSet(TestDataGenerator.REST_THOUSAND_PRODUCTS) + @DisplayName("Should return error when filtering by globally unique local specific attribute without locale") + void shouldReturnErrorWhenFilteringByGloballyUniqueLocalSpecificAttributeWithoutLocale(Evita evita, List originalProductEntities, RestTester tester) { + final AttributeValue relativeUrl = getRandomAttributeValueObject(originalProductEntities, ATTRIBUTE_RELATIVE_URL); + + tester.test(TEST_CATALOG) + .urlPathSuffix("/entity/get") + .httpMethod(Request.METHOD_GET) + .requestParams(map() + .e(ATTRIBUTE_RELATIVE_URL, relativeUrl.value()) + .e(FetchEntityEndpointHeaderDescriptor.BODY_FETCH.name(), false) + .build()) + .executeAndThen() + .statusCode(400); + } + @Test @UseDataSet(TestDataGenerator.REST_THOUSAND_PRODUCTS) @DisplayName("Should return unknown entity with multiple different global attributes") diff --git a/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/catalog/dataApi/CatalogRestListUnknownEntitiesQueryFunctionalTest.java b/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/catalog/dataApi/CatalogRestListUnknownEntitiesQueryFunctionalTest.java index 0627ecba8..e4f4b53b9 100644 --- a/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/catalog/dataApi/CatalogRestListUnknownEntitiesQueryFunctionalTest.java +++ b/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/catalog/dataApi/CatalogRestListUnknownEntitiesQueryFunctionalTest.java @@ -25,11 +25,13 @@ import io.evitadb.api.query.require.PriceContentMode; import io.evitadb.api.requestResponse.EvitaResponse; +import io.evitadb.api.requestResponse.data.AttributesContract.AttributeValue; import io.evitadb.api.requestResponse.data.EntityClassifier; import io.evitadb.api.requestResponse.data.SealedEntity; import io.evitadb.core.Evita; import io.evitadb.externalApi.rest.api.catalog.dataApi.model.header.FetchEntityEndpointHeaderDescriptor; import io.evitadb.externalApi.rest.api.catalog.dataApi.model.header.ListUnknownEntitiesEndpointHeaderDescriptor; +import io.evitadb.externalApi.rest.api.catalog.dataApi.model.header.QueryHeaderFilterArgumentsJoinType; import io.evitadb.externalApi.rest.api.testSuite.TestDataGenerator; import io.evitadb.test.Entities; import io.evitadb.test.annotation.UseDataSet; @@ -44,6 +46,7 @@ import static io.evitadb.api.query.Query.query; import static io.evitadb.api.query.QueryConstraints.*; +import static io.evitadb.externalApi.rest.api.testSuite.TestDataGenerator.ATTRIBUTE_RELATIVE_URL; import static io.evitadb.externalApi.rest.api.testSuite.TestDataGenerator.REST_THOUSAND_PRODUCTS; import static io.evitadb.test.TestConstants.TEST_CATALOG; import static io.evitadb.test.builder.MapBuilder.map; @@ -165,7 +168,7 @@ void shouldReturnUnknownEntityListByMultipleDifferentGlobalAttributes(Evita evit .requestParams(map() .e(ATTRIBUTE_CODE, List.of(codeAttribute)) .e(ATTRIBUTE_URL, List.of(urlAttribute)) - .e(ListUnknownEntitiesEndpointHeaderDescriptor.FILTER_JOIN.name(), "OR") + .e(ListUnknownEntitiesEndpointHeaderDescriptor.FILTER_JOIN.name(), QueryHeaderFilterArgumentsJoinType.OR.toString()) .build()) .executeAndThen() .statusCode(200) @@ -180,6 +183,52 @@ void shouldReturnUnknownEntityListByMultipleDifferentGlobalAttributes(Evita evit ); } + @Test + @UseDataSet(TestDataGenerator.REST_THOUSAND_PRODUCTS) + @DisplayName("Should return unknown entity by globally unique locale specific attribute without specifying collection") + void shouldReturnUnknownEntityByGloballyUniqueLocaleSpecificAttributeWithoutSpecifyingCollection(Evita evita, List originalProductEntities, RestTester tester) { + final AttributeValue relativeUrl = getRandomAttributeValueObject(originalProductEntities, ATTRIBUTE_RELATIVE_URL); + + final EntityClassifier entity = getEntity( + evita, + query( + filterBy( + attributeEquals(ATTRIBUTE_RELATIVE_URL, relativeUrl.value()), + entityLocaleEquals(relativeUrl.key().locale()) + ) + ) + ); + + tester.test(TEST_CATALOG) + .urlPathSuffix("/entity/list") + .httpMethod(Request.METHOD_GET) + .requestParams(map() + .e(ATTRIBUTE_RELATIVE_URL, List.of(relativeUrl.value())) + .e(FetchEntityEndpointHeaderDescriptor.LOCALE.name(), relativeUrl.key().locale().toLanguageTag()) + .e(FetchEntityEndpointHeaderDescriptor.BODY_FETCH.name(), false) + .build()) + .executeAndThen() + .statusCode(200) + .body("", equalTo(List.of(createEntityDto(entity)))); + } + + @Test + @UseDataSet(TestDataGenerator.REST_THOUSAND_PRODUCTS) + @DisplayName("Should return error when filtering by globally unique local specific attribute without locale") + void shouldReturnErrorWhenFilteringByGloballyUniqueLocalSpecificAttributeWithoutLocale(Evita evita, List originalProductEntities, RestTester tester) { + final AttributeValue relativeUrl = getRandomAttributeValueObject(originalProductEntities, ATTRIBUTE_RELATIVE_URL); + + tester.test(TEST_CATALOG) + .urlPathSuffix("/entity/list") + .httpMethod(Request.METHOD_GET) + .requestParams(map() + .e(ATTRIBUTE_RELATIVE_URL, List.of(relativeUrl.value())) + .e(FetchEntityEndpointHeaderDescriptor.BODY_FETCH.name(), false) + .build()) + .executeAndThen() + .statusCode(400); + } + @Test @UseDataSet(TestDataGenerator.REST_THOUSAND_PRODUCTS) @DisplayName("Should return rich unknown entity list by multiple localized globally unique attribute") diff --git a/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/testSuite/TestDataGenerator.java b/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/testSuite/TestDataGenerator.java index 6bb5a05c8..c3cdb3b50 100644 --- a/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/testSuite/TestDataGenerator.java +++ b/evita_functional_tests/src/test/java/io/evitadb/externalApi/rest/api/testSuite/TestDataGenerator.java @@ -70,6 +70,7 @@ public class TestDataGenerator { public static final String ENTITY_EMPTY_WITHOUT_PK = "emptyWithoutPk"; public static final String ENTITY_BRAND_GROUP = "BrandGroup"; public static final String ENTITY_STORE_GROUP = "BrandGroup"; + public static final String ATTRIBUTE_RELATIVE_URL = "relativeUrl"; public static final String ATTRIBUTE_SIZE = "size"; public static final String ATTRIBUTE_CREATED = "created"; public static final String ATTRIBUTE_MANUFACTURED = "manufactured"; @@ -101,6 +102,7 @@ public static DataCarrier generateMainCatalogEntities(@Nonnull Evita evita, int .openForWrite() .withAttribute(ATTRIBUTE_CODE, String.class, whichIs -> whichIs.sortable().uniqueGlobally()) .withAttribute(ATTRIBUTE_URL, String.class, whichIs -> whichIs.localized().uniqueGlobally()) + .withAttribute(ATTRIBUTE_RELATIVE_URL, String.class, whichIs -> whichIs.localized().uniqueGloballyWithinLocale().nullable()) .updateVia(session); final DataGenerator dataGenerator = new DataGenerator(); @@ -180,6 +182,7 @@ public static DataCarrier generateMainCatalogEntities(@Nonnull Evita evita, int schemaBuilder -> { schemaBuilder .withDescription("This is a description") + .withGlobalAttribute(ATTRIBUTE_RELATIVE_URL) .withAttribute(ATTRIBUTE_QUANTITY, BigDecimal.class, whichIs -> whichIs .withDescription("This is a description") .filterable()