Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions src/main/java/graphql/ExecutionInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,26 @@ public class ExecutionInput {
private final Object localContext;
private final Object root;
private final Map<String, Object> variables;
private final Map<String, Object> extensions;
private final DataLoaderRegistry dataLoaderRegistry;
private final CacheControl cacheControl;
private final ExecutionId executionId;
private final Locale locale;


@Internal
private ExecutionInput(String query, String operationName, Object context, Object root, Map<String, Object> variables, DataLoaderRegistry dataLoaderRegistry, CacheControl cacheControl, ExecutionId executionId, Locale locale, Object localContext) {
this.query = assertNotNull(query, () -> "query can't be null");
this.operationName = operationName;
this.context = context;
this.root = root;
this.variables = variables;
this.dataLoaderRegistry = dataLoaderRegistry;
this.cacheControl = cacheControl;
this.executionId = executionId;
this.locale = locale;
this.localContext = localContext;
private ExecutionInput(Builder builder) {
this.query = assertNotNull(builder.query, () -> "query can't be null");
this.operationName = builder.operationName;
this.context = builder.context;
this.root = builder.root;
this.variables = builder.variables;
this.dataLoaderRegistry = builder.dataLoaderRegistry;
this.cacheControl = builder.cacheControl;
this.executionId = builder.executionId;
this.locale = builder.locale;
this.localContext = builder.localContext;
this.extensions = builder.extensions;
}

/**
Expand All @@ -64,6 +66,7 @@ public String getOperationName() {
public Object getContext() {
return context;
}

/**
* @return the local context object to pass to all top level (i.e. query, mutation, subscription) data fetchers
*/
Expand Down Expand Up @@ -115,6 +118,13 @@ public Locale getLocale() {
return locale;
}

/**
* @return a map of extension values that can be sent in to a request
*/
public Map<String, Object> getExtensions() {
return extensions;
}

/**
* This helps you transform the current ExecutionInput object into another one by starting a builder with all
* the current values and allows you to transform it how you want.
Expand All @@ -132,6 +142,7 @@ public ExecutionInput transform(Consumer<Builder> builderConsumer) {
.dataLoaderRegistry(this.dataLoaderRegistry)
.cacheControl(this.cacheControl)
.variables(this.variables)
.extensions(this.extensions)
.executionId(this.executionId)
.locale(this.locale);

Expand Down Expand Up @@ -179,6 +190,7 @@ public static class Builder {
private Object localContext;
private Object root;
private Map<String, Object> variables = Collections.emptyMap();
public Map<String, Object> extensions = Collections.emptyMap();
//
// this is important - it allows code to later known if we never really set a dataloader and hence it can optimize
// dataloader field tracking away.
Expand Down Expand Up @@ -214,7 +226,6 @@ public Builder executionId(ExecutionId executionId) {
* Sets the locale to use for this operation
*
* @param locale the locale to use
*
* @return this builder
*/
public Builder locale(Locale locale) {
Expand All @@ -224,6 +235,7 @@ public Builder locale(Locale locale) {

/**
* Sets initial localContext in root data fetchers
*
* @return this builder
*/
public Builder localContext(Object localContext) {
Expand Down Expand Up @@ -263,6 +275,11 @@ public Builder variables(Map<String, Object> variables) {
return this;
}

public Builder extensions(Map<String, Object> extensions) {
this.extensions = assertNotNull(extensions, () -> "extensions map can't be null");
return this;
}

/**
* You should create new {@link org.dataloader.DataLoaderRegistry}s and new {@link org.dataloader.DataLoader}s for each execution. Do not
* re-use
Expand All @@ -282,7 +299,7 @@ public Builder cacheControl(CacheControl cacheControl) {
}

public ExecutionInput build() {
return new ExecutionInput(query, operationName, context, root, variables, dataLoaderRegistry, cacheControl, executionId, locale, localContext);
return new ExecutionInput(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class NoOpPreparsedDocumentProvider implements PreparsedDocumentProvider
public static final NoOpPreparsedDocumentProvider INSTANCE = new NoOpPreparsedDocumentProvider();

@Override
public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function<ExecutionInput, PreparsedDocumentEntry> computeFunction) {
return computeFunction.apply(executionInput);
public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {
return parseAndValidateFunction.apply(executionInput);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

/**
* An instance of a preparsed document entry represents the result of a query parse and validation, like
* an either implementation it contains either the correct result in th document property or the errors.
* an either implementation it contains either the correct result in the document property or the errors.
*
* NOTE: This class implements {@link java.io.Serializable} and hence it can be serialised and placed into a distributed cache. However we
* are not aiming to provide long term compatibility and do not intend for you to place this serialised data into permanent storage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@
import java.util.function.Function;

/**
* Interface that allows clients to hook in Document caching and/or the whitelisting of queries
* Interface that allows clients to hook in Document caching and/or the whitelisting of queries.
*/
@PublicSpi
public interface PreparsedDocumentProvider {
/**
* This is called to get a "cached" pre-parsed query and if its not present, then the computeFunction
* can be called to parse and validate the query
*
* @param executionInput The {@link graphql.ExecutionInput} containing the query
* @param computeFunction If the query has not be pre-parsed, this function can be called to parse it
* This is called to get a "cached" pre-parsed query and if its not present, then the "parseAndValidateFunction"
* can be called to parse and validate the query.
* <p>
* Note - the "parseAndValidateFunction" MUST be called if you dont have a per parsed version of the query because it not only parses
* and validates the query, it invokes {@link graphql.execution.instrumentation.Instrumentation} calls as well for parsing and validation.
* if you dont make a call back on this then these wont happen.
*
* @param executionInput The {@link graphql.ExecutionInput} containing the query
* @param parseAndValidateFunction If the query has not be pre-parsed, this function MUST be called to parse and validate it
* @return an instance of {@link PreparsedDocumentEntry}
*/
PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function<ExecutionInput, PreparsedDocumentEntry> computeFunction);
PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a much better name for the callback parameter because that's what it does

}


Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package graphql.execution.preparsed.persisted;

import graphql.ExecutionInput;
import graphql.PublicApi;

import java.util.Map;
import java.util.Optional;

/**
* This persisted query support class supports the Apollo scheme where the persisted
* query id is in {@link graphql.ExecutionInput#getExtensions()}.
* <p>
* You need to provide a {@link PersistedQueryCache} cache implementation
* as the backing cache.
* <p>
* See <a href="https://www.apollographql.com/docs/apollo-server/performance/apq/">Apollo Persisted Queries</a>
* <p>
* The Apollo client sends a hash of the persisted query in the input extensions in the following form
* <p>
* <pre>
* {
* "extensions":{
* "persistedQuery":{
* "version":1,
* "sha256Hash":"fcf31818e50ac3e818ca4bdbc433d6ab73176f0b9d5f9d5ad17e200cdab6fba4"
* }
* }
* }
* </pre>
*
* @see graphql.ExecutionInput#getExtensions()
*/
@PublicApi
public class ApolloPersistedQuerySupport extends PersistedQuerySupport {

public ApolloPersistedQuerySupport(PersistedQueryCache persistedQueryCache) {
super(persistedQueryCache);
}

@SuppressWarnings("unchecked")
@Override
protected Optional<Object> getPersistedQueryId(ExecutionInput executionInput) {
Map<String, Object> extensions = executionInput.getExtensions();
Map<String, Object> persistedQuery = (Map<String, Object>) extensions.get("persistedQuery");
if (persistedQuery != null) {
Object sha256Hash = persistedQuery.get("sha256Hash");
return Optional.ofNullable(sha256Hash);
}
return Optional.empty();
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Later if there is a V2, we can update this

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package graphql.execution.preparsed.persisted;

import graphql.Assert;
import graphql.ExecutionInput;
import graphql.PublicApi;
import graphql.execution.preparsed.PreparsedDocumentEntry;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* A PersistedQueryCache that is just an in memory map of known queries.
*/
@PublicApi
public class InMemoryPersistedQueryCache implements PersistedQueryCache {

private final Map<Object, PreparsedDocumentEntry> cache = new ConcurrentHashMap<>();
private final Map<Object, String> knownQueries;

public InMemoryPersistedQueryCache(Map<Object, String> knownQueries) {
this.knownQueries = Assert.assertNotNull(knownQueries);
}

public Map<Object, String> getKnownQueries() {
return knownQueries;
}

@Override
public PreparsedDocumentEntry getPersistedQueryDocument(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound {
return cache.compute(persistedQueryId, (k, v) -> {
if (v != null) {
return v;
}
String queryText = knownQueries.get(persistedQueryId);
if (queryText == null) {
throw new PersistedQueryNotFound(persistedQueryId);
}
return onCacheMiss.apply(queryText);
});
}

public static Builder newInMemoryPersistedQueryCache() {
return new Builder();
}

public static class Builder {
private final Map<Object, String> knownQueries = new HashMap<>();

public Builder addQuery(Object key, String queryText) {
knownQueries.put(key, queryText);
return this;
}

public InMemoryPersistedQueryCache build() {
return new InMemoryPersistedQueryCache(knownQueries);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package graphql.execution.preparsed.persisted;

import graphql.ExecutionInput;
import graphql.PublicSpi;
import graphql.execution.preparsed.PreparsedDocumentEntry;

/**
* This interface is used to abstract an actual cache that can cache parsed persistent queries.
*/
@PublicSpi
public interface PersistedQueryCache {

/**
* This is called to get a persisted query from cache.
* <p>
* If its present in cache then it must return a PreparsedDocumentEntry where {@link graphql.execution.preparsed.PreparsedDocumentEntry#getDocument()}
* is already parsed and validated. This will be passed onto the graphql engine as is.
* <p>
* If its a valid query id but its no present in cache, (cache miss) then you need to call back the "onCacheMiss" function with associated query text.
* This will be compiled and validated by the graphql engine and the a PreparsedDocumentEntry will be passed back ready for you to cache it.
* <p>
* If its not a valid query id then throw a {@link graphql.execution.preparsed.persisted.PersistedQueryNotFound} to indicate this.
*
* @param persistedQueryId the persisted query id
* @param executionInput the original execution input
* @param onCacheMiss the call back should it be a valid query id but its not currently not in the cache
* @return a parsed and validated PreparsedDocumentEntry where {@link graphql.execution.preparsed.PreparsedDocumentEntry#getDocument()} is set
* @throws graphql.execution.preparsed.persisted.PersistedQueryNotFound if the query id is not know at all and you have no query text
*/
PreparsedDocumentEntry getPersistedQueryDocument(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package graphql.execution.preparsed.persisted;

import graphql.PublicApi;
import graphql.execution.preparsed.PreparsedDocumentEntry;

import java.util.function.Function;

/**
* The call back when a valid persisted query is not in cache and it needs to be compiled and validated
* by the graphql engine. If you get a cache miss in your {@link graphql.execution.preparsed.persisted.PersistedQueryCache} implementation
* then you are required to call back on the provided instance of this interface
*/
@PublicApi
public interface PersistedQueryCacheMiss extends Function<String, PreparsedDocumentEntry> {
/**
* You give back the missing query text and graphql-java will compile and validate it.
*
* @param queryToBeParsedAndValidated the query text to be parsed and validated
* @return a parsed and validated query document ready for caching
*/
@Override
PreparsedDocumentEntry apply(String queryToBeParsedAndValidated);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package graphql.execution.preparsed.persisted;

import graphql.ErrorClassification;
import graphql.PublicApi;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* An exception that indicates the query id is not valid and can be found ever in cache
*/
@PublicApi
public class PersistedQueryNotFound extends RuntimeException implements ErrorClassification {
private final Object persistedQueryId;

public PersistedQueryNotFound(Object persistedQueryId) {
this.persistedQueryId = persistedQueryId;
}

@Override
public String getMessage() {
return "PersistedQueryNotFound";
}

public Object getPersistedQueryId() {
return persistedQueryId;
}

@Override
public String toString() {
return "PersistedQueryNotFound";
}

public Map<String, Object> getExtensions() {
LinkedHashMap<String, Object> extensions = new LinkedHashMap<>();
extensions.put("persistedQueryId", persistedQueryId);
extensions.put("generatedBy", "graphql-java");
return extensions;
}
}
Loading