graphql.GraphQL Maven / Gradle / Ivy
package graphql;
import graphql.execution.AbortExecutionException;
import graphql.execution.AsyncExecutionStrategy;
import graphql.execution.AsyncSerialExecutionStrategy;
import graphql.execution.Execution;
import graphql.execution.ExecutionId;
import graphql.execution.ExecutionIdProvider;
import graphql.execution.ExecutionStrategy;
import graphql.execution.SubscriptionExecutionStrategy;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.NoOpInstrumentation;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters;
import graphql.execution.preparsed.NoOpPreparsedDocumentProvider;
import graphql.execution.preparsed.PreparsedDocumentEntry;
import graphql.execution.preparsed.PreparsedDocumentProvider;
import graphql.language.Document;
import graphql.parser.Parser;
import graphql.schema.GraphQLSchema;
import graphql.validation.ValidationError;
import graphql.validation.Validator;
import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import static graphql.Assert.assertNotNull;
import static graphql.InvalidSyntaxError.toInvalidSyntaxError;
/**
* This class is where all graphql-java query execution begins. It combines the objects that are needed
* to make a successful graphql query, with the most important being the {@link graphql.schema.GraphQLSchema schema}
* and the {@link graphql.execution.ExecutionStrategy execution strategy}
*
* Building this object is very cheap and can be done on each execution if necessary. Building the schema is often not
* as cheap, especially if its parsed from graphql IDL schema format via {@link graphql.schema.idl.SchemaParser}.
*
* The data for a query is returned via {@link ExecutionResult#getData()} and any errors encountered as placed in
* {@link ExecutionResult#getErrors()}.
*
* Runtime Exceptions
*
* Runtime exceptions can be thrown by the graphql engine if certain situations are encountered. These are not errors
* in execution but rather totally unacceptable conditions in which to execute a graphql query.
*
* - {@link graphql.schema.CoercingSerializeException} - is thrown when a value cannot be serialised by a Scalar type, for example
* a String value being coerced as an Int.
*
*
* - {@link graphql.execution.UnresolvedTypeException} - is thrown if a {@link graphql.schema.TypeResolver} fails to provide a concrete
* object type given a interface or union type.
*
*
* - {@link graphql.schema.validation.InvalidSchemaException} - is thrown if the schema is not valid when built via
* {@link graphql.schema.GraphQLSchema.Builder#build()}
*
*
* - {@link graphql.GraphQLException} - is thrown as a general purpose runtime exception, for example if the code cant
* access a named field when examining a POJO.
*
*
* - {@link graphql.AssertException} - is thrown as a low level code assertion exception for truly unexpected code conditions
*
*
*
*/
@PublicApi
public class GraphQL {
private static final Logger log = LoggerFactory.getLogger(GraphQL.class);
private static final ExecutionIdProvider DEFAULT_EXECUTION_ID_PROVIDER = (query, operationName, context) -> ExecutionId.generate();
private final GraphQLSchema graphQLSchema;
private final ExecutionStrategy queryStrategy;
private final ExecutionStrategy mutationStrategy;
private final ExecutionStrategy subscriptionStrategy;
private final ExecutionIdProvider idProvider;
private final Instrumentation instrumentation;
private final PreparsedDocumentProvider preparsedDocumentProvider;
/**
* A GraphQL object ready to execute queries
*
* @param graphQLSchema the schema to use
*
* @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version.
*/
@Internal
public GraphQL(GraphQLSchema graphQLSchema) {
//noinspection deprecation
this(graphQLSchema, null, null);
}
/**
* A GraphQL object ready to execute queries
*
* @param graphQLSchema the schema to use
* @param queryStrategy the query execution strategy to use
*
* @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version.
*/
@Internal
public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy) {
//noinspection deprecation
this(graphQLSchema, queryStrategy, null);
}
/**
* A GraphQL object ready to execute queries
*
* @param graphQLSchema the schema to use
* @param queryStrategy the query execution strategy to use
* @param mutationStrategy the mutation execution strategy to use
*
* @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version.
*/
@Internal
public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) {
this(graphQLSchema, queryStrategy, mutationStrategy, null, DEFAULT_EXECUTION_ID_PROVIDER, NoOpInstrumentation.INSTANCE, NoOpPreparsedDocumentProvider.INSTANCE);
}
/**
* A GraphQL object ready to execute queries
*
* @param graphQLSchema the schema to use
* @param queryStrategy the query execution strategy to use
* @param mutationStrategy the mutation execution strategy to use
* @param subscriptionStrategy the subscription execution strategy to use
*
* @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version.
*/
@Internal
public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy) {
this(graphQLSchema, queryStrategy, mutationStrategy, subscriptionStrategy, DEFAULT_EXECUTION_ID_PROVIDER, NoOpInstrumentation.INSTANCE, NoOpPreparsedDocumentProvider.INSTANCE);
}
private GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy, ExecutionIdProvider idProvider, Instrumentation instrumentation, PreparsedDocumentProvider preparsedDocumentProvider) {
this.graphQLSchema = assertNotNull(graphQLSchema, "queryStrategy must be non null");
this.queryStrategy = queryStrategy != null ? queryStrategy : new AsyncExecutionStrategy();
this.mutationStrategy = mutationStrategy != null ? mutationStrategy : new AsyncSerialExecutionStrategy();
this.subscriptionStrategy = subscriptionStrategy != null ? subscriptionStrategy : new SubscriptionExecutionStrategy();
this.idProvider = assertNotNull(idProvider, "idProvider must be non null");
this.instrumentation = instrumentation;
this.preparsedDocumentProvider = assertNotNull(preparsedDocumentProvider, "preparsedDocumentProvider must be non null");
}
/**
* Helps you build a GraphQL object ready to execute queries
*
* @param graphQLSchema the schema to use
*
* @return a builder of GraphQL objects
*/
public static Builder newGraphQL(GraphQLSchema graphQLSchema) {
return new Builder(graphQLSchema);
}
/**
* This helps you transform the current GraphQL object into another one by starting a builder with all
* the current values and allows you to transform it how you want.
*
* @param builderConsumer the consumer code that will be given a builder to transform
*
* @return a new GraphQL object based on calling build on that builder
*/
public GraphQL transform(Consumer builderConsumer) {
Builder builder = new Builder(this.graphQLSchema);
builder
.queryExecutionStrategy(nvl(this.queryStrategy, builder.queryExecutionStrategy))
.mutationExecutionStrategy(nvl(this.mutationStrategy, builder.mutationExecutionStrategy))
.subscriptionExecutionStrategy(nvl(this.subscriptionStrategy, builder.subscriptionExecutionStrategy))
.executionIdProvider(nvl(this.idProvider, builder.idProvider))
.instrumentation(nvl(this.instrumentation, builder.instrumentation))
.preparsedDocumentProvider(nvl(this.preparsedDocumentProvider, builder.preparsedDocumentProvider));
builderConsumer.accept(builder);
return builder.build();
}
private static T nvl(T obj, T elseObj) {
return obj == null ? elseObj : obj;
}
@PublicApi
public static class Builder {
private GraphQLSchema graphQLSchema;
private ExecutionStrategy queryExecutionStrategy = new AsyncExecutionStrategy();
private ExecutionStrategy mutationExecutionStrategy = new AsyncSerialExecutionStrategy();
private ExecutionStrategy subscriptionExecutionStrategy = new SubscriptionExecutionStrategy();
private ExecutionIdProvider idProvider = DEFAULT_EXECUTION_ID_PROVIDER;
private Instrumentation instrumentation = NoOpInstrumentation.INSTANCE;
private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE;
public Builder(GraphQLSchema graphQLSchema) {
this.graphQLSchema = graphQLSchema;
}
public Builder schema(GraphQLSchema graphQLSchema) {
this.graphQLSchema = assertNotNull(graphQLSchema, "GraphQLSchema must be non null");
return this;
}
public Builder queryExecutionStrategy(ExecutionStrategy executionStrategy) {
this.queryExecutionStrategy = assertNotNull(executionStrategy, "Query ExecutionStrategy must be non null");
return this;
}
public Builder mutationExecutionStrategy(ExecutionStrategy executionStrategy) {
this.mutationExecutionStrategy = assertNotNull(executionStrategy, "Mutation ExecutionStrategy must be non null");
return this;
}
public Builder subscriptionExecutionStrategy(ExecutionStrategy executionStrategy) {
this.subscriptionExecutionStrategy = assertNotNull(executionStrategy, "Subscription ExecutionStrategy must be non null");
return this;
}
public Builder instrumentation(Instrumentation instrumentation) {
this.instrumentation = assertNotNull(instrumentation, "Instrumentation must be non null");
return this;
}
public Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) {
this.preparsedDocumentProvider = assertNotNull(preparsedDocumentProvider, "PreparsedDocumentProvider must be non null");
return this;
}
public Builder executionIdProvider(ExecutionIdProvider executionIdProvider) {
this.idProvider = assertNotNull(executionIdProvider, "ExecutionIdProvider must be non null");
return this;
}
public GraphQL build() {
assertNotNull(graphQLSchema, "queryStrategy must be non null");
assertNotNull(queryExecutionStrategy, "queryStrategy must be non null");
assertNotNull(idProvider, "idProvider must be non null");
return new GraphQL(graphQLSchema, queryExecutionStrategy, mutationExecutionStrategy, subscriptionExecutionStrategy, idProvider, instrumentation, preparsedDocumentProvider);
}
}
/**
* Executes the specified graphql query/mutation/subscription
*
* @param query the query/mutation/subscription
*
* @return an {@link ExecutionResult} which can include errors
*/
public ExecutionResult execute(String query) {
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.build();
return execute(executionInput);
}
/**
* Info: This sets context = root to be backwards compatible.
*
* @param query the query/mutation/subscription
* @param context custom object provided to each {@link graphql.schema.DataFetcher}
*
* @return an {@link ExecutionResult} which can include errors
*
* @deprecated Use {@link #execute(ExecutionInput)}
*/
@Deprecated
public ExecutionResult execute(String query, Object context) {
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.context(context)
.root(context) // This we are doing do be backwards compatible
.build();
return execute(executionInput);
}
/**
* Info: This sets context = root to be backwards compatible.
*
* @param query the query/mutation/subscription
* @param operationName the name of the operation to execute
* @param context custom object provided to each {@link graphql.schema.DataFetcher}
*
* @return an {@link ExecutionResult} which can include errors
*
* @deprecated Use {@link #execute(ExecutionInput)}
*/
@Deprecated
public ExecutionResult execute(String query, String operationName, Object context) {
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.operationName(operationName)
.context(context)
.root(context) // This we are doing do be backwards compatible
.build();
return execute(executionInput);
}
/**
* Info: This sets context = root to be backwards compatible.
*
* @param query the query/mutation/subscription
* @param context custom object provided to each {@link graphql.schema.DataFetcher}
* @param variables variable values uses as argument
*
* @return an {@link ExecutionResult} which can include errors
*
* @deprecated Use {@link #execute(ExecutionInput)}
*/
@Deprecated
public ExecutionResult execute(String query, Object context, Map variables) {
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.context(context)
.root(context) // This we are doing do be backwards compatible
.variables(variables)
.build();
return execute(executionInput);
}
/**
* Info: This sets context = root to be backwards compatible.
*
* @param query the query/mutation/subscription
* @param operationName name of the operation to execute
* @param context custom object provided to each {@link graphql.schema.DataFetcher}
* @param variables variable values uses as argument
*
* @return an {@link ExecutionResult} which can include errors
*
* @deprecated Use {@link #execute(ExecutionInput)}
*/
@Deprecated
public ExecutionResult execute(String query, String operationName, Object context, Map variables) {
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.operationName(operationName)
.context(context)
.root(context) // This we are doing do be backwards compatible
.variables(variables)
.build();
return execute(executionInput);
}
/**
* Executes the graphql query using the provided input object builder
*
* @param executionInputBuilder {@link ExecutionInput.Builder}
*
* @return an {@link ExecutionResult} which can include errors
*/
public ExecutionResult execute(ExecutionInput.Builder executionInputBuilder) {
return execute(executionInputBuilder.build());
}
/**
* Executes the graphql query using calling the builder function and giving it a new builder.
*
* This allows a lambda style like :
*
* {@code
* ExecutionResult result = graphql.execute(input -> input.query("{hello}").root(startingObj).context(contextObj));
* }
*
*
* @param builderFunction a function that is given a {@link ExecutionInput.Builder}
*
* @return an {@link ExecutionResult} which can include errors
*/
public ExecutionResult execute(UnaryOperator builderFunction) {
return execute(builderFunction.apply(ExecutionInput.newExecutionInput()).build());
}
/**
* Executes the graphql query using the provided input object
*
* @param executionInput {@link ExecutionInput}
*
* @return an {@link ExecutionResult} which can include errors
*/
public ExecutionResult execute(ExecutionInput executionInput) {
try {
return executeAsync(executionInput).join();
} catch (CompletionException e) {
if (e.getCause() instanceof RuntimeException) {
throw (RuntimeException) e.getCause();
} else {
throw e;
}
}
}
/**
* Executes the graphql query using the provided input object builder
*
* This will return a promise (aka {@link CompletableFuture}) to provide a {@link ExecutionResult}
* which is the result of executing the provided query.
*
* @param executionInputBuilder {@link ExecutionInput.Builder}
*
* @return a promise to an {@link ExecutionResult} which can include errors
*/
public CompletableFuture executeAsync(ExecutionInput.Builder executionInputBuilder) {
return executeAsync(executionInputBuilder.build());
}
/**
* Executes the graphql query using the provided input object builder
*
* This will return a promise (aka {@link CompletableFuture}) to provide a {@link ExecutionResult}
* which is the result of executing the provided query.
*
* This allows a lambda style like :
*
* {@code
* ExecutionResult result = graphql.execute(input -> input.query("{hello}").root(startingObj).context(contextObj));
* }
*
*
* @param builderFunction a function that is given a {@link ExecutionInput.Builder}
*
* @return a promise to an {@link ExecutionResult} which can include errors
*/
public CompletableFuture executeAsync(UnaryOperator builderFunction) {
return executeAsync(builderFunction.apply(ExecutionInput.newExecutionInput()).build());
}
/**
* Executes the graphql query using the provided input object
*
* This will return a promise (aka {@link CompletableFuture}) to provide a {@link ExecutionResult}
* which is the result of executing the provided query.
*
* @param executionInput {@link ExecutionInput}
*
* @return a promise to an {@link ExecutionResult} which can include errors
*/
public CompletableFuture executeAsync(ExecutionInput executionInput) {
try {
log.debug("Executing request. operation name: '{}'. query: '{}'. variables '{}'", executionInput.getOperationName(), executionInput.getQuery(), executionInput.getVariables());
InstrumentationState instrumentationState = instrumentation.createState();
InstrumentationExecutionParameters inputInstrumentationParameters = new InstrumentationExecutionParameters(executionInput, this.graphQLSchema, instrumentationState);
executionInput = instrumentation.instrumentExecutionInput(executionInput, inputInstrumentationParameters);
InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(executionInput, this.graphQLSchema, instrumentationState);
InstrumentationContext executionInstrumentation = instrumentation.beginExecution(instrumentationParameters);
GraphQLSchema graphQLSchema = instrumentation.instrumentSchema(this.graphQLSchema, instrumentationParameters);
CompletableFuture executionResult = parseValidateAndExecute(executionInput, graphQLSchema, instrumentationState);
//
// finish up instrumentation
executionResult = executionResult.whenComplete(executionInstrumentation::onEnd);
//
// allow instrumentation to tweak the result
executionResult = executionResult.thenCompose(result -> instrumentation.instrumentExecutionResult(result, instrumentationParameters));
return executionResult;
} catch (AbortExecutionException abortException) {
ExecutionResultImpl executionResult = new ExecutionResultImpl(abortException);
if (!abortException.getUnderlyingErrors().isEmpty()) {
executionResult = new ExecutionResultImpl(abortException.getUnderlyingErrors());
}
return CompletableFuture.completedFuture(executionResult);
}
}
private CompletableFuture parseValidateAndExecute(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) {
PreparsedDocumentEntry preparsedDoc = preparsedDocumentProvider.get(executionInput.getQuery(), query -> parseAndValidate(executionInput, graphQLSchema, instrumentationState));
if (preparsedDoc.hasErrors()) {
return CompletableFuture.completedFuture(new ExecutionResultImpl(preparsedDoc.getErrors()));
}
return execute(executionInput, preparsedDoc.getDocument(), graphQLSchema, instrumentationState);
}
private PreparsedDocumentEntry parseAndValidate(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) {
log.debug("Parsing query: '{}'...", executionInput.getQuery());
ParseResult parseResult = parse(executionInput, graphQLSchema, instrumentationState);
if (parseResult.isFailure()) {
log.warn("Query failed to parse : '{}'", executionInput.getQuery());
return new PreparsedDocumentEntry(toInvalidSyntaxError(parseResult.getException()));
} else {
final Document document = parseResult.getDocument();
log.debug("Validating query: '{}'", executionInput.getQuery());
final List errors = validate(executionInput, document, graphQLSchema, instrumentationState);
if (!errors.isEmpty()) {
log.warn("Query failed to validate : '{}'", executionInput.getQuery());
return new PreparsedDocumentEntry(errors);
}
return new PreparsedDocumentEntry(document);
}
}
private ParseResult parse(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) {
InstrumentationContext parseInstrumentation = instrumentation.beginParse(new InstrumentationExecutionParameters(executionInput, graphQLSchema, instrumentationState));
Parser parser = new Parser();
Document document;
try {
document = parser.parseDocument(executionInput.getQuery());
} catch (ParseCancellationException e) {
parseInstrumentation.onEnd(null, e);
return ParseResult.ofError(e);
}
parseInstrumentation.onEnd(document, null);
return ParseResult.of(document);
}
private List validate(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) {
InstrumentationContext> validationCtx = instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema, instrumentationState));
Validator validator = new Validator();
List validationErrors = validator.validateDocument(graphQLSchema, document);
validationCtx.onEnd(validationErrors, null);
return validationErrors;
}
private CompletableFuture execute(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) {
String query = executionInput.getQuery();
String operationName = executionInput.getOperationName();
Object context = executionInput.getContext();
Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation);
ExecutionId executionId = idProvider.provide(query, operationName, context);
log.debug("Executing '{}'. operation name: '{}'. query: '{}'. variables '{}'", executionId, executionInput.getOperationName(), executionInput.getQuery(), executionInput.getVariables());
CompletableFuture future = execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState);
future.whenComplete((result, throwable) -> {
if (throwable != null) {
log.error(String.format("Execution '%s' threw exception when executing : query : '%s'. variables '%s'", executionId, executionInput.getQuery(), executionInput.getVariables()), throwable);
} else {
int errorCount = result.getErrors().size();
if (errorCount > 0) {
log.debug("Execution '{}' completed with '{}' errors", executionId, errorCount);
} else {
log.debug("Execution '{}' completed with zero errors", executionId);
}
}
});
return future;
}
private static class ParseResult {
private final Document document;
private final Exception exception;
private ParseResult(Document document, Exception exception) {
this.document = document;
this.exception = exception;
}
private boolean isFailure() {
return document == null;
}
private Document getDocument() {
return document;
}
private Exception getException() {
return exception;
}
private static ParseResult of(Document document) {
return new ParseResult(document, null);
}
private static ParseResult ofError(Exception e) {
return new ParseResult(null, e);
}
}
}