From 63993408d1fa4ce3f10cf449cf3075c1f73c3de1 Mon Sep 17 00:00:00 2001 From: Ramin Gharib Date: Fri, 13 Jan 2023 10:07:22 +0100 Subject: [PATCH] keyArgument type sets the key serializer (#150) Closes #120 --- .../mirror/DefaultMirrorClientFactory.java | 38 ------------- .../client/mirror/MirrorClientFactory.java | 27 ++++++++-- .../PartitionedMirrorClientFactory.java | 15 +++++- .../type/registry/QuickTopicTypeService.java | 3 +- e2e/functional/key-range/result-range.json | 14 ----- e2e/functional/range-key/purchases.json | 54 +++++++++++++++++++ .../{key-range => range-key}/query-range.gql | 0 e2e/functional/range-key/result-range.json | 14 +++++ .../{key-range => range-key}/schema.gql | 0 .../{key-range => range-key}/tests.bats | 0 .../topic/rule/fetcher/RangeFetcherRule.java | 32 ++++++++++- .../quick/gateway/fetcher/ClientSupplier.java | 5 +- .../fetcher/DefaultClientSupplier.java | 9 ++++ .../quick/gateway/fetcher/FetcherFactory.java | 30 +++++++++-- .../gateway/GraphQLQueryExecutionTest.java | 7 ++- .../quick/gateway/GraphQLTestUtil.java | 9 ++++ justfile | 4 ++ 17 files changed, 192 insertions(+), 69 deletions(-) delete mode 100644 common/src/main/java/com/bakdata/quick/common/api/client/mirror/DefaultMirrorClientFactory.java delete mode 100644 e2e/functional/key-range/result-range.json create mode 100644 e2e/functional/range-key/purchases.json rename e2e/functional/{key-range => range-key}/query-range.gql (100%) create mode 100644 e2e/functional/range-key/result-range.json rename e2e/functional/{key-range => range-key}/schema.gql (100%) rename e2e/functional/{key-range => range-key}/tests.bats (100%) diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/mirror/DefaultMirrorClientFactory.java b/common/src/main/java/com/bakdata/quick/common/api/client/mirror/DefaultMirrorClientFactory.java deleted file mode 100644 index 116c2314..00000000 --- a/common/src/main/java/com/bakdata/quick/common/api/client/mirror/DefaultMirrorClientFactory.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.client.mirror; - -import com.bakdata.quick.common.api.client.HttpClient; -import com.bakdata.quick.common.resolver.TypeResolver; -import com.bakdata.quick.common.type.QuickTopicData; -import com.bakdata.quick.common.util.Lazy; - -/** - * Creates a {@link DefaultMirrorClient}. - */ -public class DefaultMirrorClientFactory implements MirrorClientFactory { - @Override - public MirrorClient createMirrorClient(final HttpClient client, final String topic, - final Lazy> quickTopicData) { - final MirrorHost mirrorHost = MirrorHost.createWithPrefix(topic); - final MirrorRequestManager requestManager = new DefaultMirrorRequestManager(client); - final TypeResolver valueTypeResolver = quickTopicData.get().getValueData().getResolver(); - final MirrorValueParser mirrorValueParser = - new MirrorValueParser<>(valueTypeResolver, client.objectMapper()); - return new DefaultMirrorClient<>(mirrorHost, mirrorValueParser, requestManager); - } -} diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/mirror/MirrorClientFactory.java b/common/src/main/java/com/bakdata/quick/common/api/client/mirror/MirrorClientFactory.java index 18c1d9ad..c41ae0b6 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/mirror/MirrorClientFactory.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/mirror/MirrorClientFactory.java @@ -17,14 +17,35 @@ package com.bakdata.quick.common.api.client.mirror; import com.bakdata.quick.common.api.client.HttpClient; +import com.bakdata.quick.common.resolver.TypeResolver; import com.bakdata.quick.common.type.QuickTopicData; import com.bakdata.quick.common.util.Lazy; +import org.apache.kafka.common.serialization.Serde; /** * Factory for creating {@link MirrorClient}. */ public interface MirrorClientFactory { - MirrorClient createMirrorClient(final HttpClient client, - final String topic, - final Lazy> quickTopicData); + /** + * Creates a non-partition aware {@link DefaultMirrorClient}. + * + * @param client An HTTP client + * @param topic The topic name + * @param quickTopicData The quick topic data + * @param Type of the key + * @param Type of the value + * @return A {@link MirrorClient} + */ + default MirrorClient createMirrorClient(final HttpClient client, final String topic, + final Lazy> quickTopicData) { + final MirrorHost mirrorHost = MirrorHost.createWithPrefix(topic); + final MirrorRequestManager requestManager = new DefaultMirrorRequestManager(client); + final TypeResolver valueTypeResolver = quickTopicData.get().getValueData().getResolver(); + final MirrorValueParser mirrorValueParser = + new MirrorValueParser<>(valueTypeResolver, client.objectMapper()); + return new DefaultMirrorClient<>(mirrorHost, mirrorValueParser, requestManager); + } + + MirrorClient createMirrorClient(final HttpClient client, final String topic, final Serde keySerde, + final TypeResolver typeResolver); } diff --git a/common/src/main/java/com/bakdata/quick/common/api/client/mirror/PartitionedMirrorClientFactory.java b/common/src/main/java/com/bakdata/quick/common/api/client/mirror/PartitionedMirrorClientFactory.java index cf9ab07b..da149703 100644 --- a/common/src/main/java/com/bakdata/quick/common/api/client/mirror/PartitionedMirrorClientFactory.java +++ b/common/src/main/java/com/bakdata/quick/common/api/client/mirror/PartitionedMirrorClientFactory.java @@ -31,8 +31,7 @@ public class PartitionedMirrorClientFactory implements MirrorClientFactory { @Override public MirrorClient createMirrorClient(final HttpClient client, - final String topic, - final Lazy> quickTopicData) { + final String topic, final Lazy> quickTopicData) { final MirrorHost mirrorHost = MirrorHost.createWithPrefix(topic); final MirrorRequestManager requestManager = new MirrorRequestManagerWithFallback(client, mirrorHost); final StreamsStateHost streamsStateHost = StreamsStateHost.createFromMirrorHost(mirrorHost); @@ -43,4 +42,16 @@ public MirrorClient createMirrorClient(final HttpClient client, final TypeResolver valueTypeResolver = quickTopicData.get().getValueData().getResolver(); return new PartitionedMirrorClient<>(client, valueTypeResolver, requestManager, partitionRouter); } + + @Override + public MirrorClient createMirrorClient(final HttpClient client, final String topic, + final Serde keySerde, final TypeResolver valueTypeResolver) { + final MirrorHost mirrorHost = MirrorHost.createWithPrefix(topic); + final MirrorRequestManager requestManager = new MirrorRequestManagerWithFallback(client, mirrorHost); + final StreamsStateHost streamsStateHost = StreamsStateHost.createFromMirrorHost(mirrorHost); + final Router partitionRouter = + new PartitionRouter<>(client, streamsStateHost, keySerde, new DefaultPartitionFinder(), requestManager, + topic); + return new PartitionedMirrorClient<>(client, valueTypeResolver, requestManager, partitionRouter); + } } diff --git a/common/src/main/java/com/bakdata/quick/common/type/registry/QuickTopicTypeService.java b/common/src/main/java/com/bakdata/quick/common/type/registry/QuickTopicTypeService.java index a798344d..95c076f7 100644 --- a/common/src/main/java/com/bakdata/quick/common/type/registry/QuickTopicTypeService.java +++ b/common/src/main/java/com/bakdata/quick/common/type/registry/QuickTopicTypeService.java @@ -137,7 +137,6 @@ private Single> createResolver(final QuickTopicTyp // get schema and configure the resolver with it return this.registryFetcher.getSchema(subject) .doOnError(e -> log.error("No schema found for subject {}", subject, e)) - .map(schema -> new TypeResolverWithSchema<>(this.conversionProvider.getTypeResolver(type, schema), - schema)); + .map(schema -> new TypeResolverWithSchema<>(this.conversionProvider.getTypeResolver(type, schema), schema)); } } diff --git a/e2e/functional/key-range/result-range.json b/e2e/functional/key-range/result-range.json deleted file mode 100644 index 56e23734..00000000 --- a/e2e/functional/key-range/result-range.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "productId": 123, - "price": { - "total": 30.0 - } - }, - { - "productId": 456, - "price": { - "total": 79.99 - } - } -] diff --git a/e2e/functional/range-key/purchases.json b/e2e/functional/range-key/purchases.json new file mode 100644 index 00000000..e14b4237 --- /dev/null +++ b/e2e/functional/range-key/purchases.json @@ -0,0 +1,54 @@ +[ + { + "key": "abc", + "value": { + "productId": 123, + "userId": 1, + "amount": 1, + "price": { + "total": 19.99, + "currency": "DOLLAR" + }, + "timestamp": 1 + } + }, + { + "key": "def", + "value": { + "productId": 123, + "userId": 2, + "amount": 2, + "price": { + "total": 30.00, + "currency": "DOLLAR" + }, + "timestamp": 2 + } + }, + { + "key": "ghi", + "value": { + "productId": 456, + "userId": 2, + "amount": 1, + "price": { + "total": 79.99, + "currency": "DOLLAR" + }, + "timestamp": 3 + } + }, + { + "key": "jkl", + "value": { + "productId": 789, + "userId": 2, + "amount": 1, + "price": { + "total": 99.99, + "currency": "DOLLAR" + }, + "timestamp": 4 + } + } +] diff --git a/e2e/functional/key-range/query-range.gql b/e2e/functional/range-key/query-range.gql similarity index 100% rename from e2e/functional/key-range/query-range.gql rename to e2e/functional/range-key/query-range.gql diff --git a/e2e/functional/range-key/result-range.json b/e2e/functional/range-key/result-range.json new file mode 100644 index 00000000..c57ac77d --- /dev/null +++ b/e2e/functional/range-key/result-range.json @@ -0,0 +1,14 @@ +[ + { + "productId": 123, + "price": { + "total": 30.0 + } + }, + { + "productId": 456, + "price": { + "total": 79.99 + } + } +] diff --git a/e2e/functional/key-range/schema.gql b/e2e/functional/range-key/schema.gql similarity index 100% rename from e2e/functional/key-range/schema.gql rename to e2e/functional/range-key/schema.gql diff --git a/e2e/functional/key-range/tests.bats b/e2e/functional/range-key/tests.bats similarity index 100% rename from e2e/functional/key-range/tests.bats rename to e2e/functional/range-key/tests.bats 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 132f38b1..561dc0c1 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 @@ -18,13 +18,18 @@ import com.bakdata.quick.common.graphql.GraphQLUtils; import com.bakdata.quick.gateway.DataFetcherSpecification; +import com.bakdata.quick.gateway.directives.QuickDirectiveException; import com.bakdata.quick.gateway.directives.topic.TopicDirectiveContext; +import graphql.language.TypeName; import graphql.schema.DataFetcher; import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLSchemaElement; import graphql.schema.GraphQLTypeUtil; -import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; /** * Rule for range query fetcher. @@ -60,12 +65,16 @@ public List extractDataFetchers(final TopicDirectiveCo Objects.requireNonNull(context.getTopicDirective().getKeyArgument()); Objects.requireNonNull(context.getTopicDirective().getRangeFrom()); Objects.requireNonNull(context.getTopicDirective().getRangeTo()); + + final TypeName typeName = this.extractKeyArgumentType(context, context.getTopicDirective().getKeyArgument()); + final DataFetcher dataFetcher = context.getFetcherFactory().rangeFetcher( context.getTopicDirective().getTopicName(), context.getTopicDirective().getKeyArgument(), context.getTopicDirective().getRangeFrom(), context.getTopicDirective().getRangeTo(), - context.isNullable() + context.isNullable(), + typeName ); final FieldCoordinates coordinates = this.currentCoordinates(context); return List.of(DataFetcherSpecification.of(coordinates, dataFetcher)); @@ -80,4 +89,23 @@ public boolean isValid(final TopicDirectiveContext context) { && context.getParentContainerName().equals(GraphQLUtils.QUERY_TYPE) && GraphQLTypeUtil.isList(context.getEnvironment().getElement().getType()); } + + private TypeName extractKeyArgumentType(final TopicDirectiveContext context, final String keyArgument) { + final GraphQLSchemaElement graphQLSchemaElement = context.getEnvironment() + .getElementParentTree() + .getElement(); + + final List arguments = ((GraphQLFieldDefinition) graphQLSchemaElement).getArguments(); + final Optional graphQLKeyArgument = arguments.stream() + .filter(inputValueDefinition -> inputValueDefinition.getName().equals(keyArgument)) + .findFirst(); + + if (graphQLKeyArgument.isEmpty()) { + final String errorMessage = String.format( + "Could not find the keyArgument %s in the parent type definition. Please check your schema.", + keyArgument); + throw new QuickDirectiveException(errorMessage); + } + return this.extractTypeName(graphQLKeyArgument.get().getDefinition().getType()); + } } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/ClientSupplier.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/ClientSupplier.java index bd3568d8..be778cff 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/ClientSupplier.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/ClientSupplier.java @@ -18,11 +18,14 @@ import com.bakdata.quick.common.type.QuickTopicData; import com.bakdata.quick.common.util.Lazy; +import org.apache.kafka.common.serialization.Serde; /** * Supplier for creating a new data fetcher client for a topic. */ -@FunctionalInterface public interface ClientSupplier { DataFetcherClient createClient(final String topic, final Lazy> quickTopicData); + + DataFetcherClient createClient(final String topic, final Serde keySerde, + final Lazy> quickTopicData); } diff --git a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DefaultClientSupplier.java b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DefaultClientSupplier.java index fcfdecaa..7a00a9f3 100644 --- a/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DefaultClientSupplier.java +++ b/gateway/src/main/java/com/bakdata/quick/gateway/fetcher/DefaultClientSupplier.java @@ -20,6 +20,7 @@ import com.bakdata.quick.common.api.client.mirror.MirrorClientFactory; import com.bakdata.quick.common.type.QuickTopicData; import com.bakdata.quick.common.util.Lazy; +import org.apache.kafka.common.serialization.Serde; final class DefaultClientSupplier implements ClientSupplier { private final HttpClient client; @@ -37,4 +38,12 @@ public DataFetcherClient createClient(final String topic, return new MirrorDataFetcherClient<>(new Lazy<>(() -> this.mirrorClientFactory.createMirrorClient(this.client, topic, quickTopicData))); } + + @Override + public DataFetcherClient createClient(final String topic, final Serde keySerde, + final Lazy> quickTopicData) { + return new MirrorDataFetcherClient<>(new Lazy<>(() -> + this.mirrorClientFactory.createMirrorClient(this.client, topic, keySerde, + quickTopicData.get().getValueData().getResolver()))); + } } 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 ff55cfbd..1b8e9ae9 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 @@ -19,7 +19,9 @@ import com.bakdata.quick.common.api.client.HttpClient; import com.bakdata.quick.common.api.client.mirror.PartitionedMirrorClientFactory; import com.bakdata.quick.common.config.KafkaConfig; +import com.bakdata.quick.common.type.ConversionProvider; import com.bakdata.quick.common.type.QuickTopicData; +import com.bakdata.quick.common.type.QuickTopicType; import com.bakdata.quick.common.type.TopicTypeService; import com.bakdata.quick.common.util.Lazy; import com.bakdata.quick.gateway.fetcher.subscription.KafkaSubscriptionProvider; @@ -29,14 +31,20 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import edu.umd.cs.findbugs.annotations.Nullable; +import graphql.Scalars; import graphql.execution.DataFetcherResult; +import graphql.language.NamedNode; import graphql.language.TypeName; +import graphql.scalars.ExtendedScalars; import graphql.schema.DataFetcher; import io.reactivex.Single; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.util.List; +import java.util.Map; +import java.util.Objects; import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.common.serialization.Serde; import org.reactivestreams.Publisher; /** @@ -49,7 +57,14 @@ public class FetcherFactory { private final ObjectMapper objectMapper; private final ClientSupplier clientSupplier; private final TopicTypeService topicTypeService; + private final ConversionProvider conversionProvider; + private static final Map typeMap = Map.of( + Scalars.GraphQLInt.getName(), QuickTopicType.INTEGER, + ExtendedScalars.GraphQLLong.getName(), QuickTopicType.LONG, + Scalars.GraphQLString.getName(), QuickTopicType.STRING, + Scalars.GraphQLID.getName(), QuickTopicType.STRING, + Scalars.GraphQLFloat.getName(), QuickTopicType.DOUBLE); /** * Visible for testing. @@ -57,11 +72,12 @@ public class FetcherFactory { @VisibleForTesting public FetcherFactory(final KafkaConfig kafkaConfig, final ObjectMapper objectMapper, final TopicTypeService topicTypeService, - final ClientSupplier clientSupplier) { + final ClientSupplier clientSupplier, final ConversionProvider conversionProvider) { this.kafkaConfig = kafkaConfig; this.objectMapper = objectMapper; this.topicTypeService = topicTypeService; this.clientSupplier = clientSupplier; + this.conversionProvider = conversionProvider; } /** @@ -71,9 +87,9 @@ public FetcherFactory(final KafkaConfig kafkaConfig, final ObjectMapper objectMa */ @Inject public FetcherFactory(final KafkaConfig kafkaConfig, final HttpClient client, - final TopicTypeService topicTypeService) { + final TopicTypeService topicTypeService, final ConversionProvider conversionProvider) { this(kafkaConfig, client.objectMapper(), topicTypeService, - new DefaultClientSupplier(client, new PartitionedMirrorClientFactory())); + new DefaultClientSupplier(client, new PartitionedMirrorClientFactory()), conversionProvider); } /** @@ -107,8 +123,12 @@ public DataFetcher> listArgumentFetcher(final String topic, final * Creates a {@link RangeQueryFetcher}. */ 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, this.getTopicData(topic)); + final String rangeTo, final boolean isNullable, final NamedNode type) { + final QuickTopicType quickTopicType = Objects.requireNonNull(typeMap.get(type.getName())); + + final Serde keySerde = this.conversionProvider.getSerde(quickTopicType, true); + final Lazy> topicData = this.getTopicData(topic); + final DataFetcherClient client = this.clientSupplier.createClient(topic, keySerde, topicData); return new RangeQueryFetcher<>(argument, client, rangeFrom, rangeTo, isNullable); } 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 63c099fa..3a57f0a0 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLQueryExecutionTest.java @@ -25,6 +25,7 @@ import com.bakdata.quick.common.api.model.TopicData; import com.bakdata.quick.common.api.model.TopicWriteType; import com.bakdata.quick.common.config.KafkaConfig; +import com.bakdata.quick.common.type.ConversionProvider; import com.bakdata.quick.common.type.QuickTopicType; import com.bakdata.quick.common.type.TopicTypeService; import com.bakdata.quick.gateway.GraphQLTestUtil.TestClientSupplier; @@ -359,8 +360,10 @@ private GraphQL getGraphQL(final Path schemaPath, final ClientSupplier clientSup final TopicTypeService topicTypeService = mock(TopicTypeService.class); + final ConversionProvider conversionProvider = mock(ConversionProvider.class); + final FetcherFactory fetcherFactory = new FetcherFactory(kafkaConfig, this.mapper, topicTypeService, - clientSupplier); + clientSupplier, conversionProvider); final QuickDirectiveWiring topicDirectiveWiring = new TopicDirectiveWiring(fetcherFactory); final GraphQLSchemaGenerator graphQLSchemaGenerator = @@ -400,7 +403,7 @@ private void registerTopics() { this.registryClient.register( "user-request-range", - new TopicData("user-request-range", TopicWriteType.MUTABLE, QuickTopicType.INTEGER, QuickTopicType.AVRO, + new TopicData("user-request-range", TopicWriteType.MUTABLE, QuickTopicType.STRING, QuickTopicType.AVRO, "") ).blockingAwait(); 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 71432895..6d691a54 100644 --- a/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java +++ b/gateway/src/test/java/com/bakdata/quick/gateway/GraphQLTestUtil.java @@ -33,6 +33,7 @@ import java.util.Map; import java.util.stream.Collectors; import lombok.Getter; +import org.apache.kafka.common.serialization.Serde; public final class GraphQLTestUtil { private GraphQLTestUtil() { @@ -81,6 +82,14 @@ public DataFetcherClient createClient(final String topic, return client; } + @Override + public DataFetcherClient createClient(final String topic, final Serde keySerde, + final Lazy> quickTopicData) { + final DataFetcherClient client = mock(DataFetcherClient.class); + this.clients.put(topic, client); + return client; + } + @SuppressWarnings("unchecked") public DataFetcherClient getClient(final String client) { return (DataFetcherClient) this.clients.get(client); diff --git a/justfile b/justfile index 6210bcc9..8afcc76d 100644 --- a/justfile +++ b/justfile @@ -133,6 +133,10 @@ e2e-run-multi-stream api-key quick-host: e2e-run-range api-key quick-host: docker run -v {{ e2e-dir }}/range:/tests/range -e X_API_KEY={{ api-key }} -e HOST={{ quick-host }} quick-e2e-test-runner --rm -it +# Runs the e2e range tests +e2e-run-range-key api-key quick-host: + docker run -v {{ e2e-dir }}/range:/tests/range-key -e X_API_KEY={{ api-key }} -e HOST={{ quick-host }} quick-e2e-test-runner --rm -it + # Runs the e2e schema tests e2e-run-schema api-key quick-host: docker run -v {{ e2e-dir }}/schema:/tests/schema -e X_API_KEY={{ api-key }} -e HOST={{ quick-host }} quick-e2e-test-runner --rm -it