From 6ff52213e40b5bef9f8bf88bbc4baecc0dadaff5 Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Mon, 22 Aug 2022 18:08:03 +0200 Subject: [PATCH 01/20] Add range to topic directive with range data fetcher --- .../api/client/DefaultMirrorClient.java | 23 ++-- .../quick/common/api/client/MirrorClient.java | 3 + .../api/client/PartitionedMirrorClient.java | 8 +- .../common/api/model/mirror/MirrorHost.java | 8 +- .../directives/topic/TopicDirective.java | 30 ++++- .../topic/rule/fetcher/DataFetcherRules.java | 5 +- .../rule/fetcher/ListArgumentFetcherRule.java | 2 +- .../topic/rule/fetcher/QueryFetcherRule.java | 2 +- ...tchRule.java => QueryListFetcherRule.java} | 2 +- .../topic/rule/fetcher/RangeFetcherRule.java | 52 ++++++++ ...ipleArguments.java => RangeArguments.java} | 18 ++- .../rule/validation/ValidationRules.java | 9 +- .../gateway/fetcher/DataFetcherClient.java | 3 + .../quick/gateway/fetcher/FetcherFactory.java | 6 + .../fetcher/MirrorDataFetcherClient.java | 6 + .../gateway/fetcher/QueryListFetcher.java | 2 - .../gateway/fetcher/RangeQueryFetcher.java | 64 ++++++++++ .../gateway/ControllerReturnSchemaTest.java | 7 +- .../gateway/ControllerUpdateSchemaTest.java | 6 +- .../quick/gateway/GatewayConfigTest.java | 13 +- .../quick/gateway/GatewayInitializerTest.java | 2 - .../gateway/GraphQLSchemaGeneratorTest.java | 75 ++++++----- .../quick/gateway/GraphQLSecurityTest.java | 3 +- .../quick/gateway/GraphQLTestUtil.java | 16 ++- .../fetcher/QueryListArgumentFetcherTest.java | 18 +-- .../gateway/fetcher/QueryListFetcherTest.java | 1 - .../fetcher/RangeQueryFetcherTest.java | 116 ++++++++++++++++++ .../src/test/resources/application-test.yaml | 5 +- .../shouldConvertQueryWithRange.graphql | 19 +++ ...tConvertIfRangeToArgumentIsMissing.graphql | 18 +++ ...tCoverIfRangeFromArgumentIsMissing.graphql | 18 +++ 31 files changed, 464 insertions(+), 96 deletions(-) rename gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/{QueryListFetchRule.java => QueryListFetcherRule.java} (97%) create mode 100644 gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java rename gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/{MultipleArguments.java => RangeArguments.java} (65%) create mode 100644 gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java create mode 100644 gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java create mode 100644 gateway/src/test/resources/schema/conversion/shouldConvertQueryWithRange.graphql create mode 100644 gateway/src/test/resources/schema/conversion/shouldNotConvertIfRangeToArgumentIsMissing.graphql create mode 100644 gateway/src/test/resources/schema/conversion/shouldNotCoverIfRangeFromArgumentIsMissing.graphql diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java index 89860ccc..c6c9a079 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java @@ -40,27 +40,27 @@ public class DefaultMirrorClient implements MirrorClient { /** * Constructor for the client. * - * @param topicName name of the topic the mirror is deployed - * @param client http client - * @param mirrorConfig configuration of the mirror host + * @param topicName name of the topic the mirror is deployed + * @param client http client + * @param mirrorConfig configuration of the mirror host * @param valueResolver the value's {@link TypeResolver} * @param requestManager a manager for sending requests to the mirror and processing responses */ public DefaultMirrorClient(final String topicName, final HttpClient client, final MirrorConfig mirrorConfig, - final TypeResolver valueResolver, final MirrorRequestManager requestManager) { + final TypeResolver valueResolver, final MirrorRequestManager requestManager) { this(new MirrorHost(topicName, mirrorConfig), client, valueResolver, requestManager); } /** * Constructor that can be used when the mirror client is based on an IP or other non-standard host. * - * @param mirrorHost host to use - * @param client http client + * @param mirrorHost host to use + * @param client http client * @param typeResolver the value's {@link TypeResolver} * @param requestManager a manager for sending requests to the mirror and processing responses */ public DefaultMirrorClient(final MirrorHost mirrorHost, final HttpClient client, final TypeResolver typeResolver, - final MirrorRequestManager requestManager) { + final MirrorRequestManager requestManager) { this.host = mirrorHost; this.parser = new MirrorValueParser<>(typeResolver, client.objectMapper()); this.mirrorRequestManager = requestManager; @@ -86,6 +86,15 @@ public List fetchValues(final List keys) { return this.mirrorRequestManager.sendRequest(this.host.forKeys(collect), this.parser::deserializeList); } + @Override + @Nullable + public List fetchRange(final K key, final K rangeFrom, final K rangeTo) { + return this.mirrorRequestManager.sendRequest( + this.host.forRange(key.toString(), rangeFrom.toString(), rangeTo.toString()), + this.parser::deserializeList + ); + } + @Override public boolean exists(final K key) { return this.fetchValue(key) != null; diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java index ee76ece4..824a8561 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java @@ -53,6 +53,9 @@ public interface MirrorClient { @Nullable List fetchValues(final List keys); + @Nullable + List fetchRange(final K key, final K rangeFrom, final K rangeTo); + /** * checks if a key exists in mirror topic. * diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java index 94354974..96befa57 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java @@ -83,7 +83,7 @@ public PartitionedMirrorClient(final String topicName, final MirrorHost mirrorHo @Override @Nullable public V fetchValue(final K key) { - final MirrorHost currentKeyHost = router.findHost(key); + final MirrorHost currentKeyHost = this.router.findHost(key); return this.requestManager.sendRequest(Objects.requireNonNull(currentKeyHost).forKey(key.toString()), this.parser::deserialize); } @@ -106,6 +106,12 @@ public List fetchValues(final List keys) { return keys.stream().map(this::fetchValue).collect(Collectors.toList()); } + @Override + @Nullable + public List fetchRange(final K id, final K rangeFrom, final K rangeTo) { + throw new UnsupportedOperationException("Range queries are not supported in partitioned Mirrors."); + } + @Override public boolean exists(final K key) { return this.fetchValue(key) != null; diff --git a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java index 8b87f6ee..aac7839f 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java +++ b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java @@ -30,7 +30,7 @@ public class MirrorHost { /** * Default constructor. * - * @param host the host of the mirror. This can be a service name or an IP. + * @param host the host of the mirror. This can be a service name or an IP. * @param config mirror config to use. This can set the service prefix and REST path. */ public MirrorHost(final String host, final MirrorConfig config) { @@ -59,4 +59,10 @@ public String forKeys(final Iterable keys) { public String forAll() { return String.format("http://%s%s/%s", this.config.getPrefix(), this.host, this.config.getPath()); } + + public String forRange(final String key, final String rangeFrom, final String rangeTo) { + return String.format("http://%s%s/%s/%s?from=%s&to=%s", this.config.getPrefix(), this.host, + this.config.getPath(), key, rangeFrom, rangeTo); + } + } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java index 28e7897b..41bf901b 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java @@ -49,6 +49,8 @@ public final class TopicDirective implements QuickDirective { private static final String TOPIC_NAME_ARG_NAME = "name"; private static final String KEY_ARGUMENT_ARG_NAME = "keyArgument"; private static final String KEY_FIELD_ARG_NAME = "keyField"; + private static final String RANGE_FROM_ARG_NAME = "rangeFrom"; + private static final String RANGE_TO_ARG_NAME = "rangeTo"; static { DEFINITION = DirectiveDefinition.newDirectiveDefinition() @@ -68,6 +70,16 @@ public final class TopicDirective implements QuickDirective { .name(KEY_FIELD_ARG_NAME) .type(STRING) .build()) + .inputValueDefinition( + InputValueDefinition.newInputValueDefinition() + .name(RANGE_FROM_ARG_NAME) + .type(STRING) + .build()) + .inputValueDefinition( + InputValueDefinition.newInputValueDefinition() + .name(RANGE_TO_ARG_NAME) + .type(STRING) + .build()) .directiveLocation( DirectiveLocation.newDirectiveLocation() .name(FIELD_DEFINITION.name()) @@ -80,13 +92,19 @@ public final class TopicDirective implements QuickDirective { private final String keyArgument; @Nullable private final String keyField; + @Nullable + private final String rangeFrom; + @Nullable + private final String rangeTo; private TopicDirective(final String topicName, @Nullable final String keyArgument, - @Nullable final String keyField) { + @Nullable final String keyField, @Nullable final String rangeFrom, @Nullable final String rangeTo) { Objects.requireNonNull(topicName); this.topicName = topicName; this.keyArgument = keyArgument; this.keyField = keyField; + this.rangeFrom = rangeFrom; + this.rangeTo = rangeTo; } /** @@ -97,7 +115,9 @@ public static TopicDirective fromArguments(final Collection extractDataFetchers(final TopicDirectiveCo @Override public boolean isValid(final TopicDirectiveContext context) { - return context.getTopicDirective().getKeyArgument() != null + return context.getTopicDirective().hasKeyArgument() && context.isListType() && !context.getParentContainerName().equals(GraphQLUtils.SUBSCRIPTION_TYPE) && GraphQLTypeUtil.isList(context.getEnvironment().getElement().getType()); diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryFetcherRule.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryFetcherRule.java index eb74ddc5..ff82d2e7 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryFetcherRule.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryFetcherRule.java @@ -65,7 +65,7 @@ public List extractDataFetchers(final TopicDirectiveCo @Override public boolean isValid(final TopicDirectiveContext context) { - return context.getTopicDirective().getKeyArgument() != null + return context.getTopicDirective().hasKeyArgument() && !context.isListType() && !context.getParentContainerName().equals(GraphQLUtils.SUBSCRIPTION_TYPE); } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryListFetchRule.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryListFetcherRule.java similarity index 97% rename from gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryListFetchRule.java rename to gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryListFetcherRule.java index 7dce7d1c..65ba1aac 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryListFetchRule.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/QueryListFetcherRule.java @@ -42,7 +42,7 @@ * * @see com.bakdata.quick.gateway.fetcher.QueryListFetcher */ -public class QueryListFetchRule implements DataFetcherRule { +public class QueryListFetcherRule implements DataFetcherRule { @Override public List extractDataFetchers(final TopicDirectiveContext context) { diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java new file mode 100644 index 00000000..1c9289c0 --- /dev/null +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 bakdata GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bakdata.quick.gateway.directives.topic.rule.fetcher; + +import com.bakdata.quick.common.graphql.GraphQLUtils; +import com.bakdata.quick.gateway.DataFetcherSpecification; +import com.bakdata.quick.gateway.directives.topic.TopicDirectiveContext; +import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; +import java.util.List; +import java.util.Objects; + +public class RangeFetcherRule implements DataFetcherRule{ + @Override + public List extractDataFetchers(final TopicDirectiveContext context) { + Objects.requireNonNull(context.getTopicDirective().getKeyArgument()); + Objects.requireNonNull(context.getTopicDirective().getRangeFrom()); + Objects.requireNonNull(context.getTopicDirective().getRangeTo()); + final DataFetcher dataFetcher = context.getFetcherFactory().rangeFetcher( + context.getTopicDirective().getTopicName(), + context.getTopicDirective().getKeyArgument(), + context.getTopicDirective().getRangeFrom(), + context.getTopicDirective().getRangeTo(), + context.isNullable() + ); + final FieldCoordinates coordinates = this.currentCoordinates(context); + return List.of(DataFetcherSpecification.of(coordinates, dataFetcher)); + } + + @Override + public boolean isValid(final TopicDirectiveContext context) { + return context.getTopicDirective().hasKeyArgument() + && context.getTopicDirective().hasRangeFrom() + && context.getTopicDirective().hasRangeTo() + && context.isListType() + && !context.getParentContainerName().equals(GraphQLUtils.SUBSCRIPTION_TYPE); + } +} diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/MultipleArguments.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java similarity index 65% rename from gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/MultipleArguments.java rename to gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java index 516c9de3..c059e974 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/MultipleArguments.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java @@ -19,19 +19,15 @@ import com.bakdata.quick.gateway.directives.topic.TopicDirectiveContext; import java.util.Optional; -/** - * Validation for {@link com.bakdata.quick.gateway.directives.topic.TopicDirective} - * - *

- * The query type only supports single arguments. Therefore, if the size is bigger than one this should be considered as - * not supported. - */ -public class MultipleArguments implements ValidationRule { +public class RangeArguments implements ValidationRule{ @Override public Optional validate(final TopicDirectiveContext context) { - if (context.getEnvironment().getElement().getArguments().size() > 1) { - return Optional.of("Only single arguments are supported"); + if(context.getTopicDirective().hasRangeFrom() && context.getTopicDirective().hasRangeTo()) { + return Optional.empty(); + } + else if (!context.getTopicDirective().hasRangeFrom() && !context.getTopicDirective().hasRangeTo()) { + return Optional.empty(); } - return Optional.empty(); + return Optional.of("Both rangeFrom and rangeTo arguments should be set."); } } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationRules.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationRules.java index a3b3fed3..97a80e25 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationRules.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationRules.java @@ -30,10 +30,11 @@ public class ValidationRules implements TopicDirectiveRules { static { VALIDATION_RULES = List.of( - new SubscriptionList(), - new ExclusiveArguments(), - new KeyInformation(), - new MutationRequiresTwoArguments() + new SubscriptionList(), + new ExclusiveArguments(), + new KeyInformation(), + new MutationRequiresTwoArguments(), + new RangeArguments() ); } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DataFetcherClient.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DataFetcherClient.java index d189f0a0..7d082069 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DataFetcherClient.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DataFetcherClient.java @@ -67,4 +67,7 @@ public interface DataFetcherClient { */ @Nullable List fetchList(); + + @Nullable + List fetchRange(final String id, final String from, final String to); } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/FetcherFactory.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/FetcherFactory.java index 9dfb7b8f..7c8713d0 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/FetcherFactory.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/FetcherFactory.java @@ -98,6 +98,12 @@ public DataFetcher> listArgumentFetcher(final String topic, final St return new ListArgumentFetcher<>(argument, client, isNullable, hasNullableElements); } + public DataFetcher> rangeFetcher(final String topic, final String argument, final String rangeFrom, final + String rangeTo, final boolean isNullable) { + final DataFetcherClient client = this.clientSupplier.createClient(topic); + return new RangeQueryFetcher<>(argument, client, rangeFrom, rangeTo, isNullable); + } + /** * Creates a MutationFetcher object. * diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/MirrorDataFetcherClient.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/MirrorDataFetcherClient.java index 134713d7..968029d5 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/MirrorDataFetcherClient.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/MirrorDataFetcherClient.java @@ -70,6 +70,12 @@ public List fetchList() { return this.mirrorClient.get().fetchAll(); } + @Override + @Nullable + public List fetchRange(final String id, final String from, final String to) { + return this.mirrorClient.get().fetchRange(id, from, to); + } + private DefaultMirrorClient createMirrorClient(final String host, final MirrorConfig mirrorConfig, final HttpClient client, final TypeResolver valueResolver) { diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java index 813edd48..ee56ff56 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java @@ -77,6 +77,4 @@ public List get(final DataFetchingEnvironment environment) { return values; } - - } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java new file mode 100644 index 00000000..7c3dc474 --- /dev/null +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 bakdata GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bakdata.quick.gateway.fetcher; + +import edu.umd.cs.findbugs.annotations.Nullable; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.Collections; +import java.util.List; + +public class RangeQueryFetcher implements DataFetcher> { + private final String argument; + private final String rangeFrom; + private final String rangeTo; + private final DataFetcherClient dataFetcherClient; + private final boolean isNullable; + + public RangeQueryFetcher(final String argument, + final DataFetcherClient dataFetcherClient, + final String rangeFrom, final String rangeTo, final boolean isNullable) { + this.dataFetcherClient = dataFetcherClient; + this.argument = argument; + this.rangeFrom = rangeFrom; + this.rangeTo = rangeTo; + this.isNullable = isNullable; + } + + @Override + @Nullable + public List get(final DataFetchingEnvironment environment) { + final Object argumentValue = DeferFetcher.getArgument(this.argument, environment) + .orElseThrow(() -> new RuntimeException("Could not find argument " + this.argument)); + final Object rangeFromValue = DeferFetcher.getArgument(this.rangeFrom, environment) + .orElseThrow(() -> new RuntimeException("Could not find argument " + this.rangeFrom)); + final Object rangeToValue = DeferFetcher.getArgument(this.rangeTo, environment) + .orElseThrow(() -> new RuntimeException("Could not find argument " + this.rangeTo)); + + final List results = this.dataFetcherClient.fetchRange(argumentValue.toString(), rangeFromValue.toString(), + rangeToValue.toString()); + + // got null but schema doesn't allow null + // semantically, there is no difference between null and an empty list for us in this case + // we therefore continue gracefully by simply returning a list and not throwing an exception + if (results == null && !this.isNullable) { + return Collections.emptyList(); + } + + return results; + } +} diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/ControllerReturnSchemaTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/ControllerReturnSchemaTest.java index 8c21ff62..3a7b0e94 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/ControllerReturnSchemaTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/ControllerReturnSchemaTest.java @@ -26,14 +26,13 @@ import com.bakdata.quick.common.api.model.TopicWriteType; import com.bakdata.quick.common.api.model.gateway.SchemaData; import com.bakdata.quick.common.type.QuickTopicType; -import io.micronaut.context.annotation.Property; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.client.BlockingHttpClient; -import io.micronaut.rxjava2.http.client.RxHttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.rxjava2.http.client.RxHttpClient; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import java.io.IOException; @@ -43,8 +42,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -@Property(name = "quick.kafka.bootstrap-server", value = "dummy:1234") -@Property(name = "quick.kafka.schema-registry-url", value = "http://dummy") + @MicronautTest class ControllerReturnSchemaTest { @@ -118,5 +116,4 @@ void shouldThrowErrorIfTypeDoesNotExist() { .startsWith("Type nope does not exist") ); } - } diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/ControllerUpdateSchemaTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/ControllerUpdateSchemaTest.java index d097fd9f..6c91bbb3 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/ControllerUpdateSchemaTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/ControllerUpdateSchemaTest.java @@ -24,21 +24,19 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.micronaut.context.ApplicationContext; -import io.micronaut.context.annotation.Property; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; import io.micronaut.http.client.BlockingHttpClient; -import io.micronaut.rxjava2.http.client.RxHttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.rxjava2.http.client.RxHttpClient; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import java.util.Optional; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; -@Property(name = "quick.kafka.bootstrap-server", value = "dummy:1234") -@Property(name = "quick.kafka.schema-registry-url", value = "http://dummy") + @MicronautTest class ControllerUpdateSchemaTest { @Client("/") diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GatewayConfigTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GatewayConfigTest.java index 787ef192..d482d77e 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GatewayConfigTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GatewayConfigTest.java @@ -18,18 +18,15 @@ import static org.assertj.core.api.Assertions.assertThat; -import io.micronaut.context.annotation.Property; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import com.bakdata.quick.common.ConfigUtils; +import java.util.Map; import org.junit.jupiter.api.Test; -@MicronautTest -@Property(name = "quick.schema.path", value = "/test/path/schema.graphql") -@Property(name = "quick.kafka.bootstrap-server", value = "dummy:1234") -@Property(name = "quick.kafka.schema-registry-url", value = "http://dummy") class GatewayConfigTest { @Test - void createConfig(final GatewayConfig config) { + void createConfig() { + final Map properties = Map.of("quick.schema.path", "/test/path/schema.graphql"); + final GatewayConfig config = ConfigUtils.createWithProperties(properties, GatewayConfig.class); assertThat(config.getPath()).isEqualTo("/test/path/schema.graphql"); } - } diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GatewayInitializerTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GatewayInitializerTest.java index d483041d..95b8e96a 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GatewayInitializerTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GatewayInitializerTest.java @@ -39,8 +39,6 @@ * In contrast to files in the test resource directory, they cannot be accessed by the class loader. */ @MicronautTest -@Property(name = "quick.kafka.bootstrap-server", value = "dummy:1234") -@Property(name = "quick.kafka.schema-registry-url", value = "http://dummy") @Property(name = "quick.definition.path", value = "/definition/schema.graphql") class GatewayInitializerTest { diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java index 390bcd84..32743b2f 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java @@ -40,7 +40,6 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; -import io.micronaut.context.annotation.Property; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import java.io.IOException; @@ -52,8 +51,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; -@Property(name = "quick.kafka.schema-registry-url", value = "mock://dummy") -@Property(name = "quick.kafka.broker", value = "dummy") @MicronautTest(startApplication = false) class GraphQLSchemaGeneratorTest { @@ -63,7 +60,7 @@ class GraphQLSchemaGeneratorTest { @Inject GraphQLSchemaGeneratorTest(final GraphQLSchemaGenerator generator, - final TestTopicRegistryClient registryClient) { + final TestTopicRegistryClient registryClient) { this.generator = generator; this.registryClient = registryClient; } @@ -262,6 +259,25 @@ void shouldConvertQueryWithPrimitiveType(final TestInfo testInfo) throws IOExcep .isInstanceOf(QueryKeyArgumentFetcher.class); } + @Test + void shouldConvertQueryWithRange(final TestInfo testInfo) throws IOException { + final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql"); + final GraphQLSchema schema = this.generator.create(Files.readString(schemaPath)); + + final List topicDirectiveArguments = + GraphQLTestUtil.getTopicDirectiveArgumentsFromField("Query", "userRequests", schema); + + assertThat(topicDirectiveArguments) + .hasSize(4) + .extracting(GraphQLArgument::getName) + .containsExactly("name", "keyArgument", "rangeFrom", "rangeTo"); + + assertThat(topicDirectiveArguments) + .hasSize(4) + .extracting(GraphQLArgument::getValue) + .containsExactly("user-request-range", "userId", "timestampFrom", "timestampTo"); + } + @Test void shouldConvertQueryAllWithComplexType(final TestInfo testInfo) throws IOException { final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql"); @@ -471,53 +487,54 @@ void shouldConvertMutation(final TestInfo testInfo) throws IOException { @Test void shouldNotConvertIfMissingKeyInfoInQueryType(final TestInfo testInfo) throws IOException { - final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql"); - String schema = Files.readString(schemaPath); - assertThatExceptionOfType(QuickDirectiveException.class) - .isThrownBy(() -> this.generator.create(schema)) - .withMessage("When the return type is not a list for a non-mutation and non-subscription type," - + " key information (keyArgument or keyField) is needed."); + this.assertQuickDirectiveExceptionMessage(testInfo, + "When the return type is not a list for a non-mutation and non-subscription type," + + " key information (keyArgument or keyField) is needed."); } @Test void shouldNotConvertIfMissingKeyInfoInBasicType(final TestInfo testInfo) throws IOException { - final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql"); - final String schema = Files.readString(schemaPath); - assertThatExceptionOfType(QuickDirectiveException.class) - .isThrownBy(() -> this.generator.create(schema)) - .withMessage("When the return type is not a list for a non-mutation and non-subscription type," - + " key information (keyArgument or keyField) is needed."); + this.assertQuickDirectiveExceptionMessage(testInfo, + "When the return type is not a list for a non-mutation and non-subscription type," + + " key information (keyArgument or keyField) is needed."); } @Test void shouldNotConvertIfMutationDoesNotHaveTwoArgs(final TestInfo testInfo) throws IOException { - final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql"); - final String schema = Files.readString(schemaPath); - assertThatExceptionOfType(QuickDirectiveException.class) - .isThrownBy(() -> this.generator.create(schema)) - .withMessage("Mutation requires two input arguments"); + this.assertQuickDirectiveExceptionMessage(testInfo, "Mutation requires two input arguments"); } @Test void shouldNotConvertIfKeyArgAndInputNameDifferentInQueryType(final TestInfo testInfo) throws IOException { - final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql"); - final String schema = Files.readString(schemaPath); - assertThatExceptionOfType(QuickDirectiveException.class) - .isThrownBy(() -> this.generator.create(schema)) - .withMessage("Key argument has to be identical to the input name."); + this.assertQuickDirectiveExceptionMessage(testInfo, "Key argument has to be identical to the input name."); } @Test void shouldNotConvertIfKeyArgAndInputNameDifferentInNonQueryType(final TestInfo testInfo) throws IOException { + this.assertQuickDirectiveExceptionMessage(testInfo, "Key argument has to be identical to the input name."); + } + + @Test + void shouldNotConvertIfRangeToArgumentIsMissing(final TestInfo testInfo) throws IOException { + this.assertQuickDirectiveExceptionMessage(testInfo, "Both rangeFrom and rangeTo arguments should be set."); + } + + @Test + void shouldNotCoverIfRangeFromArgumentIsMissing(final TestInfo testInfo) throws IOException { + this.assertQuickDirectiveExceptionMessage(testInfo, "Both rangeFrom and rangeTo arguments should be set."); + } + + private void assertQuickDirectiveExceptionMessage(final TestInfo testInfo, final String message) + throws IOException { final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql"); final String schema = Files.readString(schemaPath); assertThatExceptionOfType(QuickDirectiveException.class) - .isThrownBy(() -> this.generator.create(schema)) - .withMessage("Key argument has to be identical to the input name."); + .isThrownBy(() -> this.generator.create(schema)) + .withMessage(message); } private static void hasFieldWithListType(final GraphQLObjectType objectType, final String insuredPersonId, - final String fieldTypeName) { + final String fieldTypeName) { assertThat(objectType.getFieldDefinition(insuredPersonId).getType()) .isNotNull() .isInstanceOfSatisfying(GraphQLList.class, list -> diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSecurityTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSecurityTest.java index 68fa4f55..85fc7489 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSecurityTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSecurityTest.java @@ -45,8 +45,7 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; -@Property(name = "quick.kafka.bootstrap-server", value = "dummy:1234") -@Property(name = "quick.kafka.schema-registry-url", value = "http://dummy") + @Property(name = "micronaut.security.enabled", value = StringUtils.TRUE) @MicronautTest class GraphQLSecurityTest { diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java index 0dee0748..fa04651c 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java @@ -18,18 +18,23 @@ import static org.mockito.Mockito.mock; +import com.bakdata.quick.gateway.directives.topic.TopicDirective; import com.bakdata.quick.gateway.fetcher.DataFetcherClient; import com.bakdata.quick.gateway.fetcher.ClientSupplier; import graphql.schema.DataFetcher; import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLSchema; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import lombok.Getter; public final class GraphQLTestUtil { - private GraphQLTestUtil() {} + private GraphQLTestUtil() { + } public static DataFetcher getFieldDataFetcher(final String objectName, final String fieldName, @@ -50,6 +55,15 @@ public static GraphQLFieldDefinition getFieldDefinition(final String objectName, .orElseThrow(); } + public static List getTopicDirectiveArgumentsFromField(final String objectName, + final String fieldName, + final GraphQLSchema schema) { + return getFieldDefinition(objectName, fieldName, schema) + .getDirective(TopicDirective.DIRECTIVE_NAME) + .getArguments().stream().filter(graphQLArgument -> graphQLArgument.getValue() != null) + .collect(Collectors.toList()); + } + static final class TestClientSupplier implements ClientSupplier { @Getter private final Map> clients; diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/QueryListArgumentFetcherTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/QueryListArgumentFetcherTest.java index b5213a6d..846bba43 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/QueryListArgumentFetcherTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/QueryListArgumentFetcherTest.java @@ -33,7 +33,7 @@ import java.util.List; import java.util.Map; import lombok.Builder; -import lombok.Data; +import lombok.Value; import okhttp3.OkHttpClient; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -136,7 +136,7 @@ void shouldFetchEmptyListWhenResultIsNullAndReturnTypeIsNotNullable() { @Test @SuppressWarnings("unchecked") - void shouldFetchEmptyListWhenResultNotIsNullAndDoesNotHaveNullableElements() { + void shouldFetchEmptyListWhenResultIsNotNullAndDoesNotHaveNullableElements() { final Purchase purchase1 = Purchase.builder() .purchaseId("testId1") .productId(1) @@ -171,18 +171,18 @@ private MirrorDataFetcherClient createClient(final Class clazz) { return new MirrorDataFetcherClient<>(this.host, this.client, this.mirrorConfig, resolver); } - @Data + @Value @Builder private static class Purchase { - private String purchaseId; - private int productId; - private int amount; + String purchaseId; + int productId; + int amount; } - @Data + @Value @Builder private static class Product { - private int productId; - private String name; + int productId; + String name; } } diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/QueryListFetcherTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/QueryListFetcherTest.java index b26d3f69..ef1e02e4 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/QueryListFetcherTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/QueryListFetcherTest.java @@ -83,7 +83,6 @@ void shouldFetchListOfObjects() throws Exception { @Test void shouldFetchListOfStrings() throws Exception { - final List list = List.of("abc", "def"); final String listJson = this.mapper.writeValueAsString(new MirrorValue<>(list)); this.server.enqueue(new MockResponse().setBody(listJson)); diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java new file mode 100644 index 00000000..417fd2aa --- /dev/null +++ b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2022 bakdata GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bakdata.quick.gateway.fetcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; + +import com.bakdata.quick.common.api.client.HttpClient; +import com.bakdata.quick.common.api.model.mirror.MirrorValue; +import com.bakdata.quick.common.config.MirrorConfig; +import com.bakdata.quick.common.resolver.KnownTypeResolver; +import com.bakdata.quick.common.resolver.TypeResolver; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.DataFetchingEnvironmentImpl; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import lombok.Builder; +import lombok.Value; +import okhttp3.OkHttpClient; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class RangeQueryFetcherTest { + public static final boolean isNullable = true; + private final ObjectMapper mapper = new ObjectMapper(); + private final MockWebServer server = new MockWebServer(); + private final HttpClient client = new HttpClient(this.mapper, new OkHttpClient()); + private final MirrorConfig mirrorConfig = MirrorConfig.directAccess(); + private final String host = String.format("localhost:%s", this.server.getPort()); + + @Test + void shouldFetchListWhenListArgumentOfTypeString() throws JsonProcessingException { + final UserRequest userRequest = UserRequest.builder() + .userId(1) + .timestamp(1) + .requests(3) + .build(); + + final UserRequest userRequest2 = UserRequest.builder() + .userId(1) + .timestamp(2) + .requests(5) + .build(); + + this.server.enqueue( + new MockResponse().setBody( + this.mapper.writeValueAsString(new MirrorValue<>(List.of(userRequest, userRequest2))))); + + final DataFetcherClient fetcherClient = this.createClient(UserRequest.class); + + final RangeQueryFetcher rangeQueryFetcher = + new RangeQueryFetcher<>("userId", fetcherClient, "timestampFrom","timestampTo", isNullable); + + final Map arguments = Map.of("userId", "1", "timestampFrom", "1", "timestampTo", "2"); + + final DataFetchingEnvironment env = DataFetchingEnvironmentImpl.newDataFetchingEnvironment() + .localContext(arguments).build(); + + final List actual = rangeQueryFetcher.get(env); + assertThat(actual).isEqualTo(List.of(userRequest, userRequest2)); + } + + @Test + @SuppressWarnings("unchecked") + void shouldFetchEmptyListWhenResultIsNullAndReturnTypeIsNotNullable() { + final MirrorDataFetcherClient fetcherClient = Mockito.mock(MirrorDataFetcherClient.class); + + Mockito.when(fetcherClient.fetchRange(anyString(), anyString(), anyString())).thenReturn(null); + + final RangeQueryFetcher rangeQueryFetcher = + new RangeQueryFetcher<>("userId", fetcherClient, "timestampFrom","timestampTo", false); + + final Map arguments = Map.of("userId", "1", "timestampFrom", "1", "timestampTo", "2"); + + final DataFetchingEnvironment env = DataFetchingEnvironmentImpl.newDataFetchingEnvironment() + .localContext(arguments).build(); + + final List actual = rangeQueryFetcher.get(env); + + assertThat(actual).isEqualTo(Collections.emptyList()); + } + + @NotNull + private MirrorDataFetcherClient createClient(final Class clazz) { + final TypeResolver resolver = new KnownTypeResolver<>(clazz, this.mapper); + return new MirrorDataFetcherClient<>(this.host, this.client, this.mirrorConfig, resolver); + } + + @Value + @Builder + private static class UserRequest { + int userId; + int timestamp; + int requests; + } +} \ No newline at end of file diff --git a/gateway/src/test/resources/application-test.yaml b/gateway/src/test/resources/application-test.yaml index b3e78e51..74ccf09f 100644 --- a/gateway/src/test/resources/application-test.yaml +++ b/gateway/src/test/resources/application-test.yaml @@ -11,11 +11,12 @@ endpoints: enabled: false quick: + kafka: + bootstrap-server: dummy:9092 + schema-registry-url: http://test:8081 definition: path: "definition/definition.yaml" apikey: test_key - kafka: - bootstrap-server: dummy:123 mirror: prefix: "" # prefix must be empty, as the host is simply 'localhost' and not for example 'quick-mirror-localhost' diff --git a/gateway/src/test/resources/schema/conversion/shouldConvertQueryWithRange.graphql b/gateway/src/test/resources/schema/conversion/shouldConvertQueryWithRange.graphql new file mode 100644 index 00000000..cafb9bc1 --- /dev/null +++ b/gateway/src/test/resources/schema/conversion/shouldConvertQueryWithRange.graphql @@ -0,0 +1,19 @@ +type Query { + userRequests( + userId: Int + timestampFrom: Int + timestampTo: Int + ): [UserRequests] + @topic(name: "user-request-range", + keyArgument: "userId", + rangeFrom: "timestampFrom", + rangeTo: "timestampTo") +} + +type UserRequests { + userId: Int + serviceId: Int + timestamp: Int + requests: Int + success: Int +} diff --git a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfRangeToArgumentIsMissing.graphql b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfRangeToArgumentIsMissing.graphql new file mode 100644 index 00000000..644501b7 --- /dev/null +++ b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfRangeToArgumentIsMissing.graphql @@ -0,0 +1,18 @@ +type Query { + userRequests( + userId: Int + timestampFrom: Int + timestampTo: Int + ): [UserRequests] + @topic(name: "user-request-range", + keyArgument: "userId", + rangeFrom: "timestampFrom") +} + +type UserRequests { + userId: Int + serviceId: Int + timestamp: Int + requests: Int + success: Int +} diff --git a/gateway/src/test/resources/schema/conversion/shouldNotCoverIfRangeFromArgumentIsMissing.graphql b/gateway/src/test/resources/schema/conversion/shouldNotCoverIfRangeFromArgumentIsMissing.graphql new file mode 100644 index 00000000..fb7012ce --- /dev/null +++ b/gateway/src/test/resources/schema/conversion/shouldNotCoverIfRangeFromArgumentIsMissing.graphql @@ -0,0 +1,18 @@ +type Query { + userRequests( + userId: Int + timestampFrom: Int + timestampTo: Int + ): [UserRequests] + @topic(name: "user-request-range", + keyArgument: "userId", + rangeTo: "timestampTo") +} + +type UserRequests { + userId: Int + serviceId: Int + timestamp: Int + requests: Int + success: Int +} From be5247d9b5710b314331b8d4e025d4e538b819a9 Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Tue, 23 Aug 2022 11:39:26 +0200 Subject: [PATCH 02/20] small changes --- .../bakdata/quick/common/api/model/mirror/MirrorHost.java | 7 +++++-- .../quick/gateway/directives/topic/TopicDirective.java | 4 +++- .../directives/topic/rule/fetcher/RangeFetcherRule.java | 1 + .../java/com/bakdata/quick/gateway/GraphQLTestUtil.java | 4 +--- .../quick/gateway/fetcher/RangeQueryFetcherTest.java | 2 +- .../com/bakdata/quick/gateway/security/ApiKeyTest.java | 2 -- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java index aac7839f..0bcdb0ad 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java +++ b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java @@ -60,9 +60,12 @@ public String forAll() { return String.format("http://%s%s/%s", this.config.getPrefix(), this.host, this.config.getPath()); } - public String forRange(final String key, final String rangeFrom, final String rangeTo) { + /** + * Generates a URL for fetching a range of keys + */ + public String forRange(final String key, final String from, final String to) { return String.format("http://%s%s/%s/%s?from=%s&to=%s", this.config.getPrefix(), this.host, - this.config.getPath(), key, rangeFrom, rangeTo); + this.config.getPath(), key, from, to); } } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java index 41bf901b..97ff0d45 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java @@ -37,7 +37,9 @@ * directive @topic( * name: String!, * keyArgument: String, - * keyField: String + * keyField: String, + * rangeFrom: String, + * rangeTo: String * ) on FIELD_DEFINITION * } */ diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java index 1c9289c0..2415d066 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java @@ -47,6 +47,7 @@ public boolean isValid(final TopicDirectiveContext context) { && context.getTopicDirective().hasRangeFrom() && context.getTopicDirective().hasRangeTo() && context.isListType() + && context.getEnvironment().getElement().getArguments().size() == 3 && !context.getParentContainerName().equals(GraphQLUtils.SUBSCRIPTION_TYPE); } } diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java index fa04651c..ee1d9b42 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java @@ -33,9 +33,7 @@ import lombok.Getter; public final class GraphQLTestUtil { - private GraphQLTestUtil() { - } - + private GraphQLTestUtil() {} public static DataFetcher getFieldDataFetcher(final String objectName, final String fieldName, final GraphQLSchema schema) { diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java index 417fd2aa..efa31de7 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java @@ -113,4 +113,4 @@ private static class UserRequest { int timestamp; int requests; } -} \ No newline at end of file +} diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/security/ApiKeyTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/security/ApiKeyTest.java index e946150a..03c998b1 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/security/ApiKeyTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/security/ApiKeyTest.java @@ -40,8 +40,6 @@ import org.junit.jupiter.api.Test; @MicronautTest -@Property(name = "quick.kafka.bootstrap-server", value = "dummy:1234") -@Property(name = "quick.kafka.schema-registry-url", value = "http://dummy") @Property(name = "micronaut.security.enabled", value = "true") class ApiKeyTest { private static final String SECURE_PATH = "control/definition"; From d5de0eca73c510022c8dc62c28b112c706f83c7d Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Tue, 23 Aug 2022 13:27:11 +0200 Subject: [PATCH 03/20] Add javadocs --- .../api/client/DefaultMirrorClient.java | 4 +-- .../quick/common/api/client/MirrorClient.java | 10 ++++++- .../api/client/PartitionedMirrorClient.java | 27 +++++++++--------- .../topic/rule/fetcher/RangeFetcherRule.java | 28 +++++++++++++++++++ .../topic/rule/validation/RangeArguments.java | 5 ++++ .../quick/gateway/fetcher/DeferFetcher.java | 2 +- .../gateway/fetcher/KeyFieldFetcher.java | 8 +++--- .../gateway/fetcher/MutationFetcher.java | 3 +- .../gateway/fetcher/QueryListFetcher.java | 8 +++--- .../gateway/fetcher/RangeQueryFetcher.java | 4 +++ .../gateway/ControllerUpdateSchemaTest.java | 1 - .../quick/gateway/GraphQLSecurityTest.java | 5 ++-- .../quick/gateway/GraphQLTestUtil.java | 2 +- 13 files changed, 76 insertions(+), 31 deletions(-) diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java index c6c9a079..37862cd0 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java @@ -88,9 +88,9 @@ public List fetchValues(final List keys) { @Override @Nullable - public List fetchRange(final K key, final K rangeFrom, final K rangeTo) { + public List fetchRange(final K key, final String from, final String rangeTo) { return this.mirrorRequestManager.sendRequest( - this.host.forRange(key.toString(), rangeFrom.toString(), rangeTo.toString()), + this.host.forRange(key.toString(), from, rangeTo), this.parser::deserializeList ); } diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java index 824a8561..7040a579 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java @@ -53,8 +53,16 @@ public interface MirrorClient { @Nullable List fetchValues(final List keys); + /** + * fetches a range of a given key from the mirror topic. + * + * @param key a key to be fetched + * @param from lower bound of the range field + * @param to higher bound of the range field + * @return a list of values. + */ @Nullable - List fetchRange(final K key, final K rangeFrom, final K rangeTo); + List fetchRange(final K key, final String from, final String to); /** * checks if a key exists in mirror topic. diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java index 96befa57..40d7e86d 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java @@ -37,8 +37,8 @@ import org.apache.kafka.common.serialization.Serde; /** - * MirrorClient that has access to information about partition-host mapping. This enables it to efficiently - * route requests in case when there is more than one mirror replica. + * MirrorClient that has access to information about partition-host mapping. This enables it to efficiently route + * requests in case when there is more than one mirror replica. * * @param key type * @param value type @@ -59,16 +59,16 @@ public class PartitionedMirrorClient implements MirrorClient { /** * Constructor that can be used when the mirror client is based on an IP or other non-standard host. * - * @param topicName the name of the topic - * @param mirrorHost host to use - * @param client http client - * @param keySerde the serde for the key - * @param valueResolver the value's {@link TypeResolver} + * @param topicName the name of the topic + * @param mirrorHost host to use + * @param client http client + * @param keySerde the serde for the key + * @param valueResolver the value's {@link TypeResolver} * @param partitionFinder strategy for finding partitions */ public PartitionedMirrorClient(final String topicName, final MirrorHost mirrorHost, final HttpClient client, - final Serde keySerde, final TypeResolver valueResolver, - final PartitionFinder partitionFinder) { + final Serde keySerde, final TypeResolver valueResolver, + final PartitionFinder partitionFinder) { this.streamsStateHost = StreamsStateHost.fromMirrorHost(mirrorHost); this.client = client; this.parser = new MirrorValueParser<>(valueResolver, client.objectMapper()); @@ -108,8 +108,10 @@ public List fetchValues(final List keys) { @Override @Nullable - public List fetchRange(final K id, final K rangeFrom, final K rangeTo) { - throw new UnsupportedOperationException("Range queries are not supported in partitioned Mirrors."); + public List fetchRange(final K key, final String from, final String to) { + final MirrorHost currentKeyHost = this.router.findHost(key); + return this.requestManager.sendRequest( + Objects.requireNonNull(currentKeyHost).forRange(key.toString(), from, to), this.parser::deserializeList); } @Override @@ -118,8 +120,7 @@ public boolean exists(final K key) { } /** - * Responsible for fetching the information about the partition - host mapping from - * the mirror. + * Responsible for fetching the information about the partition - host mapping from the mirror. * * @return a mapping between a partition (a number) and a corresponding host */ diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java index 2415d066..e8b92874 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java @@ -24,6 +24,34 @@ import java.util.List; import java.util.Objects; +/** + * Rule for range query fetcher. + * + *

+ *

Example:

+ *
{@code
+ * type Query {
+ *     userRequests(
+ *         userId: Int
+ *         timestampFrom: Int
+ *         timestampTo: Int
+ *     ): [UserRequests] @topic(name: "user-request-range",
+ *                              keyArgument: "userId",
+ *                              rangeFrom: "timestampFrom",
+ *                              rangeTo: "timestampTo")
+ * }
+ *
+ * type UserRequests {
+ *     userId: Int
+ *     serviceId: Int
+ *     timestamp: Int
+ *     requests: Int
+ *     success: Int
+ * }
+ * }
+ * + * @see com.bakdata.quick.gateway.fetcher.RangeQueryFetcher + */ public class RangeFetcherRule implements DataFetcherRule{ @Override public List extractDataFetchers(final TopicDirectiveContext context) { diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java index c059e974..509a3a03 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java @@ -19,6 +19,11 @@ import com.bakdata.quick.gateway.directives.topic.TopicDirectiveContext; import java.util.Optional; +/** + * Validation for {@link com.bakdata.quick.gateway.directives.topic.TopicDirective} + * + * rangeFrom and rangeTo should be either both present or both absent in the topic directive. + */ public class RangeArguments implements ValidationRule{ @Override public Optional validate(final TopicDirectiveContext context) { diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DeferFetcher.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DeferFetcher.java index 0e8acb2a..b158d9dd 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DeferFetcher.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DeferFetcher.java @@ -57,7 +57,7 @@ public class DeferFetcher implements DataFetcher> { private static final Object PLACEHOLDER = new Object(); /** - * Retrieves an argument from the user query. + * Retrieves the value of an argument from the user query. * *

* With this, arguments passed through with this class are handled correctly. diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/KeyFieldFetcher.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/KeyFieldFetcher.java index e864dca3..5987d485 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/KeyFieldFetcher.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/KeyFieldFetcher.java @@ -42,7 +42,7 @@ * *

* Consider the following schema: - *

+ * 
{@code
  *   type Query {
  *       findPurchase(purchaseId: ID): Purchase @topic(name: "purchase", keyArgument: "purchaseId")
  *   }
@@ -56,7 +56,7 @@
  *   type Product {
  *       ...
  *   }
- * 
+ * }
* *

* First, the data fetcher connected with findPurchase is called and returns a Purchase object with a missing product @@ -73,8 +73,8 @@ public class KeyFieldFetcher implements DataFetcher { * Constructor. * * @param objectMapper json handler - * @param argument name of the argument to extract key from - * @param client underlying HTTP mirror client + * @param argument name of the argument to extract key from + * @param client underlying HTTP mirror client */ public KeyFieldFetcher(final ObjectMapper objectMapper, final String argument, final DataFetcherClient client) { this.objectMapper = objectMapper; diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/MutationFetcher.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/MutationFetcher.java index 54cba2fe..9a3218a3 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/MutationFetcher.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/MutationFetcher.java @@ -20,6 +20,7 @@ import com.bakdata.quick.common.type.QuickTopicData; import com.bakdata.quick.common.util.Lazy; import com.bakdata.quick.gateway.ingest.KafkaIngestService; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import edu.umd.cs.findbugs.annotations.Nullable; import graphql.GraphQLException; @@ -71,7 +72,7 @@ public MutationFetcher(final String topic, @Override @Nullable - public V get(final DataFetchingEnvironment environment) throws Exception { + public V get(final DataFetchingEnvironment environment) throws JsonProcessingException { log.debug("Incoming request: Ingest payload for topic {}", this.topic); final Optional keyInputArgument = DeferFetcher.getArgument(this.keyInputArgumentName, environment); diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java index ee56ff56..01676106 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java @@ -29,12 +29,12 @@ * *

* Consider the following schema: - *

+ * 
{@code
  *   type Query {
  *       allPurchases: [Purchase]
  *   }
+ * }
  * 
- * *

* There, the gateway must fetch all purchases from the corresponding mirror as there is no argument. This is done by * this data fetcher. @@ -47,8 +47,8 @@ public class QueryListFetcher implements DataFetcher> { /** * Standard constructor. * - * @param dataFetcherClient mirror http client - * @param isNullable true if list itself can be null + * @param dataFetcherClient mirror http client + * @param isNullable true if list itself can be null * @param hasNullableElements true if list elements can be null */ public QueryListFetcher(final DataFetcherClient dataFetcherClient, final boolean isNullable, diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java index 7c3dc474..e9b6dcd6 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java @@ -22,6 +22,10 @@ import java.util.Collections; import java.util.List; +/** + * Data Fetcher that takes the query's key, rangeFrom, and rangeTo arguments and fetches values from the mirror's range + * index. + */ public class RangeQueryFetcher implements DataFetcher> { private final String argument; private final String rangeFrom; diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/ControllerUpdateSchemaTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/ControllerUpdateSchemaTest.java index 6c91bbb3..ddf7d298 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/ControllerUpdateSchemaTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/ControllerUpdateSchemaTest.java @@ -36,7 +36,6 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; - @MicronautTest class ControllerUpdateSchemaTest { @Client("/") diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSecurityTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSecurityTest.java index 85fc7489..5e1bc1a0 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSecurityTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSecurityTest.java @@ -34,18 +34,17 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; import io.micronaut.http.client.BlockingHttpClient; -import io.micronaut.rxjava2.http.client.RxHttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.runtime.server.EmbeddedServer; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.rxjava2.http.client.RxHttpClient; import io.micronaut.rxjava2.http.client.websockets.RxWebSocketClient; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import java.util.Map; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; - @Property(name = "micronaut.security.enabled", value = StringUtils.TRUE) @MicronautTest class GraphQLSecurityTest { diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java index ee1d9b42..5882ad75 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java @@ -19,8 +19,8 @@ import static org.mockito.Mockito.mock; import com.bakdata.quick.gateway.directives.topic.TopicDirective; -import com.bakdata.quick.gateway.fetcher.DataFetcherClient; import com.bakdata.quick.gateway.fetcher.ClientSupplier; +import com.bakdata.quick.gateway.fetcher.DataFetcherClient; import graphql.schema.DataFetcher; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLArgument; From c27f78e96865c167f8239e81dd4487e30aaba1ff Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Tue, 23 Aug 2022 14:23:44 +0200 Subject: [PATCH 04/20] add execution test --- .../gateway/GraphQLQueryExecutionTest.java | 80 +++++++++++++++---- .../execution/shouldExecuteRange.graphql | 16 ++++ .../execution/shouldExecuteRangeQuery.graphql | 5 ++ 3 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 gateway/src/test/resources/schema/execution/shouldExecuteRange.graphql create mode 100644 gateway/src/test/resources/schema/execution/shouldExecuteRangeQuery.graphql diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java index f266399f..a29a6573 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java @@ -42,7 +42,7 @@ import java.util.List; import java.util.Map; import lombok.Builder; -import lombok.Data; +import lombok.Value; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -115,6 +115,43 @@ void shouldExecuteQueryWithSingleField(final TestInfo testInfo) throws IOExcepti .containsEntry("productId", "product"); } + @Test + void shouldExecuteRange(final TestInfo testInfo) throws IOException { + final String name = testInfo.getTestMethod().orElseThrow().getName(); + final Path schemaPath = workingDirectory.resolve(name + ".graphql"); + final Path queryPath = workingDirectory.resolve(name + "Query.graphql"); + + final GraphQLSchema schema = this.generator.create(Files.readString(schemaPath)); + final GraphQL graphQL = GraphQL.newGraphQL(schema).build(); + + final DataFetcherClient dataFetcherClient = this.supplier.getClients().get("user-request-range"); + + final List userRequests = List.of( + UserRequest.builder().userId(1).timestamp(1).requests(5).build(), + UserRequest.builder().userId(1).timestamp(2).requests(10).build(), + UserRequest.builder().userId(1).timestamp(3).requests(8).build() + ); + + when(dataFetcherClient.fetchRange("1", "1", "3")).thenAnswer(invocation -> userRequests); + + final ExecutionResult executionResult = graphQL.execute(Files.readString(queryPath)); + + assertThat(executionResult.getErrors()).isEmpty(); + + final Map>> data = executionResult.getData(); + assertThat(data.get("userRequests")) + .isNotNull() + .hasSize(3) + .satisfies(userRequest -> { + assertThat(userRequest.get(0).get("requests")).isEqualTo(5); + + }).satisfies(userRequest -> { + assertThat(userRequest.get(1).get("requests")).isEqualTo(10); + }).satisfies(userRequest -> { + assertThat(userRequest.get(2).get("requests")).isEqualTo(8); + }); + } + @Test void shouldExecuteListQueryWithSingleFieldAndModification(final TestInfo testInfo) throws IOException { final String name = testInfo.getTestMethod().orElseThrow().getName(); @@ -259,7 +296,8 @@ void shouldThrowErrorForNonNullableField() throws IOException { .hasSize(1) .first() .satisfies(error -> { - assertThat(error.getMessage()).startsWith("The field at path '/findPurchase/productId' was declared as a non null type"); + assertThat(error.getMessage()).startsWith( + "The field at path '/findPurchase/productId' was declared as a non null type"); assertThat(error.getPath()).containsExactly("findPurchase", "productId"); }); } @@ -290,29 +328,43 @@ private void registerTopics() { "url-topic", new TopicData("url-topic", TopicWriteType.MUTABLE, QuickTopicType.STRING, QuickTopicType.AVRO, "") ).blockingAwait(); + + this.registryClient.register( + "user-request-range", + new TopicData("user-request-range", TopicWriteType.MUTABLE, QuickTopicType.INTEGER, QuickTopicType.AVRO, + "") + ).blockingAwait(); } - @Data + @Value @Builder private static class Purchase { - private String purchaseId; - private String productId; - private int amount; + String purchaseId; + String productId; + int amount; } - @Data + @Value @Builder private static class Product { - private String productId; - private String name; - private String description; - private Price price; + String productId; + String name; + String description; + Price price; } - @Data + @Value @Builder private static class Price { - private double total; - private String currency; + double total; + String currency; + } + + @Value + @Builder + private static class UserRequest { + int userId; + int timestamp; + int requests; } } diff --git a/gateway/src/test/resources/schema/execution/shouldExecuteRange.graphql b/gateway/src/test/resources/schema/execution/shouldExecuteRange.graphql new file mode 100644 index 00000000..45b2377d --- /dev/null +++ b/gateway/src/test/resources/schema/execution/shouldExecuteRange.graphql @@ -0,0 +1,16 @@ +type Query { + userRequests( + userId: Int + timestampFrom: Int + timestampTo: Int + ): [UserRequests] @topic(name: "user-request-range", + keyArgument: "userId", + rangeFrom: "timestampFrom", + rangeTo: "timestampTo") +} + +type UserRequests { + userId: Int + timestamp: Int + requests: Int +} diff --git a/gateway/src/test/resources/schema/execution/shouldExecuteRangeQuery.graphql b/gateway/src/test/resources/schema/execution/shouldExecuteRangeQuery.graphql new file mode 100644 index 00000000..519adf6a --- /dev/null +++ b/gateway/src/test/resources/schema/execution/shouldExecuteRangeQuery.graphql @@ -0,0 +1,5 @@ +{ + userRequests(userId: 1, timestampFrom: 1, timestampTo: 3) { + requests + } +} From 6d4e23e23c65c6eb081feacf94921924ab3ebcbd Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Wed, 24 Aug 2022 10:27:08 +0200 Subject: [PATCH 05/20] add test for mirror host --- .../common/api/model/mirror/MirrorHost.java | 1 - .../api/model/mirror/MirrorHostTest.java | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java diff --git a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java index 0bcdb0ad..43f008e2 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java +++ b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java @@ -67,5 +67,4 @@ public String forRange(final String key, final String from, final String to) { return String.format("http://%s%s/%s/%s?from=%s&to=%s", this.config.getPrefix(), this.host, this.config.getPath(), key, from, to); } - } diff --git a/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java b/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java new file mode 100644 index 00000000..e11a999e --- /dev/null +++ b/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022 bakdata GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bakdata.quick.common.api.model.mirror; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.bakdata.quick.common.config.MirrorConfig; +import java.util.List; +import org.junit.jupiter.api.Test; + +class MirrorHostTest { + + private static final String MIRROR_HOST_PREFIX = "quick-mirror"; + private static final String MIRROR_HOST_PATH = "mirror"; + + @Test + void shouldConstructCorrectUrlForKeyRequest() { + final MirrorHost mirrorHost = new MirrorHost("test-for-key", new MirrorConfig()); + final String actual = mirrorHost.forKey("give-me-key"); + final String url = "http://%s-test-for-key/%s/give-me-key"; + assertThat(actual).isEqualTo(String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH)); + } + + @Test + void shouldConstructCorrectUrlForKeysRequest() { + final MirrorHost mirrorHost = new MirrorHost("test-for-keys", new MirrorConfig()); + final String actual = mirrorHost.forKeys(List.of("test-1", "test-2", "test-3")); + final String url = "http://%s-test-for-keys/%s?ids=test-1,test-2,test-3"; + assertThat(actual).isEqualTo(String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH)); + } + + @Test + void shouldConstructCorrectUrlForAllRequest() { + final MirrorHost mirrorHost = new MirrorHost("test-for-all", new MirrorConfig()); + final String actual = mirrorHost.forAll(); + final String url = "http://%s-test-for-all/%s"; + assertThat(actual).isEqualTo(String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH)); + } + + @Test + void shouldConstructCorrectUrlForRangeRequest() { + final MirrorHost mirrorHost = new MirrorHost("test-for-rage", new MirrorConfig()); + final String actual = mirrorHost.forRange("test-key", "range-field-from", "range-field-to"); + final String url = "http://%s-test-for-rage/%s/%s?from=%s&to=%s"; + assertThat(actual).isEqualTo( + String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH, "test-key", "range-field-from", "range-field-to")); + } +} From e5a46fdc7f26fc36d4c59a2222ca39a1d8c7ebaa Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Thu, 25 Aug 2022 16:11:56 +0200 Subject: [PATCH 06/20] Add validation rules --- .../topic/rule/fetcher/RangeFetcherRule.java | 2 +- .../topic/rule/validation/KeyInformation.java | 76 +++++-------------- .../topic/rule/validation/RangeArguments.java | 49 ++++++++++-- .../rule/validation/ValidationUtility.java | 65 ++++++++++++++++ .../gateway/GraphQLSchemaGeneratorTest.java | 12 ++- ...ndInputNameDifferentInNonQueryType.graphql | 1 + ...rgAndInputNameDifferentInQueryType.graphql | 1 + ...ConvertIfMissingKeyInfoInBasicType.graphql | 1 + ...ConvertIfMissingKeyInfoInQueryType.graphql | 1 + ...onvertIfMutationDoesNotHaveTwoArgs.graphql | 1 + ...tConvertIfRangeToArgumentIsMissing.graphql | 1 + ...IfKeyArgumentInRangeQueryIsMissing.graphql | 18 +++++ ...overtIfRangeFromArgumentIsMissing.graphql} | 1 + ...tIfReturnTypeOfRangeQueryIsNotList.graphql | 20 +++++ 14 files changed, 182 insertions(+), 67 deletions(-) create mode 100644 gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java create mode 100644 gateway/src/test/resources/schema/conversion/shouldNotCovertIfKeyArgumentInRangeQueryIsMissing.graphql rename gateway/src/test/resources/schema/conversion/{shouldNotCoverIfRangeFromArgumentIsMissing.graphql => shouldNotCovertIfRangeFromArgumentIsMissing.graphql} (95%) create mode 100644 gateway/src/test/resources/schema/conversion/shouldNotCovertIfReturnTypeOfRangeQueryIsNotList.graphql diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java index e8b92874..15c19066 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java @@ -76,6 +76,6 @@ public boolean isValid(final TopicDirectiveContext context) { && context.getTopicDirective().hasRangeTo() && context.isListType() && context.getEnvironment().getElement().getArguments().size() == 3 - && !context.getParentContainerName().equals(GraphQLUtils.SUBSCRIPTION_TYPE); + && context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE); } } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/KeyInformation.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/KeyInformation.java index 10086444..784da27c 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/KeyInformation.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/KeyInformation.java @@ -16,6 +16,8 @@ package com.bakdata.quick.gateway.directives.topic.rule.validation; +import static com.bakdata.quick.gateway.directives.topic.rule.validation.ValidationUtility.makeCheckForKeyArgument; + import com.bakdata.quick.common.graphql.GraphQLUtils; import com.bakdata.quick.gateway.directives.topic.TopicDirectiveContext; import graphql.language.InputValueDefinition; @@ -28,10 +30,10 @@ * Validation for {@link com.bakdata.quick.gateway.directives.topic.TopicDirective} * *

- * When a user declares a non-mutation or a non-subscription type with a return type that is not a list, - * they have to provide key information - either keyField or keyArgument. - * The following example presents the correct way of providing key information (key argument is present - * when the return type is not a list, and it is the same as the input name (id): + * When a user declares a non-mutation or a non-subscription type with a return type that is not a list, they have to + * provide key information - either keyField or keyArgument. The following example presents the correct way of providing + * key information (key argument is present when the return type is not a list, and it is the same as the input name + * (id): *

{@code
  * type Product {
  *     id: ID!
@@ -40,10 +42,10 @@
  * type Query {
  *     getProduct(id: ID): Product @topic(name: "product-topic", keyArgument: "id")
  * }
-}
- * On the other hand, the example below depicts a code example in which the rules for providing - * key information are violated (the directive in the product field does not have the key argument at all, - * and the one in the url field has an incorrect value of the key argument - it should be productId): + * }
+ * On the other hand, the example below depicts a code example in which the rules for providing key information are + * violated (the directive in the product field does not have the key argument at all, and the one in the url field has + * an incorrect value of the key argument - it should be productId): *
{@code
  * type Query {
  *     getProduct(productId: ID): ProductInfo
@@ -58,62 +60,20 @@ public class KeyInformation implements ValidationRule {
 
     @Override
     public Optional validate(final TopicDirectiveContext context) {
-        if (this.checkIfBasicContextPropertiesAreInvalid(context)) {
+        if (checkIfBasicContextPropertiesAreInvalid(context)) {
             return Optional.of("When the return type is not a list for a non-mutation and non-subscription type,"
-                    + " key information (keyArgument or keyField) is needed.");
+                + " key information (keyArgument or keyField) is needed.");
         }
         // additional check for key arguments
-        final Optional additionalKeyArgCheckResult = makeCheckForKeyArgument(context);
-        if (additionalKeyArgCheckResult.isPresent()) {
-            return additionalKeyArgCheckResult;
-        }
+        return makeCheckForKeyArgument(context);
         // TODO: additional check for key field
-        return Optional.empty();
-    }
-
-    private Optional makeCheckForKeyArgument(final TopicDirectiveContext context) {
-        if (context.getTopicDirective().getKeyArgument() != null) {
-            final boolean inputNameAndKeyArgsMatch;
-            if (context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE)) {
-                inputNameAndKeyArgsMatch = this.checkIfInputNameAndKeyArgMatchInQueryType(context);
-            } else {
-                inputNameAndKeyArgsMatch = this.checkIfInputNameAndKeyArgMatchInNonQueryType(context);
-            }
-            if (!inputNameAndKeyArgsMatch) {
-                return Optional.of("Key argument has to be identical to the input name.");
-            }
-        }
-        return Optional.empty();
     }
 
-    private boolean checkIfBasicContextPropertiesAreInvalid(final TopicDirectiveContext context) {
+    private static boolean checkIfBasicContextPropertiesAreInvalid(final TopicDirectiveContext context) {
         return !context.getParentContainerName().equals(GraphQLUtils.MUTATION_TYPE)
-                && !context.getParentContainerName().equals(GraphQLUtils.SUBSCRIPTION_TYPE)
-                && !context.isListType()
-                && !context.getTopicDirective().hasKeyArgument()
-                && !context.getTopicDirective().hasKeyField();
-    }
-
-    private boolean checkIfInputNameAndKeyArgMatchInQueryType(final TopicDirectiveContext context) {
-        final String keyArg = context.getTopicDirective().getKeyArgument();
-        return context.getEnvironment().getElement().getDefinition().getInputValueDefinitions()
-                .stream()
-                .map(InputValueDefinition::getName)
-                .anyMatch(name -> Objects.equals(name, keyArg));
-    }
-
-    private boolean checkIfInputNameAndKeyArgMatchInNonQueryType(final TopicDirectiveContext context) {
-        final Optional queryTypeDef = context.getEnvironment().getRegistry().getType("Query");
-        if (queryTypeDef.isEmpty()) {
-            throw new IllegalStateException("Something went wrong - The query type is mandatory.");
-        } else {
-            final String keyArg = context.getTopicDirective().getKeyArgument();
-            // We can cast here because we know that Query must be of the type ObjectTypeDefinition
-            return ((ObjectTypeDefinition) queryTypeDef.get()).getFieldDefinitions()
-                    .stream()
-                    .flatMap(fieldDef -> fieldDef.getInputValueDefinitions().stream())
-                    .map(InputValueDefinition::getName)
-                    .anyMatch(name -> Objects.equals(name, keyArg));
-        }
+            && !context.getParentContainerName().equals(GraphQLUtils.SUBSCRIPTION_TYPE)
+            && !context.isListType()
+            && !context.getTopicDirective().hasKeyArgument()
+            && !context.getTopicDirective().hasKeyField();
     }
 }
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
index 509a3a03..9d46954c 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
@@ -16,23 +16,58 @@
 
 package com.bakdata.quick.gateway.directives.topic.rule.validation;
 
+import com.bakdata.quick.common.graphql.GraphQLUtils;
 import com.bakdata.quick.gateway.directives.topic.TopicDirectiveContext;
 import java.util.Optional;
 
 /**
- * Validation for {@link com.bakdata.quick.gateway.directives.topic.TopicDirective}
+ * Validation for range queries. These rules should apply:
+ * 1. The Parent container should be a Query and not Mutation/Subscription
+ * 2. Both rangeFrom and rangeTo fields should exist in the topic directive
+ * 3. A valid keyArgument should exist in the topic directive
+ * 4. The return type of the query should be list
+ * 

+ *

Valid schema:

+ *
{@code
+ * type Query {
+ *     userRequests(
+ *         userId: Int
+ *         timestampFrom: Int
+ *         timestampTo: Int
+ *     ): [UserRequests] @topic(name: "user-request-range",
+ *                              keyArgument: "userId",
+ *                              rangeFrom: "timestampFrom",
+ *                              rangeTo: "timestampTo")
+ * }
  *
- * rangeFrom and rangeTo should be either both present or both absent in the topic directive.
+ * type UserRequests {
+ *     userId: Int
+ *     serviceId: Int
+ *     timestamp: Int
+ *     requests: Int
+ *     success: Int
+ * }
+ * }
*/ -public class RangeArguments implements ValidationRule{ +public class RangeArguments implements ValidationRule { @Override public Optional validate(final TopicDirectiveContext context) { - if(context.getTopicDirective().hasRangeFrom() && context.getTopicDirective().hasRangeTo()) { - return Optional.empty(); - } - else if (!context.getTopicDirective().hasRangeFrom() && !context.getTopicDirective().hasRangeTo()) { + if (checkIfItIsRange(context)) { + if (!context.getTopicDirective().hasKeyArgument()) { + return Optional.of("You must define a keyArgument."); + } else if (!context.isListType()) { + return Optional.of("The return type of range queries should be a list."); + } + return ValidationUtility.makeCheckForKeyArgument(context); + } else if (!context.getTopicDirective().hasRangeFrom() && !context.getTopicDirective().hasRangeTo()) { return Optional.empty(); } return Optional.of("Both rangeFrom and rangeTo arguments should be set."); } + + private static boolean checkIfItIsRange(final TopicDirectiveContext context) { + return context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE) + && context.getTopicDirective().hasRangeFrom() + && context.getTopicDirective().hasRangeTo(); + } } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java new file mode 100644 index 00000000..520b7df8 --- /dev/null +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 bakdata GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bakdata.quick.gateway.directives.topic.rule.validation; + +import com.bakdata.quick.common.graphql.GraphQLUtils; +import com.bakdata.quick.gateway.directives.topic.TopicDirectiveContext; +import graphql.language.InputValueDefinition; +import graphql.language.ObjectTypeDefinition; +import java.util.Objects; +import java.util.Optional; + +public class ValidationUtility { + + public static Optional makeCheckForKeyArgument(final TopicDirectiveContext context) { + if (context.getTopicDirective().getKeyArgument() != null) { + final boolean inputNameAndKeyArgsMatch; + if (context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE)) { + inputNameAndKeyArgsMatch = checkIfInputNameAndKeyArgMatchInQueryType(context); + } else { + inputNameAndKeyArgsMatch = checkIfInputNameAndKeyArgMatchInNonQueryType(context); + } + if (!inputNameAndKeyArgsMatch) { + return Optional.of("Key argument has to be identical to the input name."); + } + } + return Optional.empty(); + } + + private static boolean checkIfInputNameAndKeyArgMatchInQueryType(final TopicDirectiveContext context) { + final String keyArg = context.getTopicDirective().getKeyArgument(); + return context.getEnvironment().getElement().getDefinition().getInputValueDefinitions() + .stream() + .map(InputValueDefinition::getName) + .anyMatch(name -> Objects.equals(name, keyArg)); + } + + private static boolean checkIfInputNameAndKeyArgMatchInNonQueryType(final TopicDirectiveContext context) { + final Optional queryTypeDef = context.getEnvironment().getRegistry().getType("Query"); + if (queryTypeDef.isEmpty()) { + throw new IllegalStateException("Something went wrong - The query type is mandatory."); + } else { + final String keyArg = context.getTopicDirective().getKeyArgument(); + // We can cast here because we know that Query must be of the type ObjectTypeDefinition + return ((ObjectTypeDefinition) queryTypeDef.get()).getFieldDefinitions() + .stream() + .flatMap(fieldDef -> fieldDef.getInputValueDefinitions().stream()) + .map(InputValueDefinition::getName) + .anyMatch(name -> Objects.equals(name, keyArg)); + } + } +} diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java index 32743b2f..0dd140e5 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java @@ -520,10 +520,20 @@ void shouldNotConvertIfRangeToArgumentIsMissing(final TestInfo testInfo) throws } @Test - void shouldNotCoverIfRangeFromArgumentIsMissing(final TestInfo testInfo) throws IOException { + void shouldNotCovertIfRangeFromArgumentIsMissing(final TestInfo testInfo) throws IOException { this.assertQuickDirectiveExceptionMessage(testInfo, "Both rangeFrom and rangeTo arguments should be set."); } + @Test + void shouldNotCovertIfKeyArgumentInRangeQueryIsMissing(final TestInfo testInfo) throws IOException { + this.assertQuickDirectiveExceptionMessage(testInfo, "You must define a keyArgument."); + } + + @Test + void shouldNotCovertIfReturnTypeOfRangeQueryIsNotList(final TestInfo testInfo) throws IOException { + this.assertQuickDirectiveExceptionMessage(testInfo, "The return type of range queries should be a list."); + } + private void assertQuickDirectiveExceptionMessage(final TestInfo testInfo, final String message) throws IOException { final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql"); diff --git a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfKeyArgAndInputNameDifferentInNonQueryType.graphql b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfKeyArgAndInputNameDifferentInNonQueryType.graphql index e40174e3..6bbd7986 100644 --- a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfKeyArgAndInputNameDifferentInNonQueryType.graphql +++ b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfKeyArgAndInputNameDifferentInNonQueryType.graphql @@ -1,3 +1,4 @@ +# Invalid Schema type Query { getProduct(productId: ID): ProductInfo } diff --git a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfKeyArgAndInputNameDifferentInQueryType.graphql b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfKeyArgAndInputNameDifferentInQueryType.graphql index 9c9f9952..cd804ee0 100644 --- a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfKeyArgAndInputNameDifferentInQueryType.graphql +++ b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfKeyArgAndInputNameDifferentInQueryType.graphql @@ -1,3 +1,4 @@ +# Invalid Schema type Query { getClick(id: ID): Long @topic(name: "click-topic", keyArgument: "aDifferentKeyArgument") } diff --git a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMissingKeyInfoInBasicType.graphql b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMissingKeyInfoInBasicType.graphql index 1b5890aa..08262c8f 100644 --- a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMissingKeyInfoInBasicType.graphql +++ b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMissingKeyInfoInBasicType.graphql @@ -1,3 +1,4 @@ +# Invalid Schema type Query { getProduct(productId: ID): ProductInfo } diff --git a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMissingKeyInfoInQueryType.graphql b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMissingKeyInfoInQueryType.graphql index 60c142ec..8750982f 100644 --- a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMissingKeyInfoInQueryType.graphql +++ b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMissingKeyInfoInQueryType.graphql @@ -1,3 +1,4 @@ +# Invalid Schema type Product { id: ID! name: String! diff --git a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMutationDoesNotHaveTwoArgs.graphql b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMutationDoesNotHaveTwoArgs.graphql index 33af2fe6..f20e7588 100644 --- a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMutationDoesNotHaveTwoArgs.graphql +++ b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfMutationDoesNotHaveTwoArgs.graphql @@ -1,3 +1,4 @@ +# Invalid Schema type Mutation { setClick(clickCount: Long): Long @topic(name: "click-topic") } diff --git a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfRangeToArgumentIsMissing.graphql b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfRangeToArgumentIsMissing.graphql index 644501b7..20ca62c1 100644 --- a/gateway/src/test/resources/schema/conversion/shouldNotConvertIfRangeToArgumentIsMissing.graphql +++ b/gateway/src/test/resources/schema/conversion/shouldNotConvertIfRangeToArgumentIsMissing.graphql @@ -1,3 +1,4 @@ +# Invalid Schema type Query { userRequests( userId: Int diff --git a/gateway/src/test/resources/schema/conversion/shouldNotCovertIfKeyArgumentInRangeQueryIsMissing.graphql b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfKeyArgumentInRangeQueryIsMissing.graphql new file mode 100644 index 00000000..353718b8 --- /dev/null +++ b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfKeyArgumentInRangeQueryIsMissing.graphql @@ -0,0 +1,18 @@ +type Query { + userRequests( + userId: Int + timestampFrom: Int + timestampTo: Int + ): [UserRequests] + @topic(name: "user-request-range", + rangeFrom: "timestampFrom", + rangeTo: "timestampTo") +} + +type UserRequests { + userId: Int + serviceId: Int + timestamp: Int + requests: Int + success: Int +} diff --git a/gateway/src/test/resources/schema/conversion/shouldNotCoverIfRangeFromArgumentIsMissing.graphql b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeFromArgumentIsMissing.graphql similarity index 95% rename from gateway/src/test/resources/schema/conversion/shouldNotCoverIfRangeFromArgumentIsMissing.graphql rename to gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeFromArgumentIsMissing.graphql index fb7012ce..0c057b68 100644 --- a/gateway/src/test/resources/schema/conversion/shouldNotCoverIfRangeFromArgumentIsMissing.graphql +++ b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeFromArgumentIsMissing.graphql @@ -1,3 +1,4 @@ +# Invalid Schema type Query { userRequests( userId: Int diff --git a/gateway/src/test/resources/schema/conversion/shouldNotCovertIfReturnTypeOfRangeQueryIsNotList.graphql b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfReturnTypeOfRangeQueryIsNotList.graphql new file mode 100644 index 00000000..0ce9c036 --- /dev/null +++ b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfReturnTypeOfRangeQueryIsNotList.graphql @@ -0,0 +1,20 @@ +# Invalid Schema +type Query { + userRequests( + userId: Int + timestampFrom: Int + timestampTo: Int + ): UserRequests + @topic(name: "user-request-range", + keyArgument: "userId", + rangeFrom: "timestampFrom", + rangeTo: "timestampTo") +} + +type UserRequests { + userId: Int + serviceId: Int + timestamp: Int + requests: Int + success: Int +} From 71e5723b063dff1717b72d2a57eb66d85565d552 Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Tue, 6 Sep 2022 10:43:25 +0200 Subject: [PATCH 07/20] fix mirror host test --- .../bakdata/quick/common/api/model/mirror/MirrorHostTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java b/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java index e11a999e..e06204ce 100644 --- a/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java +++ b/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java @@ -39,7 +39,7 @@ void shouldConstructCorrectUrlForKeyRequest() { void shouldConstructCorrectUrlForKeysRequest() { final MirrorHost mirrorHost = new MirrorHost("test-for-keys", new MirrorConfig()); final String actual = mirrorHost.forKeys(List.of("test-1", "test-2", "test-3")); - final String url = "http://%s-test-for-keys/%s?ids=test-1,test-2,test-3"; + final String url = "http://%s-test-for-keys/%s/keys?ids=test-1,test-2,test-3"; assertThat(actual).isEqualTo(String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH)); } From 9a6ba8a20e252a4f427f60d8ce9d2f87835c09fc Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Tue, 6 Sep 2022 11:14:43 +0200 Subject: [PATCH 08/20] Add E2E test for gateway --- e2e/functional/range/query-range.gql | 8 +++ e2e/functional/range/result-range.json | 20 ++++++++ e2e/functional/range/schema.gql | 19 +++++++ e2e/functional/range/tests.bats | 66 +++++++++++++++++++++++++ e2e/functional/range/user-requests.json | 62 +++++++++++++++++++++++ 5 files changed, 175 insertions(+) create mode 100644 e2e/functional/range/query-range.gql create mode 100644 e2e/functional/range/result-range.json create mode 100644 e2e/functional/range/schema.gql create mode 100644 e2e/functional/range/tests.bats create mode 100644 e2e/functional/range/user-requests.json diff --git a/e2e/functional/range/query-range.gql b/e2e/functional/range/query-range.gql new file mode 100644 index 00000000..a3fc8852 --- /dev/null +++ b/e2e/functional/range/query-range.gql @@ -0,0 +1,8 @@ +query { + userRequests(userId: 123, timestampFrom:1, timestampTo:3) { + userId + serviceId + requests + success + } +} diff --git a/e2e/functional/range/result-range.json b/e2e/functional/range/result-range.json new file mode 100644 index 00000000..9839c826 --- /dev/null +++ b/e2e/functional/range/result-range.json @@ -0,0 +1,20 @@ +[ + { + "userId": 123, + "serviceId": "abc", + "requests": 5, + "success": 2 + }, + { + "userId": 123, + "serviceId": "def", + "requests": 8, + "success": 4 + }, + { + "userId": 123, + "serviceId": "def", + "requests": 6, + "success": 5 + } +] diff --git a/e2e/functional/range/schema.gql b/e2e/functional/range/schema.gql new file mode 100644 index 00000000..cafb9bc1 --- /dev/null +++ b/e2e/functional/range/schema.gql @@ -0,0 +1,19 @@ +type Query { + userRequests( + userId: Int + timestampFrom: Int + timestampTo: Int + ): [UserRequests] + @topic(name: "user-request-range", + keyArgument: "userId", + rangeFrom: "timestampFrom", + rangeTo: "timestampTo") +} + +type UserRequests { + userId: Int + serviceId: Int + timestamp: Int + requests: Int + success: Int +} diff --git a/e2e/functional/range/tests.bats b/e2e/functional/range/tests.bats new file mode 100644 index 00000000..decc3bf1 --- /dev/null +++ b/e2e/functional/range/tests.bats @@ -0,0 +1,66 @@ +#!/usr/bin/env ./test/libs/bats/bin/bats +# shellcheck shell=bats + +CONTENT_TYPE="content-type:application/json" +API_KEY="X-API-Key:${X_API_KEY}" +TOPIC="user-request-range" +TYPE="UserRequests" +GATEWAY="range-gateway-test" +INGEST_URL="${HOST}/ingest/${TOPIC}" +GRAPHQL_URL="${HOST}/gateway/${GATEWAY}/graphql" +GRAPHQL_CLI="gql-cli ${GRAPHQL_URL} -H ${API_KEY}" + + +setup() { + if [ "$BATS_TEST_NUMBER" -eq 1 ]; then + printf "creating context for %s\n" "$HOST" + printf "with API_KEY: %s\n" "${X_API_KEY}" + quick context create --host "${HOST}" --key "${X_API_KEY}" + fi +} + +@test "should deploy product-gateway" { + run quick gateway create ${GATEWAY} + echo "$output" + sleep 30 + [ "$status" -eq 0 ] + [ "$output" = "Create gateway ${GATEWAY} (this may take a few seconds)" ] +} + +@test "should apply schema to range-gateway" { + run quick gateway apply ${GATEWAY} -f schema.gql + echo "$output" + [ "$status" -eq 0 ] + [ "$output" = "Applied schema to gateway ${GATEWAY}" ] +} + +#TODO: Remove skip after implementing the Mirror and query service +@test "should create user-request-range topic with key integer and value schema" { + skip + run quick topic create ${TOPIC} --key-type integer --value-type schema --schema "${GATEWAY}.${TYPE}" --range-field timestamp --point + echo "$output" + [ "$status" -eq 0 ] + [ "$output" = "Created new topic ${TOPIC}" ] +} + +@test "should ingest valid data in user-request-range" { + skip + curl --request POST --url "${INGEST_URL}" --header "${CONTENT_TYPE}" --header "${API_KEY}" --data "@./user-requests.json" +} + +@test "should retrieve range of inserted items" { + skip + sleep 30 + result="$(${GRAPHQL_CLI} < query-single.gql | jq -j .userRequests)" + expected="$(cat result-single.json)" + echo "$result" + [ "$result" = "$expected" ] +} + +teardown() { + if [[ "${#BATS_TEST_NAMES[@]}" -eq "$BATS_TEST_NUMBER" ]]; then + quick gateway delete ${GATEWAY} + #TODO: Uncomment the topic deletion + # quick topic delete ${TOPIC} + fi +} diff --git a/e2e/functional/range/user-requests.json b/e2e/functional/range/user-requests.json new file mode 100644 index 00000000..4e8ae1e2 --- /dev/null +++ b/e2e/functional/range/user-requests.json @@ -0,0 +1,62 @@ +[ + { + "key": 123, + "value": { + "userId": 123, + "serviceId": "abc", + "timestamp": 1, + "requests": 5, + "success": 2 + } + }, + { + "key": 123, + "value": { + "userId": 123, + "serviceId": "def", + "timestamp": 2, + "requests": 8, + "success": 4 + } + }, + { + "key": 123, + "value": { + "userId": 123, + "serviceId": "def", + "timestamp": 3, + "requests": 6, + "success": 5 + } + }, + { + "key": 123, + "value": { + "userId": 123, + "serviceId": "abc", + "timestamp": 4, + "requests": 6, + "success": 5 + } + }, + { + "key": 456, + "value": { + "userId": 456, + "serviceId": "hij", + "timestamp": 1, + "requests": 10, + "success": 8 + } + }, + { + "key": 456, + "value": { + "userId": 456, + "serviceId": "abc", + "timestamp": 2, + "requests": 3, + "success": 3 + } + } +] \ No newline at end of file From b18da6cef570ab41f440d60a4320481bd7d4ae42 Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Tue, 6 Sep 2022 16:29:00 +0200 Subject: [PATCH 09/20] Add range to field --- .../quick/common/api/client/MirrorClient.java | 10 +-- .../api/client/PartitionedMirrorClient.java | 13 ++-- .../topic/rule/fetcher/RangeFetcherRule.java | 5 +- .../topic/rule/validation/RangeArguments.java | 11 ++-- .../rule/validation/ValidationUtility.java | 5 +- .../gateway/GraphQLQueryExecutionTest.java | 62 ++++++++++++++++--- .../gateway/GraphQLSchemaGeneratorTest.java | 19 ++++++ ...shouldConvertQueryWithRangeOnField.graphql | 12 ++++ .../shouldExecuteRangeOnField.graphql | 12 ++++ .../shouldExecuteRangeOnFieldQuery.graphql | 7 +++ 10 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 gateway/src/test/resources/schema/conversion/shouldConvertQueryWithRangeOnField.graphql create mode 100644 gateway/src/test/resources/schema/execution/shouldExecuteRangeOnField.graphql create mode 100644 gateway/src/test/resources/schema/execution/shouldExecuteRangeOnFieldQuery.graphql diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java index 7040a579..6691f556 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/MirrorClient.java @@ -29,7 +29,7 @@ public interface MirrorClient { /** - * fetches the value of the given key from the mirror topic. + * Fetches the value of the given key from the mirror topic. * * @param key a key to be fetched * @return a list of values. If the requested mirror responds with a NOT_FOUND code the function returns null. @@ -38,14 +38,14 @@ public interface MirrorClient { V fetchValue(final K key); /** - * fetches all the values of a mirror topic. + * Fetches all the values of a mirror topic. * * @return returns a list of all values in a topic. null. */ List fetchAll(); /** - * fetches the values of a list of keys from the mirror topic. + * Fetches the values of a list of keys from the mirror topic. * * @param keys list of keys to be fetched * @return a list of values. If the requested mirror responds with a NOT_FOUND code the function returns null. @@ -54,7 +54,7 @@ public interface MirrorClient { List fetchValues(final List keys); /** - * fetches a range of a given key from the mirror topic. + * Fetches a range of a given key from the mirror topic. * * @param key a key to be fetched * @param from lower bound of the range field @@ -65,7 +65,7 @@ public interface MirrorClient { List fetchRange(final K key, final String from, final String to); /** - * checks if a key exists in mirror topic. + * Checks if a key exists in mirror topic. * * @return True/False if key exists in mirror topic */ diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java index 40d7e86d..05cddf25 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java @@ -83,9 +83,9 @@ public PartitionedMirrorClient(final String topicName, final MirrorHost mirrorHo @Override @Nullable public V fetchValue(final K key) { - final MirrorHost currentKeyHost = this.router.findHost(key); - return this.requestManager.sendRequest(Objects.requireNonNull(currentKeyHost).forKey(key.toString()), - this.parser::deserialize); + final MirrorHost currentKeyHost = Objects.requireNonNull(this.router.findHost(key), + String.format("Could not find the a Mirror host for key %s", key)); + return this.requestManager.sendRequest(currentKeyHost.forKey(key.toString()), this.parser::deserialize); } @Override @@ -109,9 +109,10 @@ public List fetchValues(final List keys) { @Override @Nullable public List fetchRange(final K key, final String from, final String to) { - final MirrorHost currentKeyHost = this.router.findHost(key); - return this.requestManager.sendRequest( - Objects.requireNonNull(currentKeyHost).forRange(key.toString(), from, to), this.parser::deserializeList); + final MirrorHost currentKeyHost = Objects.requireNonNull(this.router.findHost(key), + String.format("Could not find the a Mirror host for key %s", key)); + return this.requestManager.sendRequest(currentKeyHost.forRange(key.toString(), from, to), + this.parser::deserializeList); } @Override diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java index 15c19066..39fc02dc 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java @@ -52,7 +52,7 @@ * * @see com.bakdata.quick.gateway.fetcher.RangeQueryFetcher */ -public class RangeFetcherRule implements DataFetcherRule{ +public class RangeFetcherRule implements DataFetcherRule { @Override public List extractDataFetchers(final TopicDirectiveContext context) { Objects.requireNonNull(context.getTopicDirective().getKeyArgument()); @@ -75,7 +75,6 @@ public boolean isValid(final TopicDirectiveContext context) { && context.getTopicDirective().hasRangeFrom() && context.getTopicDirective().hasRangeTo() && context.isListType() - && context.getEnvironment().getElement().getArguments().size() == 3 - && context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE); + && context.getEnvironment().getElement().getArguments().size() == 3; } } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java index 9d46954c..b53638ad 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java @@ -21,11 +21,9 @@ import java.util.Optional; /** - * Validation for range queries. These rules should apply: - * 1. The Parent container should be a Query and not Mutation/Subscription - * 2. Both rangeFrom and rangeTo fields should exist in the topic directive - * 3. A valid keyArgument should exist in the topic directive - * 4. The return type of the query should be list + * Validation for range queries. These rules should apply: 1. The Parent container should be a Query and not + * Mutation/Subscription 2. Both rangeFrom and rangeTo fields should exist in the topic directive 3. A valid keyArgument + * should exist in the topic directive 4. The return type of the query should be list *

*

Valid schema:

*
{@code
@@ -66,8 +64,7 @@ public Optional validate(final TopicDirectiveContext context) {
     }
 
     private static boolean checkIfItIsRange(final TopicDirectiveContext context) {
-        return context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE)
-            && context.getTopicDirective().hasRangeFrom()
+        return context.getTopicDirective().hasRangeFrom()
             && context.getTopicDirective().hasRangeTo();
     }
 }
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java
index 520b7df8..6bc8731a 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java
@@ -23,7 +23,10 @@
 import java.util.Objects;
 import java.util.Optional;
 
-public class ValidationUtility {
+public final class ValidationUtility {
+
+    private ValidationUtility() {
+    }
 
     public static Optional makeCheckForKeyArgument(final TopicDirectiveContext context) {
         if (context.getTopicDirective().getKeyArgument() != null) {
diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java
index f2489ebd..f76050b1 100644
--- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java
+++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java
@@ -43,6 +43,7 @@
 import java.util.Map;
 import lombok.Builder;
 import lombok.Value;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
 
@@ -142,14 +143,42 @@ void shouldExecuteRange(final TestInfo testInfo) throws IOException {
         assertThat(data.get("userRequests"))
             .isNotNull()
             .hasSize(3)
-            .satisfies(userRequest -> {
-                assertThat(userRequest.get(0).get("requests")).isEqualTo(5);
+            .satisfies(userRequest -> assertThat(userRequest.get(0).get("requests")).isEqualTo(5))
+            .satisfies(userRequest -> assertThat(userRequest.get(1).get("requests")).isEqualTo(10))
+            .satisfies(userRequest -> assertThat(userRequest.get(2).get("requests")).isEqualTo(8));
+    }
 
-            }).satisfies(userRequest -> {
-                assertThat(userRequest.get(1).get("requests")).isEqualTo(10);
-            }).satisfies(userRequest -> {
-                assertThat(userRequest.get(2).get("requests")).isEqualTo(8);
-            });
+    @Test
+    @Disabled
+    void shouldExecuteRangeOnField(final TestInfo testInfo) throws IOException {
+        final String name = testInfo.getTestMethod().orElseThrow().getName();
+        final Path schemaPath = workingDirectory.resolve(name + ".graphql");
+        final Path queryPath = workingDirectory.resolve(name + "Query.graphql");
+
+        final GraphQLSchema schema = this.generator.create(Files.readString(schemaPath));
+        final GraphQL graphQL = GraphQL.newGraphQL(schema).build();
+
+        final DataFetcherClient dataFetcherClient = this.supplier.getClients().get("info-topic");
+
+        final List productInfo = List.of(
+            ProductInfo.builder().info(Info.builder().key(1).timestamp(1).build()).build(),
+            ProductInfo.builder().info(Info.builder().key(1).timestamp(2).build()).build(),
+            ProductInfo.builder().info(Info.builder().key(1).timestamp(3).build()).build()
+        );
+
+        when(dataFetcherClient.fetchRange("1", "1", "3")).thenAnswer(invocation -> productInfo);
+
+        final ExecutionResult executionResult = graphQL.execute(Files.readString(queryPath));
+
+        assertThat(executionResult.getErrors()).isEmpty();
+
+        final Map>> data = executionResult.getData();
+        assertThat(data.get("product"))
+            .isNotNull()
+            .hasSize(3)
+            .satisfies(userRequest -> assertThat(userRequest.get(0).get("requests")).isEqualTo(5))
+            .satisfies(userRequest -> assertThat(userRequest.get(1).get("requests")).isEqualTo(10))
+            .satisfies(userRequest -> assertThat(userRequest.get(2).get("requests")).isEqualTo(8));
     }
 
     @Test
@@ -355,6 +384,12 @@ private void registerTopics() {
             new TopicData("user-request-range", TopicWriteType.MUTABLE, QuickTopicType.INTEGER, QuickTopicType.AVRO,
                 "")
         ).blockingAwait();
+
+        this.registryClient.register(
+            "info-topic",
+            new TopicData("info-topic", TopicWriteType.MUTABLE, QuickTopicType.INTEGER, QuickTopicType.AVRO,
+                "")
+        ).blockingAwait();
     }
 
     @Value
@@ -388,4 +423,17 @@ private static class UserRequest {
         int timestamp;
         int requests;
     }
+
+    @Value
+    @Builder
+    private static class ProductInfo {
+        Info info;
+    }
+
+    @Value
+    @Builder
+    private static class Info {
+        int key;
+        int timestamp;
+    }
 }
diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java
index 0dd140e5..682c7b2e 100644
--- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java
+++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java
@@ -278,6 +278,25 @@ void shouldConvertQueryWithRange(final TestInfo testInfo) throws IOException {
             .containsExactly("user-request-range", "userId", "timestampFrom", "timestampTo");
     }
 
+    @Test
+    void shouldConvertQueryWithRangeOnField(final TestInfo testInfo) throws IOException {
+        final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql");
+        final GraphQLSchema schema = this.generator.create(Files.readString(schemaPath));
+
+        final List topicDirectiveArguments =
+            GraphQLTestUtil.getTopicDirectiveArgumentsFromField("ProductInfo", "info", schema);
+
+        assertThat(topicDirectiveArguments)
+            .hasSize(4)
+            .extracting(GraphQLArgument::getName)
+            .containsExactly("name", "keyArgument", "rangeFrom", "rangeTo");
+
+        assertThat(topicDirectiveArguments)
+            .hasSize(4)
+            .extracting(GraphQLArgument::getValue)
+            .containsExactly("info-topic", "key", "timestampFrom", "timestampTo");
+    }
+
     @Test
     void shouldConvertQueryAllWithComplexType(final TestInfo testInfo) throws IOException {
         final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql");
diff --git a/gateway/src/test/resources/schema/conversion/shouldConvertQueryWithRangeOnField.graphql b/gateway/src/test/resources/schema/conversion/shouldConvertQueryWithRangeOnField.graphql
new file mode 100644
index 00000000..476f53ef
--- /dev/null
+++ b/gateway/src/test/resources/schema/conversion/shouldConvertQueryWithRangeOnField.graphql
@@ -0,0 +1,12 @@
+type Query {
+    product(key: Int, timestampFrom: Int, timestampTo: Int): ProductInfo
+}
+
+type ProductInfo {
+    info: [Info] @topic(name: "info-topic", keyArgument: "key", rangeFrom: "timestampFrom", rangeTo: "timestampTo")
+}
+
+type Info {
+    key: Int
+    timestamp: Int
+}
diff --git a/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnField.graphql b/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnField.graphql
new file mode 100644
index 00000000..476f53ef
--- /dev/null
+++ b/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnField.graphql
@@ -0,0 +1,12 @@
+type Query {
+    product(key: Int, timestampFrom: Int, timestampTo: Int): ProductInfo
+}
+
+type ProductInfo {
+    info: [Info] @topic(name: "info-topic", keyArgument: "key", rangeFrom: "timestampFrom", rangeTo: "timestampTo")
+}
+
+type Info {
+    key: Int
+    timestamp: Int
+}
diff --git a/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnFieldQuery.graphql b/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnFieldQuery.graphql
new file mode 100644
index 00000000..d808afc1
--- /dev/null
+++ b/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnFieldQuery.graphql
@@ -0,0 +1,7 @@
+{
+    product(key: 1, timestampFrom: 1, timestampTo: 3)  {
+        info {
+            key
+        }
+    }
+}

From 6a862c6251d1d4320d629d958c655abbed561ca6 Mon Sep 17 00:00:00 2001
From: Ramin Gharib 
Date: Tue, 6 Sep 2022 16:44:49 +0200
Subject: [PATCH 10/20] refactor test

---
 e2e/functional/range/tests.bats               |  2 +-
 .../fetcher/RangeQueryFetcherTest.java        | 48 +++++++++++--------
 2 files changed, 29 insertions(+), 21 deletions(-)

diff --git a/e2e/functional/range/tests.bats b/e2e/functional/range/tests.bats
index decc3bf1..d1b0acb0 100644
--- a/e2e/functional/range/tests.bats
+++ b/e2e/functional/range/tests.bats
@@ -60,7 +60,7 @@ setup() {
 teardown() {
     if [[ "${#BATS_TEST_NAMES[@]}" -eq "$BATS_TEST_NUMBER" ]]; then
         quick gateway delete ${GATEWAY}
-        #TODO: Uncomment the topic deletion
+        #TODO: Uncomment the topic deletion after the Range Mirror implementation is ready
         # quick topic delete ${TOPIC}
     fi
 }
diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java
index efa31de7..9f29a1f7 100644
--- a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java
+++ b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java
@@ -50,26 +50,26 @@ class RangeQueryFetcherTest {
 
     @Test
     void shouldFetchListWhenListArgumentOfTypeString() throws JsonProcessingException {
-        final UserRequest userRequest = UserRequest.builder()
-            .userId(1)
-            .timestamp(1)
-            .requests(3)
-            .build();
-
-        final UserRequest userRequest2 = UserRequest.builder()
-            .userId(1)
-            .timestamp(2)
-            .requests(5)
-            .build();
+        final List userRequests = List.of(
+            UserRequest.builder()
+                .userId(1)
+                .timestamp(1)
+                .requests(Request.builder().count(10).successful(8).build())
+                .build(),
+            UserRequest.builder()
+                .userId(1)
+                .timestamp(2)
+                .requests(Request.builder().count(10).successful(8).build())
+                .build()
+        );
 
         this.server.enqueue(
-            new MockResponse().setBody(
-                this.mapper.writeValueAsString(new MirrorValue<>(List.of(userRequest, userRequest2)))));
+            new MockResponse().setBody(this.mapper.writeValueAsString(new MirrorValue<>(userRequests))));
 
-        final DataFetcherClient fetcherClient = this.createClient(UserRequest.class);
+        final DataFetcherClient fetcherClient = this.createClient();
 
         final RangeQueryFetcher rangeQueryFetcher =
-            new RangeQueryFetcher<>("userId", fetcherClient, "timestampFrom","timestampTo", isNullable);
+            new RangeQueryFetcher<>("userId", fetcherClient, "timestampFrom", "timestampTo", isNullable);
 
         final Map arguments = Map.of("userId", "1", "timestampFrom", "1", "timestampTo", "2");
 
@@ -77,7 +77,7 @@ void shouldFetchListWhenListArgumentOfTypeString() throws JsonProcessingExceptio
             .localContext(arguments).build();
 
         final List actual = rangeQueryFetcher.get(env);
-        assertThat(actual).isEqualTo(List.of(userRequest, userRequest2));
+        assertThat(actual).isEqualTo(userRequests);
     }
 
     @Test
@@ -88,7 +88,7 @@ void shouldFetchEmptyListWhenResultIsNullAndReturnTypeIsNotNullable() {
         Mockito.when(fetcherClient.fetchRange(anyString(), anyString(), anyString())).thenReturn(null);
 
         final RangeQueryFetcher rangeQueryFetcher =
-            new RangeQueryFetcher<>("userId", fetcherClient, "timestampFrom","timestampTo", false);
+            new RangeQueryFetcher<>("userId", fetcherClient, "timestampFrom", "timestampTo", false);
 
         final Map arguments = Map.of("userId", "1", "timestampFrom", "1", "timestampTo", "2");
 
@@ -101,8 +101,8 @@ void shouldFetchEmptyListWhenResultIsNullAndReturnTypeIsNotNullable() {
     }
 
     @NotNull
-    private  MirrorDataFetcherClient createClient(final Class clazz) {
-        final TypeResolver resolver = new KnownTypeResolver<>(clazz, this.mapper);
+    private MirrorDataFetcherClient createClient() {
+        final TypeResolver resolver = new KnownTypeResolver<>(UserRequest.class, this.mapper);
         return new MirrorDataFetcherClient<>(this.host, this.client, this.mirrorConfig, resolver);
     }
 
@@ -111,6 +111,14 @@ private  MirrorDataFetcherClient createClient(final Class clazz) {
     private static class UserRequest {
         int userId;
         int timestamp;
-        int requests;
+        Request requests;
     }
+
+    @Value
+    @Builder
+    private static class Request {
+        int count;
+        int successful;
+    }
+
 }

From 4a57a99c356b4902e3c3e8a6b2156b6e298ed268 Mon Sep 17 00:00:00 2001
From: Ramin Gharib 
Date: Wed, 7 Sep 2022 09:25:57 +0200
Subject: [PATCH 11/20] fix reange query test

---
 .../api/client/DefaultMirrorClient.java       |  8 ++--
 .../api/client/PartitionedMirrorClient.java   | 38 +++++++++--------
 .../fetcher/RangeQueryFetcherTest.java        | 42 +++++++++++--------
 3 files changed, 50 insertions(+), 38 deletions(-)

diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java
index 4a741f73..8acd111f 100644
--- a/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java
+++ b/common/src/main/java/com/bakdata/quick/common/api/client/DefaultMirrorClient.java
@@ -94,10 +94,12 @@ public List fetchValues(final List keys) {
     @Override
     @Nullable
     public List fetchRange(final K key, final String from, final String rangeTo) {
-        return this.mirrorRequestManager.sendRequest(
-            this.host.forRange(key.toString(), from, rangeTo),
-            this.parser::deserializeList
+        final ResponseWrapper response = this.mirrorRequestManager.makeRequest(
+            this.host.forRange(key.toString(), from, rangeTo)
         );
+        return Objects.requireNonNullElse(
+            this.mirrorRequestManager.processResponse(response, this.parser::deserializeList),
+            Collections.emptyList());
     }
 
     @Override
diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java
index 6c429868..ccf4e9e0 100644
--- a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java
+++ b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java
@@ -37,8 +37,8 @@
 import org.apache.kafka.common.serialization.Serde;
 
 /**
- * MirrorClient that has access to information about partition-host mapping.
- * This information enables it to efficiently route requests in the case when there is more than one mirror replica.
+ * MirrorClient that has access to information about partition-host mapping. This information enables it to efficiently
+ * route requests in the case when there is more than one mirror replica.
  *
  * @param  key type
  * @param  value type
@@ -58,19 +58,18 @@ public class PartitionedMirrorClient implements MirrorClient {
     private final List knownHosts;
 
     /**
-     * Next to its default task of instantiation PartitionHost, it takes responsibility for
-     * creating several business objects and initializing the PartitionRouter with a mapping
-     * retrieved from StreamController.
+     * Next to its default task of instantiation PartitionHost, it takes responsibility for creating several business
+     * objects and initializing the PartitionRouter with a mapping retrieved from StreamController.
      *
-     * @param mirrorHost      mirror host to use
-     * @param client          http client
-     * @param keySerde        the serde for the key
-     * @param valueResolver   the value's {@link TypeResolver}
+     * @param mirrorHost mirror host to use
+     * @param client http client
+     * @param keySerde the serde for the key
+     * @param valueResolver the value's {@link TypeResolver}
      * @param partitionFinder strategy for finding partitions
      */
     public PartitionedMirrorClient(final MirrorHost mirrorHost, final HttpClient client,
-                                   final Serde keySerde, final TypeResolver valueResolver,
-                                   final PartitionFinder partitionFinder) {
+        final Serde keySerde, final TypeResolver valueResolver,
+        final PartitionFinder partitionFinder) {
         this.topicName = mirrorHost.getHost();
         this.streamsStateHost = StreamsStateHost.fromMirrorHost(mirrorHost);
         this.client = client;
@@ -126,8 +125,13 @@ public List fetchValues(final List keys) {
     public List fetchRange(final K key, final String from, final String to) {
         final MirrorHost currentKeyHost = Objects.requireNonNull(this.router.findHost(key),
             String.format("Could not find the a Mirror host for key %s", key));
-        return this.requestManager.sendRequest(currentKeyHost.forRange(key.toString(), from, to),
-            this.parser::deserializeList);
+        final ResponseWrapper response = this.requestManager
+            .makeRequest(Objects.requireNonNull(currentKeyHost).forRange(key.toString(), from, to));
+        if (response.isUpdateCacheHeaderSet()) {
+            log.debug("The update header has been set. Updating router info.");
+            this.updateRouterInfo();
+        }
+        return this.requestManager.processResponse(response, this.parser::deserializeList);
     }
 
     @Override
@@ -149,11 +153,9 @@ private Map makeRequestForPartitionHostMapping() {
             }
             final Map partitionHostMappingResponse = this.client.objectMapper()
                 .readValue(responseBody.byteStream(), MAP_TYPE_REFERENCE);
-            if (log.isInfoEnabled()) {
-                log.info("Collected information about the partitions and hosts."
-                        + " There are {} partitions and {} distinct hosts", partitionHostMappingResponse.size(),
-                    (int) partitionHostMappingResponse.values().stream().distinct().count());
-            }
+            log.info("Collected information about the partitions and hosts."
+                    + " There are {} partitions and {} distinct hosts", partitionHostMappingResponse.size(),
+                (int) partitionHostMappingResponse.values().stream().distinct().count());
             return partitionHostMappingResponse;
         } catch (final IOException exception) {
             throw new MirrorException("There was a problem handling the response: ",
diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java
index 9f29a1f7..6dc2aa3b 100644
--- a/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java
+++ b/gateway/src/test/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcherTest.java
@@ -36,7 +36,7 @@
 import okhttp3.OkHttpClient;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
-import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
@@ -48,23 +48,33 @@ class RangeQueryFetcherTest {
     private final MirrorConfig mirrorConfig = MirrorConfig.directAccess();
     private final String host = String.format("localhost:%s", this.server.getPort());
 
+    @BeforeEach
+    void initRouterAndMirror() throws JsonProcessingException {
+        // mapping from partition to host for initializing PartitionRouter
+        final String routerBody = TestUtils.generateBodyForRouterWith(Map.of(1, this.host, 2, this.host));
+        this.server.enqueue(new MockResponse().setBody(routerBody));
+    }
+
     @Test
-    void shouldFetchListWhenListArgumentOfTypeString() throws JsonProcessingException {
-        final List userRequests = List.of(
-            UserRequest.builder()
-                .userId(1)
-                .timestamp(1)
-                .requests(Request.builder().count(10).successful(8).build())
-                .build(),
-            UserRequest.builder()
-                .userId(1)
-                .timestamp(2)
-                .requests(Request.builder().count(10).successful(8).build())
-                .build()
+    void shouldFetchRange() throws JsonProcessingException {
+        final UserRequest userRequest1 = UserRequest.builder()
+            .userId(1)
+            .timestamp(1)
+            .requests(Request.builder().count(10).successful(8).build())
+            .build();
+        final UserRequest userRequest2 = UserRequest.builder()
+            .userId(1)
+            .timestamp(2)
+            .requests(Request.builder().count(10).successful(8).build())
+            .build();
+
+        final List userRequests = List.of(
+            userRequest1,
+            userRequest2
         );
 
-        this.server.enqueue(
-            new MockResponse().setBody(this.mapper.writeValueAsString(new MirrorValue<>(userRequests))));
+        final String userRequestJson = this.mapper.writeValueAsString(new MirrorValue<>(userRequests));
+        this.server.enqueue(new MockResponse().setBody(userRequestJson));
 
         final DataFetcherClient fetcherClient = this.createClient();
 
@@ -100,7 +110,6 @@ void shouldFetchEmptyListWhenResultIsNullAndReturnTypeIsNotNullable() {
         assertThat(actual).isEqualTo(Collections.emptyList());
     }
 
-    @NotNull
     private MirrorDataFetcherClient createClient() {
         final TypeResolver resolver = new KnownTypeResolver<>(UserRequest.class, this.mapper);
         return new MirrorDataFetcherClient<>(this.host, this.client, this.mirrorConfig, resolver);
@@ -120,5 +129,4 @@ private static class Request {
         int count;
         int successful;
     }
-
 }

From 0bd64aba0da5a9c6c519af9b3e766742e3a1df47 Mon Sep 17 00:00:00 2001
From: Ramin Gharib 
Date: Wed, 7 Sep 2022 13:02:16 +0200
Subject: [PATCH 12/20] revert range on field

---
 .../rule/fetcher/ListArgumentFetcherRule.java |  2 +
 .../topic/rule/fetcher/RangeFetcherRule.java  |  7 ++-
 .../topic/rule/validation/RangeArguments.java |  8 ++--
 .../gateway/GraphQLQueryExecutionTest.java    | 47 -------------------
 .../gateway/GraphQLSchemaGeneratorTest.java   | 24 ++--------
 ...ldNotCovertIfRangeIsDefinedOnField.graphql | 12 +++++
 .../shouldExecuteRangeOnField.graphql         | 12 -----
 .../shouldExecuteRangeOnFieldQuery.graphql    |  7 ---
 8 files changed, 29 insertions(+), 90 deletions(-)
 create mode 100644 gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeIsDefinedOnField.graphql
 delete mode 100644 gateway/src/test/resources/schema/execution/shouldExecuteRangeOnField.graphql
 delete mode 100644 gateway/src/test/resources/schema/execution/shouldExecuteRangeOnFieldQuery.graphql

diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/ListArgumentFetcherRule.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/ListArgumentFetcherRule.java
index 73123e1a..5a057970 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/ListArgumentFetcherRule.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/ListArgumentFetcherRule.java
@@ -65,6 +65,8 @@ public List extractDataFetchers(final TopicDirectiveCo
     @Override
     public boolean isValid(final TopicDirectiveContext context) {
         return context.getTopicDirective().hasKeyArgument()
+            && !context.getTopicDirective().hasRangeFrom()
+            && !context.getTopicDirective().hasRangeTo()
             && context.isListType()
             && !context.getParentContainerName().equals(GraphQLUtils.SUBSCRIPTION_TYPE)
             && GraphQLTypeUtil.isList(context.getEnvironment().getElement().getType());
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java
index 39fc02dc..132f38b1 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/fetcher/RangeFetcherRule.java
@@ -21,6 +21,8 @@
 import com.bakdata.quick.gateway.directives.topic.TopicDirectiveContext;
 import graphql.schema.DataFetcher;
 import graphql.schema.FieldCoordinates;
+import graphql.schema.GraphQLTypeUtil;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 
@@ -74,7 +76,8 @@ public boolean isValid(final TopicDirectiveContext context) {
         return context.getTopicDirective().hasKeyArgument()
             && context.getTopicDirective().hasRangeFrom()
             && context.getTopicDirective().hasRangeTo()
-            && context.isListType()
-            && context.getEnvironment().getElement().getArguments().size() == 3;
+            && !context.getParentContainerName().equals(GraphQLUtils.SUBSCRIPTION_TYPE)
+            && context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE)
+            && GraphQLTypeUtil.isList(context.getEnvironment().getElement().getType());
     }
 }
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
index b53638ad..47e57a3b 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
@@ -50,8 +50,10 @@
 public class RangeArguments implements ValidationRule {
     @Override
     public Optional validate(final TopicDirectiveContext context) {
-        if (checkIfItIsRange(context)) {
-            if (!context.getTopicDirective().hasKeyArgument()) {
+        if (hasRangeFromAndRangeTo(context)) {
+            if (!context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE)) {
+                return Optional.of("Range queries is only supported on Query types.");
+            } else if (!context.getTopicDirective().hasKeyArgument()) {
                 return Optional.of("You must define a keyArgument.");
             } else if (!context.isListType()) {
                 return Optional.of("The return type of range queries should be a list.");
@@ -63,7 +65,7 @@ public Optional validate(final TopicDirectiveContext context) {
         return Optional.of("Both rangeFrom and rangeTo arguments should be set.");
     }
 
-    private static boolean checkIfItIsRange(final TopicDirectiveContext context) {
+    private static boolean hasRangeFromAndRangeTo(final TopicDirectiveContext context) {
         return context.getTopicDirective().hasRangeFrom()
             && context.getTopicDirective().hasRangeTo();
     }
diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java
index f76050b1..8479a50b 100644
--- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java
+++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java
@@ -43,7 +43,6 @@
 import java.util.Map;
 import lombok.Builder;
 import lombok.Value;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
 
@@ -148,39 +147,6 @@ void shouldExecuteRange(final TestInfo testInfo) throws IOException {
             .satisfies(userRequest -> assertThat(userRequest.get(2).get("requests")).isEqualTo(8));
     }
 
-    @Test
-    @Disabled
-    void shouldExecuteRangeOnField(final TestInfo testInfo) throws IOException {
-        final String name = testInfo.getTestMethod().orElseThrow().getName();
-        final Path schemaPath = workingDirectory.resolve(name + ".graphql");
-        final Path queryPath = workingDirectory.resolve(name + "Query.graphql");
-
-        final GraphQLSchema schema = this.generator.create(Files.readString(schemaPath));
-        final GraphQL graphQL = GraphQL.newGraphQL(schema).build();
-
-        final DataFetcherClient dataFetcherClient = this.supplier.getClients().get("info-topic");
-
-        final List productInfo = List.of(
-            ProductInfo.builder().info(Info.builder().key(1).timestamp(1).build()).build(),
-            ProductInfo.builder().info(Info.builder().key(1).timestamp(2).build()).build(),
-            ProductInfo.builder().info(Info.builder().key(1).timestamp(3).build()).build()
-        );
-
-        when(dataFetcherClient.fetchRange("1", "1", "3")).thenAnswer(invocation -> productInfo);
-
-        final ExecutionResult executionResult = graphQL.execute(Files.readString(queryPath));
-
-        assertThat(executionResult.getErrors()).isEmpty();
-
-        final Map>> data = executionResult.getData();
-        assertThat(data.get("product"))
-            .isNotNull()
-            .hasSize(3)
-            .satisfies(userRequest -> assertThat(userRequest.get(0).get("requests")).isEqualTo(5))
-            .satisfies(userRequest -> assertThat(userRequest.get(1).get("requests")).isEqualTo(10))
-            .satisfies(userRequest -> assertThat(userRequest.get(2).get("requests")).isEqualTo(8));
-    }
-
     @Test
     void shouldExecuteListQueryWithSingleFieldAndModification(final TestInfo testInfo) throws IOException {
         final String name = testInfo.getTestMethod().orElseThrow().getName();
@@ -423,17 +389,4 @@ private static class UserRequest {
         int timestamp;
         int requests;
     }
-
-    @Value
-    @Builder
-    private static class ProductInfo {
-        Info info;
-    }
-
-    @Value
-    @Builder
-    private static class Info {
-        int key;
-        int timestamp;
-    }
 }
diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java
index 682c7b2e..a70b14c0 100644
--- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java
+++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java
@@ -278,25 +278,6 @@ void shouldConvertQueryWithRange(final TestInfo testInfo) throws IOException {
             .containsExactly("user-request-range", "userId", "timestampFrom", "timestampTo");
     }
 
-    @Test
-    void shouldConvertQueryWithRangeOnField(final TestInfo testInfo) throws IOException {
-        final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql");
-        final GraphQLSchema schema = this.generator.create(Files.readString(schemaPath));
-
-        final List topicDirectiveArguments =
-            GraphQLTestUtil.getTopicDirectiveArgumentsFromField("ProductInfo", "info", schema);
-
-        assertThat(topicDirectiveArguments)
-            .hasSize(4)
-            .extracting(GraphQLArgument::getName)
-            .containsExactly("name", "keyArgument", "rangeFrom", "rangeTo");
-
-        assertThat(topicDirectiveArguments)
-            .hasSize(4)
-            .extracting(GraphQLArgument::getValue)
-            .containsExactly("info-topic", "key", "timestampFrom", "timestampTo");
-    }
-
     @Test
     void shouldConvertQueryAllWithComplexType(final TestInfo testInfo) throws IOException {
         final Path schemaPath = workingDirectory.resolve(testInfo.getTestMethod().orElseThrow().getName() + ".graphql");
@@ -543,6 +524,11 @@ void shouldNotCovertIfRangeFromArgumentIsMissing(final TestInfo testInfo) throws
         this.assertQuickDirectiveExceptionMessage(testInfo, "Both rangeFrom and rangeTo arguments should be set.");
     }
 
+    @Test
+    void shouldNotCovertIfRangeIsDefinedOnField(final TestInfo testInfo) throws  IOException {
+        this.assertQuickDirectiveExceptionMessage(testInfo, "Range queries is only supported on Query types.");
+    }
+
     @Test
     void shouldNotCovertIfKeyArgumentInRangeQueryIsMissing(final TestInfo testInfo) throws  IOException {
         this.assertQuickDirectiveExceptionMessage(testInfo, "You must define a keyArgument.");
diff --git a/gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeIsDefinedOnField.graphql b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeIsDefinedOnField.graphql
new file mode 100644
index 00000000..971c24be
--- /dev/null
+++ b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeIsDefinedOnField.graphql
@@ -0,0 +1,12 @@
+type Query {
+    product(key: Int, timestampFrom: Int, timestampTo: Int): ProductInfo!
+}
+
+type ProductInfo {
+    info: [Info!] @topic(name: "info-topic", keyArgument: "key", rangeFrom: "timestampFrom", rangeTo: "timestampTo")
+}
+
+type Info {
+    key: Int
+    timestamp: Int!
+}
diff --git a/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnField.graphql b/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnField.graphql
deleted file mode 100644
index 476f53ef..00000000
--- a/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnField.graphql
+++ /dev/null
@@ -1,12 +0,0 @@
-type Query {
-    product(key: Int, timestampFrom: Int, timestampTo: Int): ProductInfo
-}
-
-type ProductInfo {
-    info: [Info] @topic(name: "info-topic", keyArgument: "key", rangeFrom: "timestampFrom", rangeTo: "timestampTo")
-}
-
-type Info {
-    key: Int
-    timestamp: Int
-}
diff --git a/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnFieldQuery.graphql b/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnFieldQuery.graphql
deleted file mode 100644
index d808afc1..00000000
--- a/gateway/src/test/resources/schema/execution/shouldExecuteRangeOnFieldQuery.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-    product(key: 1, timestampFrom: 1, timestampTo: 3)  {
-        info {
-            key
-        }
-    }
-}

From 650df67cc7b5a1059ad168c18ecb8e6798e3bfbe Mon Sep 17 00:00:00 2001
From: Ramin Gharib 
Date: Wed, 7 Sep 2022 13:27:28 +0200
Subject: [PATCH 13/20] Update shouldNotCovertIfRangeIsDefinedOnField.graphql

---
 .../conversion/shouldNotCovertIfRangeIsDefinedOnField.graphql    | 1 +
 1 file changed, 1 insertion(+)

diff --git a/gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeIsDefinedOnField.graphql b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeIsDefinedOnField.graphql
index 971c24be..c97a3138 100644
--- a/gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeIsDefinedOnField.graphql
+++ b/gateway/src/test/resources/schema/conversion/shouldNotCovertIfRangeIsDefinedOnField.graphql
@@ -1,3 +1,4 @@
+# Invalid Schema
 type Query {
     product(key: Int, timestampFrom: Int, timestampTo: Int): ProductInfo!
 }

From 0ea8b0ab291a62565615c866c114024f81a58446 Mon Sep 17 00:00:00 2001
From: Ramin Gharib 
Date: Thu, 8 Sep 2022 13:42:05 +0200
Subject: [PATCH 14/20] Add reviews

---
 .../directives/topic/rule/validation/RangeArguments.java | 2 +-
 .../topic/rule/validation/ValidationUtility.java         | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
index 47e57a3b..c573cf6d 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
@@ -52,7 +52,7 @@ public class RangeArguments implements ValidationRule {
     public Optional validate(final TopicDirectiveContext context) {
         if (hasRangeFromAndRangeTo(context)) {
             if (!context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE)) {
-                return Optional.of("Range queries is only supported on Query types.");
+                return Optional.of("Range queries are only supported on Query types.");
             } else if (!context.getTopicDirective().hasKeyArgument()) {
                 return Optional.of("You must define a keyArgument.");
             } else if (!context.isListType()) {
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java
index 6bc8731a..d2f6bae0 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java
@@ -23,11 +23,20 @@
 import java.util.Objects;
 import java.util.Optional;
 
+/**
+ * Utilities to validate the topic directive
+ */
 public final class ValidationUtility {
 
     private ValidationUtility() {
     }
 
+    /**
+     * Checks if the keyArgument of a topic directive is valid or not
+     *
+     * @param context Topic directive context
+     * @return Optional empty if the keyArgument is valid
+     */
     public static Optional makeCheckForKeyArgument(final TopicDirectiveContext context) {
         if (context.getTopicDirective().getKeyArgument() != null) {
             final boolean inputNameAndKeyArgsMatch;

From 6951ab831311dbc1d65f474c3ba3a1103ff21f24 Mon Sep 17 00:00:00 2001
From: Ramin Gharib 
Date: Thu, 8 Sep 2022 13:47:55 +0200
Subject: [PATCH 15/20] Add checkstyle

---
 .../common/api/model/mirror/MirrorHost.java   |  2 +-
 .../directives/topic/TopicDirective.java      |  2 ++
 .../topic/rule/validation/RangeArguments.java |  1 +
 .../rule/validation/ValidationUtility.java    |  4 ++--
 .../quick/gateway/fetcher/FetcherFactory.java | 20 +++++++++----------
 .../gateway/fetcher/QueryListFetcher.java     |  1 +
 .../gateway/fetcher/RangeQueryFetcher.java    |  9 +++++++++
 7 files changed, 26 insertions(+), 13 deletions(-)

diff --git a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java
index 22f132e1..0c554438 100644
--- a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java
+++ b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java
@@ -71,7 +71,7 @@ public String forAll() {
     }
 
     /**
-     * Generates a URL for fetching a range of keys
+     * Generates a URL for fetching a range of keys.
      */
     public String forRange(final String key, final String from, final String to) {
         return String.format("http://%s%s/%s/%s?from=%s&to=%s", this.config.getPrefix(), this.host,
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java
index 97ff0d45..052c52ca 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/TopicDirective.java
@@ -129,9 +129,11 @@ public boolean hasKeyArgument() {
     public boolean hasKeyField() {
         return this.keyField != null;
     }
+
     public boolean hasRangeFrom() {
         return this.rangeFrom != null;
     }
+
     public boolean hasRangeTo() {
         return this.rangeTo != null;
     }
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
index c573cf6d..b5e716e6 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java
@@ -24,6 +24,7 @@
  * Validation for range queries. These rules should apply: 1. The Parent container should be a Query and not
  * Mutation/Subscription 2. Both rangeFrom and rangeTo fields should exist in the topic directive 3. A valid keyArgument
  * should exist in the topic directive 4. The return type of the query should be list
+ *
  * 

*

Valid schema:

*
{@code
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java
index d2f6bae0..a67610c4 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/ValidationUtility.java
@@ -24,7 +24,7 @@
 import java.util.Optional;
 
 /**
- * Utilities to validate the topic directive
+ * Utilities to validate the topic directive.
  */
 public final class ValidationUtility {
 
@@ -32,7 +32,7 @@ private ValidationUtility() {
     }
 
     /**
-     * Checks if the keyArgument of a topic directive is valid or not
+     * Checks if the keyArgument of a topic directive is valid or not.
      *
      * @param context Topic directive context
      * @return Optional empty if the keyArgument is valid
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/FetcherFactory.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/FetcherFactory.java
index 7c8713d0..105b9146 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/FetcherFactory.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/FetcherFactory.java
@@ -55,7 +55,7 @@ public class FetcherFactory {
      */
     @VisibleForTesting
     public FetcherFactory(final KafkaConfig kafkaConfig, final ObjectMapper objectMapper,
-                          final ClientSupplier clientSupplier, final TopicTypeService topicTypeService) {
+        final ClientSupplier clientSupplier, final TopicTypeService topicTypeService) {
         this.kafkaConfig = kafkaConfig;
         this.objectMapper = objectMapper;
         this.clientSupplier = clientSupplier;
@@ -69,14 +69,14 @@ public FetcherFactory(final KafkaConfig kafkaConfig, final ObjectMapper objectMa
      */
     @Inject
     public FetcherFactory(final KafkaConfig kafkaConfig,
-                          final HttpClient client, final MirrorConfig mirrorConfig,
-                          final TopicTypeService topicTypeService) {
+        final HttpClient client, final MirrorConfig mirrorConfig,
+        final TopicTypeService topicTypeService) {
         this(kafkaConfig, client.objectMapper(),
             new DefaultClientSupplier(client, topicTypeService, mirrorConfig), topicTypeService);
     }
 
     public  DataFetcher> subscriptionFetcher(final String topic, final String operationName,
-                                                                @Nullable final String argument) {
+        @Nullable final String argument) {
         final Lazy> topicData = this.getTopicData(topic);
         return new SubscriptionFetcher<>(this.kafkaConfig, topicData, operationName, argument);
     }
@@ -87,19 +87,19 @@ public  DataFetcher queryFetcher(final String topic, final String argument
     }
 
     public  DataFetcher> queryListFetcher(final String topic, final boolean isNullable,
-                                                     final boolean hasNullableElements) {
+        final boolean hasNullableElements) {
         final DataFetcherClient client = this.clientSupplier.createClient(topic);
         return new QueryListFetcher<>(client, isNullable, hasNullableElements);
     }
 
     public  DataFetcher> listArgumentFetcher(final String topic, final String argument,
-                                                        final boolean isNullable, final boolean hasNullableElements) {
+        final boolean isNullable, final boolean hasNullableElements) {
         final DataFetcherClient client = this.clientSupplier.createClient(topic);
         return new ListArgumentFetcher<>(argument, client, isNullable, hasNullableElements);
     }
 
-    public  DataFetcher> rangeFetcher(final String topic, final String argument, final String rangeFrom, final
-        String rangeTo, final boolean isNullable) {
+    public  DataFetcher> rangeFetcher(final String topic, final String argument, final String rangeFrom,
+        final String rangeTo, final boolean isNullable) {
         final DataFetcherClient client = this.clientSupplier.createClient(topic);
         return new RangeQueryFetcher<>(argument, client, rangeFrom, rangeTo, isNullable);
     }
@@ -110,7 +110,7 @@ public  DataFetcher> rangeFetcher(final String topic, final String ar
      * @see MutationFetcher
      */
     public  DataFetcher mutationFetcher(final String topic, final String keyArgumentName,
-                                                 final String valueArgumentName) {
+        final String valueArgumentName) {
         final Lazy> data = this.getTopicDataWithTopicTypeService(topic);
         return new MutationFetcher<>(topic,
             keyArgumentName,
@@ -130,7 +130,7 @@ public DataFetcher keyFieldFetcher(final String topic, final String keyF
     }
 
     public  SubscriptionProvider subscriptionProvider(final String topic, final String operationName,
-                                                                  @Nullable final String argument) {
+        @Nullable final String argument) {
         return new KafkaSubscriptionProvider<>(this.kafkaConfig, this.getTopicData(topic), operationName,
             argument);
     }
diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java
index 01676106..d3ecf21a 100644
--- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java
+++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/QueryListFetcher.java
@@ -35,6 +35,7 @@
  *   }
  * }
  * 
+ *
  * 

* There, the gateway must fetch all purchases from the corresponding mirror as there is no argument. This is done by * this data fetcher. diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java index e9b6dcd6..e6b430a8 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/RangeQueryFetcher.java @@ -33,6 +33,15 @@ public class RangeQueryFetcher implements DataFetcher> { private final DataFetcherClient dataFetcherClient; private final boolean isNullable; + /** + * Standard constructor. + * + * @param argument name of the argument to extract key from + * @param dataFetcherClient underlying HTTP mirror client + * @param rangeFrom name of the range from field + * @param rangeTo name of the range to field + * @param isNullable true if list itself can be null + */ public RangeQueryFetcher(final String argument, final DataFetcherClient dataFetcherClient, final String rangeFrom, final String rangeTo, final boolean isNullable) { From 3d6ebc567fc54a944344fc6fdede76becf973100 Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Thu, 8 Sep 2022 13:53:32 +0200 Subject: [PATCH 16/20] Update files --- .../com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java index a70b14c0..6f075085 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLSchemaGeneratorTest.java @@ -526,7 +526,7 @@ void shouldNotCovertIfRangeFromArgumentIsMissing(final TestInfo testInfo) throws @Test void shouldNotCovertIfRangeIsDefinedOnField(final TestInfo testInfo) throws IOException { - this.assertQuickDirectiveExceptionMessage(testInfo, "Range queries is only supported on Query types."); + this.assertQuickDirectiveExceptionMessage(testInfo, "Range queries are only supported on Query types."); } @Test From ae02c43a5036f2a776a1dc3688b1efe7e389d20f Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Thu, 8 Sep 2022 13:55:29 +0200 Subject: [PATCH 17/20] Update user-requests.json --- e2e/functional/range/user-requests.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/functional/range/user-requests.json b/e2e/functional/range/user-requests.json index 4e8ae1e2..478944d5 100644 --- a/e2e/functional/range/user-requests.json +++ b/e2e/functional/range/user-requests.json @@ -59,4 +59,4 @@ "success": 3 } } -] \ No newline at end of file +] From 3b7bb75b9fced0aea538c0eca2503ad24fd72b5c Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Fri, 9 Sep 2022 14:16:53 +0200 Subject: [PATCH 18/20] Add reviews --- .../common/api/client/PartitionedMirrorClient.java | 14 +++++++++----- .../topic/rule/validation/RangeArguments.java | 13 ++++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java index ccf4e9e0..63cb5c21 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/PartitionedMirrorClient.java @@ -124,11 +124,13 @@ public List fetchValues(final List keys) { @Nullable public List fetchRange(final K key, final String from, final String to) { final MirrorHost currentKeyHost = Objects.requireNonNull(this.router.findHost(key), - String.format("Could not find the a Mirror host for key %s", key)); + String.format("Could not find the a Mirror host %s for key %s", this.topicName, key)); final ResponseWrapper response = this.requestManager .makeRequest(Objects.requireNonNull(currentKeyHost).forRange(key.toString(), from, to)); if (response.isUpdateCacheHeaderSet()) { - log.debug("The update header has been set. Updating router info."); + log.debug("The update header has been set for host {} and key {}. Updating router info.", + this.topicName, + key); this.updateRouterInfo(); } return this.requestManager.processResponse(response, this.parser::deserializeList); @@ -153,9 +155,11 @@ private Map makeRequestForPartitionHostMapping() { } final Map partitionHostMappingResponse = this.client.objectMapper() .readValue(responseBody.byteStream(), MAP_TYPE_REFERENCE); - log.info("Collected information about the partitions and hosts." - + " There are {} partitions and {} distinct hosts", partitionHostMappingResponse.size(), - (int) partitionHostMappingResponse.values().stream().distinct().count()); + if (log.isInfoEnabled()) { + log.info("Collected information about the partitions and hosts." + + " There are {} partitions and {} distinct hosts", partitionHostMappingResponse.size(), + (int) partitionHostMappingResponse.values().stream().distinct().count()); + } return partitionHostMappingResponse; } catch (final IOException exception) { throw new MirrorException("There was a problem handling the response: ", diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java index b5e716e6..d6046c49 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/directives/topic/rule/validation/RangeArguments.java @@ -21,9 +21,16 @@ import java.util.Optional; /** - * Validation for range queries. These rules should apply: 1. The Parent container should be a Query and not - * Mutation/Subscription 2. Both rangeFrom and rangeTo fields should exist in the topic directive 3. A valid keyArgument - * should exist in the topic directive 4. The return type of the query should be list + * Validation for range queries. + * + *

+ * These rules should apply: + *

    + *
  1. The Parent container should be a Query and not Mutation/Subscription + *
  2. Both rangeFrom and rangeTo fields should exist in the topic directive + *
  3. A valid keyArgument should exist in the topic directive + *
  4. The return type of the query should be list + *
* *

*

Valid schema:

From 9db48bf3fa333128790740391b550090f7f98488 Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Mon, 12 Sep 2022 13:26:21 +0200 Subject: [PATCH 19/20] Update range URL --- .../common/api/model/mirror/MirrorHost.java | 2 +- .../common/api/model/mirror/MirrorHostTest.java | 16 ++++++++++------ gradle.properties | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java index 0c554438..7ac8ddb2 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java +++ b/common/src/main/java/com/bakdata/quick/common/api/model/mirror/MirrorHost.java @@ -74,7 +74,7 @@ public String forAll() { * Generates a URL for fetching a range of keys. */ public String forRange(final String key, final String from, final String to) { - return String.format("http://%s%s/%s/%s?from=%s&to=%s", this.config.getPrefix(), this.host, + return String.format("http://%s%s/%s/range/%s?from=%s&to=%s", this.config.getPrefix(), this.host, this.config.getPath(), key, from, to); } diff --git a/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java b/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java index e06204ce..3a8d4de2 100644 --- a/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java +++ b/common/src/test/java/com/bakdata/quick/common/api/model/mirror/MirrorHostTest.java @@ -32,7 +32,8 @@ void shouldConstructCorrectUrlForKeyRequest() { final MirrorHost mirrorHost = new MirrorHost("test-for-key", new MirrorConfig()); final String actual = mirrorHost.forKey("give-me-key"); final String url = "http://%s-test-for-key/%s/give-me-key"; - assertThat(actual).isEqualTo(String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH)); + final String expected = String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH); + assertThat(actual).isEqualTo(expected); } @Test @@ -40,7 +41,8 @@ void shouldConstructCorrectUrlForKeysRequest() { final MirrorHost mirrorHost = new MirrorHost("test-for-keys", new MirrorConfig()); final String actual = mirrorHost.forKeys(List.of("test-1", "test-2", "test-3")); final String url = "http://%s-test-for-keys/%s/keys?ids=test-1,test-2,test-3"; - assertThat(actual).isEqualTo(String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH)); + final String expected = String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH); + assertThat(actual).isEqualTo(expected); } @Test @@ -48,15 +50,17 @@ void shouldConstructCorrectUrlForAllRequest() { final MirrorHost mirrorHost = new MirrorHost("test-for-all", new MirrorConfig()); final String actual = mirrorHost.forAll(); final String url = "http://%s-test-for-all/%s"; - assertThat(actual).isEqualTo(String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH)); + final String expected = String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH); + assertThat(actual).isEqualTo(expected); } @Test void shouldConstructCorrectUrlForRangeRequest() { final MirrorHost mirrorHost = new MirrorHost("test-for-rage", new MirrorConfig()); final String actual = mirrorHost.forRange("test-key", "range-field-from", "range-field-to"); - final String url = "http://%s-test-for-rage/%s/%s?from=%s&to=%s"; - assertThat(actual).isEqualTo( - String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH, "test-key", "range-field-from", "range-field-to")); + final String url = "http://%s-test-for-rage/%s/range/%s?from=%s&to=%s"; + final String expected = + String.format(url, MIRROR_HOST_PREFIX, MIRROR_HOST_PATH, "test-key", "range-field-from", "range-field-to"); + assertThat(actual).isEqualTo(expected); } } diff --git a/gradle.properties b/gradle.properties index d2a436c2..0fc09023 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.caching=true +org.gradle.caching=false org.gradle.parallel=flase # Embedded Kafka does not reliably work in parallel since Kafka 3.0 #----- Sonar # systemProp.sonar.host.url=http://localhost:9000 From 0d146b9515b83fc36302b89584c24468a2665278 Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Mon, 12 Sep 2022 13:28:16 +0200 Subject: [PATCH 20/20] Update gradle.properties --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0fc09023..d2a436c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.caching=false +org.gradle.caching=true org.gradle.parallel=flase # Embedded Kafka does not reliably work in parallel since Kafka 3.0 #----- Sonar # systemProp.sonar.host.url=http://localhost:9000