Skip to content

Commit

Permalink
feat(#316): GQL and REST unknown entity endpoints support for globall…
Browse files Browse the repository at this point in the history
…y-within-locale attributes
  • Loading branch information
lukashornych committed Dec 5, 2023
1 parent aecc5de commit b441431
Show file tree
Hide file tree
Showing 15 changed files with 413 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -145,6 +147,8 @@ private <A extends Serializable & Comparable<A>> 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) {
Expand All @@ -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<GlobalAttributeSchemaContract, Object> globallyUniqueAttributes) {

private static Arguments from(@Nonnull DataFetchingEnvironment environment, @Nonnull CatalogSchemaContract catalogSchema) {
final HashMap<String, Object> arguments = new HashMap<>(environment.getArguments());

// left over arguments are globally unique attribute filters as defined by schema
final Map<GlobalAttributeSchemaContract, Object> globallyUniqueAttributes = extractUniqueAttributesFromArguments(arguments, catalogSchema);
final Map<GlobalAttributeSchemaContract, Object> 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<GlobalAttributeSchemaContract, Object> extractUniqueAttributesFromArguments(
@Nonnull HashMap<String, Object> arguments,
@Nonnull Map<String, Object> arguments,
@Nonnull CatalogSchemaContract catalogSchema
) {
final Map<GlobalAttributeSchemaContract, Object> uniqueAttributes = createHashMap(arguments.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -151,6 +153,8 @@ private <A extends Serializable & Comparable<A>> 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) {
Expand Down Expand Up @@ -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<GlobalAttributeSchemaContract, List<Object>> globallyUniqueAttributes) {

private static Arguments from(@Nonnull DataFetchingEnvironment environment, @Nonnull CatalogSchemaContract catalogSchema) {
final HashMap<String, Object> 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<GlobalAttributeSchemaContract, List<Object>> globallyUniqueAttributes = extractUniqueAttributesFromArguments(arguments, catalogSchema);
final Map<GlobalAttributeSchemaContract, List<Object>> 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<GlobalAttributeSchemaContract, List<Object>> extractUniqueAttributesFromArguments(
@Nonnull HashMap<String, Object> arguments,
@Nonnull Map<String, Object> arguments,
@Nonnull CatalogSchemaContract catalogSchema
) {
final Map<GlobalAttributeSchemaContract, List<Object>> uniqueAttributes = createHashMap(arguments.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ public Optional<OpenApiCatalogEndpoint> 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())
Expand Down Expand Up @@ -234,7 +236,9 @@ public Optional<OpenApiCatalogEndpoint> 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())
Expand Down Expand Up @@ -302,11 +306,11 @@ public OpenApiCollectionEndpoint buildDeleteEntitiesByQueryEndpoint(@Nonnull Ent
}

@Nonnull
private List<OpenApiEndpointParameter> buildFetchQueryParametersForUnknownEntity(boolean localized) {
private List<OpenApiEndpointParameter> buildFetchQueryParametersForUnknownEntity(boolean needsLocale) {
final List<OpenApiEndpointParameter> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down Expand Up @@ -123,17 +126,18 @@ public static <A extends Serializable & Comparable<A>> FilterBy buildFilterByFor
@Nonnull CatalogSchemaContract catalogSchema) {
final List<FilterConstraint> filterConstraints = new LinkedList<>();

if (parameters.containsKey(FetchEntityEndpointHeaderDescriptor.LOCALE.name())) {
filterConstraints.add(QueryConstraints.entityLocaleEquals((Locale) parameters.get(FetchEntityEndpointHeaderDescriptor.LOCALE.name())));
}
final Map<GlobalAttributeSchemaContract, Object> 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(),
Expand Down Expand Up @@ -162,22 +166,22 @@ public static <A extends Serializable & Comparable<A>> FilterBy buildFilterByFor
@Nonnull CatalogSchemaContract catalogSchema) {
final List<FilterConstraint> filterConstraints = new LinkedList<>();

if (parameters.containsKey(FetchEntityEndpointHeaderDescriptor.LOCALE.name())) {
filterConstraints.add(QueryConstraints.entityLocaleEquals((Locale) parameters.get(FetchEntityEndpointHeaderDescriptor.LOCALE.name())));
}
final Map<GlobalAttributeSchemaContract, Object> 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(),
Expand All @@ -198,13 +202,36 @@ public static <A extends Serializable & Comparable<A>> FilterBy buildFilterByFor
}

@Nonnull
private static List<GlobalAttributeSchemaContract> getGloballyUniqueAttributes(CatalogSchemaContract catalogSchema) {
return catalogSchema
.getAttributes()
.values()
.stream()
.filter(GlobalAttributeSchemaContract::isUniqueGlobally)
.toList();
private static Map<GlobalAttributeSchemaContract, Object> getGloballyUniqueAttributesFromParameters(@Nonnull Map<String, Object> parameters,
@Nonnull CatalogSchemaContract catalogSchema) {
final Map<GlobalAttributeSchemaContract, Object> uniqueAttributes = createHashMap(parameters.size());

for (Entry<String, Object> 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")
Expand Down
Loading

0 comments on commit b441431

Please sign in to comment.